├── .nvmrc
├── .eslintignore
├── .gitignore
├── .prettierrc
├── test
├── setup.js
├── helper.test.js
└── express-elasticsearch-logger.test.js
├── .editorconfig
├── .eslintrc.js
├── .travis.yml
├── LICENSE
├── lib
├── readme.hbs
├── elasticsearch.js
├── helper.js
├── config.js
└── express-elasticsearch-logger.js
├── express
├── bin
│ └── www
└── app.js
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 10
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea/
3 | *.log
4 | **DS_Store
5 | coverage.html
6 | npm-debug.log
7 | lib-cov
8 | dist/tests.js
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": false,
6 | "trailingComma": "all",
7 | "jsxBracketSameLine": true
8 | }
9 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai")
2 | const sinonChai = require("sinon-chai")
3 | const chaiAsPromised = require("chai-as-promised")
4 | const chaid = require("chaid")
5 |
6 | chai.use(sinonChai).use(chaiAsPromised).use(chaid)
7 | chai.should()
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = false
10 | insert_final_newline = false
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs")
2 | const path = require("path")
3 | const prettierOptions = JSON.parse(
4 | fs.readFileSync(path.join(__dirname, "./.prettierrc"), "utf8"),
5 | )
6 |
7 | module.exports = {
8 | parserOptions: {
9 | allowImportExportEverywhere: true,
10 | },
11 | extends: ["airbnb-base", "prettier"],
12 | env: {
13 | mocha: true,
14 | node: true,
15 | es6: true,
16 | },
17 | rules: {
18 | "no-underscore-dangle":"off",
19 | "prettier/prettier": [2, prettierOptions],
20 | "no-console": "error",
21 | "func-names": ["error", "never"],
22 | quotes: ["error", "double"],
23 | semi: ["error", "never"],
24 | },
25 | plugins: ["prettier", "mocha"],
26 | }
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 8.9.4
4 | deploy:
5 | provider: npm
6 | email: junrai82@gmail.com
7 | on:
8 | tags: true
9 | api_key:
10 | secure: LyIT8M3N7VlrTydgLwJ0zB6YXwLS8b/4tP9FKi1yDXgazJ2nyJGZqzGFstjmP/FCD8iQ15EVrvWoBU5vIpSsRxHuOX9VVzx9efaKNIEDeaIpvdXw76KHOXLFsoeCG/XTyZq1KnLSzo+9l6ZlkRD3kCZKHpN4S/H/UmXRsmH6TYqimq5hPcLk8ngB3dFFGHH2C00LAV6n4BN1RwY9H6iPNA+iDzrTzakcpk+e2lflJ4K3F/SXAm7u4VAmx5gE5Kwjn3YO3LXIWDgYyRQrSR0JhGOVy3sdx88A8WsvOKsCgqXdV1QmmfkjQWQjQ4KTcqaXhjaO0ndsreQ5bJ4ighqb23efaRLAD/PppZpQb0GkJ3mEc//MxT3MaAU8MhOhoL3xgPLnmrDQshjxfcgGZVrGl0l5FcIxJ7+9cKu0GBM+pikK5VFrN2OPLdkdjja4d/TzTrTJj9ECd/xY3leNu+OSyC1jwRj6K5ivX4PUL6Z+BThy5w5A+eFlpvTr1vB4uhbjKQEFn5X5OmAT/34OetFqh2udk7qGYYY6Mfw+xgyqIKpmaXH4A+Lb4aPvw1eklLzw22i1Cf7oxzDmgnIviOlaGt36IQKGfBAFLnIOrrLQLriXRXtdAoaD7Rb1CQzEHlvATKUonAgucSZmRvWPG3xjIlU/RlFZtFF03/zEoUUfAe4=
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # Open Works License
2 |
3 | This is version 0.9.4 of the Open Works License
4 |
5 | ## Terms
6 |
7 | Permission is hereby granted by the holder(s) of copyright or other legal
8 | privileges, author(s) or assembler(s), and contributor(s) of this work, to any
9 | person who obtains a copy of this work in any form, to reproduce, modify,
10 | distribute, publish, sell, sublicense, use, and/or otherwise deal in the
11 | licensed material without restriction, provided the following conditions are
12 | met:
13 |
14 | Redistributions, modified or unmodified, in whole or in part, must retain
15 | applicable copyright and other legal privilege notices, the above license
16 | notice, these conditions, and the following disclaimer.
17 |
18 | NO WARRANTY OF ANY KIND IS IMPLIED BY, OR SHOULD BE INFERRED FROM, THIS LICENSE
19 | OR THE ACT OF DISTRIBUTION UNDER THE TERMS OF THIS LICENSE, INCLUDING BUT NOT
20 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
21 | AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS, ASSEMBLERS, OR HOLDERS OF
22 | COPYRIGHT OR OTHER LEGAL PRIVILEGE BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
23 | LIABILITY, WHETHER IN ACTION OF CONTRACT, TORT, OR OTHERWISE ARISING FROM, OUT
24 | OF, OR IN CONNECTION WITH THE WORK OR THE USE OF OR OTHER DEALINGS IN THE WORK.
25 |
--------------------------------------------------------------------------------
/lib/readme.hbs:
--------------------------------------------------------------------------------
1 | # express-elasticsearch-logger [](http://travis-ci.org/alexmingoia/express-elasticsearch-logger) [](https://coveralls.io/r/alexmingoia/express-elasticsearch-logger) [](https://www.npmjs.org/package/express-elasticsearch-logger) [](https://david-dm.org/alexmingoia/express-elasticsearch-logger)
2 |
3 | > Log Express app requests to ElasticSearch.
4 |
5 | {{#module name="express-elasticsearch-logger"}}{{>body}}{{/module}}## Installation
6 |
7 | Install using [npm](https://www.npmjs.org/):
8 |
9 | ```sh
10 | npm install express-elasticsearch-logger
11 | ```
12 |
13 | ## API Reference
14 |
15 | {{#module name="express-elasticsearch-logger"}}{{>exported}}{{/module}}## Contributing
16 |
17 | Please submit all issues and pull requests to the [alexmingoia/express-elasticsearch-logger](http://github.com/alexmingoia/express-elasticsearch-logger) repository!
18 |
19 | ## Tasks
20 |
21 | List available tasks with `gulp help`.
22 |
23 | ## Tests
24 |
25 | Run tests using `npm test` or `gulp test`.
26 |
27 | ## Support
28 |
29 | If you have any problem or suggestion please open an issue [here](https://github.com/alexmingoia/express-elasticsearch-logger/issues).
30 |
--------------------------------------------------------------------------------
/lib/elasticsearch.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /**
3 | * Log `request` to elasticsearch.
4 | *
5 | * @param {String} index index name
6 | * @param {elasticsearch.Client} client
7 | * @param {Object} doc document to save
8 | * @private
9 | */
10 | function log(doc, index, client) {
11 | client.index(
12 | {
13 | index,
14 | body: doc,
15 | },
16 | function (error) {
17 | if (error) {
18 | console.error(error)
19 | }
20 | },
21 | )
22 | }
23 |
24 | /**
25 | * Ensure log index and request mapping exist.
26 | *
27 | * @param {Object} config
28 | * @param {elasticsearch.Client} client
29 | * @param {Function} done
30 | * @private
31 | */
32 | function ensureIndexExists(config, client, done) {
33 | const { settings, mapping: mappings } = config
34 |
35 | client.indices.exists(
36 | {
37 | index: config.index,
38 | },
39 | (err, response) => {
40 | if (err) {
41 | done(err)
42 | return
43 | }
44 | const { body } = response
45 | if (body) {
46 | client.indices.putMapping(
47 | {
48 | index: config.index,
49 | body: config.mapping,
50 | },
51 | done,
52 | )
53 | } else {
54 | client.indices.create(
55 | {
56 | index: config.index,
57 | body: {
58 | mappings,
59 | ...(settings && { settings }),
60 | },
61 | },
62 | done,
63 | )
64 | }
65 | },
66 | )
67 | }
68 |
69 | module.exports = {
70 | log,
71 | ensureIndexExists,
72 | }
73 |
--------------------------------------------------------------------------------
/express/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('express:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "express-elasticsearch-logger",
3 | "description": "Log Express app requests to ElasticSearch.",
4 | "version": "4.0.0",
5 | "homepage": "https://github.com/alexmingoia/express-elasticsearch-logger",
6 | "author": {
7 | "name": "Alex Mingoia",
8 | "email": "talk@alexmingoia.com"
9 | },
10 | "contributors": [
11 | {
12 | "name": "Potsawee Vechpanich",
13 | "email": "potsawee@rentspree.com"
14 | }
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/alexmingoia/express-elasticsearch-logger.git"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/alexmingoia/express-elasticsearch-logger/issues"
22 | },
23 | "licenses": [
24 | {
25 | "type": "Open Works License (OWL)",
26 | "url": "https://github.com/alexmingoia/express-elasticsearch-logger/blob/master/LICENSE"
27 | }
28 | ],
29 | "main": "lib/express-elasticsearch-logger.js",
30 | "files": [
31 | "dist",
32 | "lib"
33 | ],
34 | "engines": {
35 | "node": ">= 8",
36 | "npm": ">= 5"
37 | },
38 | "scripts": {
39 | "test": "mocha 'test/**/*.test.js' --require ./test/setup.js",
40 | "lint": "eslint .",
41 | "lint:fix": "npm run lint -- --fix",
42 | "start-express": "DEBUG=* node ./express/bin/www",
43 | "watch": "npm-watch"
44 | },
45 | "watch": {
46 | "start-express": "{lib,express}/*.js"
47 | },
48 | "dependencies": {
49 | "@elastic/elasticsearch": "^7.6.1",
50 | "clone-deep": "^0.2.4"
51 | },
52 | "devDependencies": {
53 | "chai": "^4.2.0",
54 | "chai-as-promised": "^7.1.1",
55 | "chaid": "^1.0.2",
56 | "cookie-parser": "^1.4.5",
57 | "debug": "~2.6.9",
58 | "eslint": "^6.8.0",
59 | "eslint-config-airbnb-base": "^14.1.0",
60 | "eslint-config-prettier": "^6.11.0",
61 | "eslint-plugin-import": "^2.20.2",
62 | "eslint-plugin-mocha": "^6.3.0",
63 | "eslint-plugin-prettier": "^3.1.3",
64 | "express": "^4.17.1",
65 | "http-errors": "^1.7.3",
66 | "jsdoc-to-markdown": "^5.0.3",
67 | "jshint": "^2.9.2",
68 | "jshint-stylish": "^2.1.0",
69 | "mocha": "^7.1.2",
70 | "mocha-lcov-reporter": "1.2.0",
71 | "npm-watch": "^0.6.0",
72 | "prettier": "^2.0.5",
73 | "sinon": "^7.0.0",
74 | "sinon-chai": "^3.5.0",
75 | "supertest": "^1.2.0",
76 | "vinyl-buffer": "^1.0.0",
77 | "vinyl-source-stream": "^1.0.0"
78 | },
79 | "keywords": [
80 | "express",
81 | "elasticsearch",
82 | "log",
83 | "logger",
84 | "request"
85 | ],
86 | "publishConfig": {
87 | "access": "public"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/express/app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | /* eslint-disable no-unused-vars */
3 | const createError = require("http-errors")
4 | const express = require("express")
5 | const cookieParser = require("cookie-parser")
6 | const expressLogger = require("../lib/express-elasticsearch-logger")
7 |
8 | const app = express()
9 |
10 | app.use(express.json())
11 | app.use(express.urlencoded({ extended: false }))
12 | app.use(cookieParser())
13 |
14 | const loggerHost = "http://localhost:9200"
15 | app.use(
16 | expressLogger.requestHandler({
17 | host: loggerHost,
18 | whitelist: {
19 | request: [
20 | "body",
21 | "email",
22 | "httpVersion",
23 | "headers",
24 | "method",
25 | "originalUrl",
26 | "path",
27 | "query",
28 | ],
29 | response: ["statusCode", "sent"],
30 | },
31 | censor: [
32 | "ssn",
33 | "socialSecurityNumber",
34 | "password",
35 | "form.socialSecurityNumber",
36 | "userInfo.password",
37 | ],
38 | mapping: {
39 | properties: {
40 | response: {
41 | properties: {
42 | sent: {
43 | type: "object",
44 | enabled: false,
45 | },
46 | },
47 | },
48 | request: {
49 | properties: {
50 | email: {
51 | type: "text",
52 | },
53 | },
54 | },
55 | error: {
56 | properties: {
57 | errors: {
58 | type: "object",
59 | enabled: false,
60 | },
61 | },
62 | },
63 | },
64 | },
65 | }),
66 | )
67 |
68 | app.get("/", function (req, res) {
69 | res.send(new Date())
70 | })
71 |
72 | const router = express.Router()
73 |
74 | router.get("/", function (req, res) {
75 | res.send(req.query)
76 | })
77 |
78 | router.post("/", function (req, res) {
79 | res.send(req.body)
80 | })
81 |
82 | app.use("/route", router)
83 |
84 | app.use(expressLogger.errorHandler)
85 |
86 | // catch 404 and forward to error handler
87 | app.use(function (req, res, next) {
88 | next(createError(404))
89 | })
90 |
91 | // error handler
92 | app.use(function (err, req, res, next) {
93 | // set locals, only providing error in development
94 | res.locals.message = err.message
95 | res.locals.error = req.app.get("env") === "development" ? err : {}
96 |
97 | // render the error page
98 | res.status(err.status || 500)
99 | res.send(err)
100 | })
101 |
102 | module.exports = app
103 |
--------------------------------------------------------------------------------
/lib/helper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | const _censorDeep = function (obj, censorKeyArray) {
3 | if (censorKeyArray.length === 0 && typeof obj !== "undefined") {
4 | // this means function reach its base condition, return censor
5 | return "**CENSORED**"
6 | }
7 | const targetKey = censorKeyArray[0]
8 | if (Array.isArray(obj) && obj.length > 0 && targetKey !== undefined) {
9 | if (targetKey === "*") {
10 | const mappedArray = obj.map(function (item) {
11 | const restKey = censorKeyArray.slice(1, censorKeyArray.length)
12 | return _censorDeep(item, restKey)
13 | })
14 | return mappedArray
15 | }
16 | if (obj[targetKey] !== undefined) {
17 | obj[targetKey] = _censorDeep(
18 | obj[targetKey],
19 | censorKeyArray.splice(1, censorKeyArray.length),
20 | )
21 | }
22 | return obj
23 | }
24 | if (obj instanceof Object && obj[targetKey] !== "undefined") {
25 | obj[targetKey] = _censorDeep(
26 | obj[targetKey],
27 | censorKeyArray.splice(1, censorKeyArray.length),
28 | )
29 | return obj
30 | }
31 | return obj
32 | }
33 |
34 | exports.censor = function (obj, censorKeyArray) {
35 | censorKeyArray.forEach(function (key) {
36 | // split with dot notation
37 | const keyArray = key.split(".")
38 | if (keyArray.length >= 1) {
39 | // this mean the key exist
40 | const targetKey = keyArray[0]
41 | if (typeof obj[targetKey] !== "undefined") {
42 | obj[targetKey] = _censorDeep(
43 | obj[targetKey],
44 | keyArray.splice(1, keyArray.length),
45 | )
46 | }
47 | }
48 | })
49 | }
50 |
51 | exports._censorDeep = _censorDeep
52 |
53 | const deepMerge = (b, a, mergeArray) => {
54 | if (mergeArray && Array.isArray(a) && Array.isArray(b)) {
55 | return [...b, ...a].filter((v, i, arr) => arr.indexOf(v) === i)
56 | }
57 | if (Array.isArray(a) && Array.isArray(b)) {
58 | return b
59 | }
60 | Object.keys(b).forEach((key) => {
61 | if (typeof b[key] === "object") {
62 | a[key] = deepMerge(
63 | b[key],
64 | typeof a[key] === "object" ? a[key] : {},
65 | mergeArray,
66 | )
67 | } else {
68 | a[key] = b[key]
69 | }
70 | })
71 | return a
72 | }
73 |
74 | exports.deepMerge = deepMerge
75 |
76 | exports.indexDateQuarter = (date) =>
77 | `${date.toISOString().substr(0, 4)}-q${Math.ceil(
78 | +date.toISOString().substr(5, 2) / 3,
79 | )}`
80 |
81 | exports.indexDateHalfYear = (date) =>
82 | `${date.toISOString().substr(0, 4)}-h${Math.ceil(
83 | +date.toISOString().substr(5, 2) / 6,
84 | )}`
85 |
86 | exports.indexDateMonth = (date) => date.toISOString().substr(0, 7)
87 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | const indexSettings = {
2 | index: {
3 | number_of_shards: "3",
4 | number_of_replicas: "2",
5 | refresh_interval: "60s",
6 | analysis: {
7 | normalizer: {
8 | lowercase: {
9 | type: "custom",
10 | char_filter: [],
11 | filter: ["lowercase"],
12 | },
13 | },
14 | },
15 | },
16 | }
17 |
18 | const defaultMapping = {
19 | dynamic_templates: [
20 | {
21 | headers_fields: {
22 | match_mapping_type: "string",
23 | path_match: "request.headers.*",
24 | mapping: {
25 | type: "keyword",
26 | ignore_above: 256,
27 | normalizer: "lowercase",
28 | },
29 | },
30 | },
31 | ],
32 | properties: {
33 | env: {
34 | type: "keyword",
35 | index: true,
36 | },
37 | duration: {
38 | type: "integer",
39 | },
40 | "@timestamp": {
41 | type: "date",
42 | },
43 | request: {
44 | properties: {
45 | userId: {
46 | type: "text",
47 | fields: {
48 | keyword: {
49 | type: "keyword",
50 | normalizer: "lowercase",
51 | },
52 | },
53 | },
54 | email: {
55 | type: "text",
56 | fields: {
57 | keyword: {
58 | type: "keyword",
59 | normalizer: "lowercase",
60 | },
61 | },
62 | },
63 | headers: {
64 | properties: {
65 | accept: {
66 | type: "keyword",
67 | normalizer: "lowercase",
68 | },
69 | acceptencoding: {
70 | type: "keyword",
71 | normalizer: "lowercase",
72 | },
73 | authorization: {
74 | type: "text",
75 | analyzer: "standard",
76 | },
77 | cdnloop: {
78 | type: "keyword",
79 | normalizer: "lowercase",
80 | },
81 | cfconnectingip: {
82 | type: "keyword",
83 | normalizer: "lowercase",
84 | },
85 | cfipcountry: {
86 | type: "keyword",
87 | normalizer: "lowercase",
88 | },
89 | cfray: {
90 | type: "keyword",
91 | normalizer: "lowercase",
92 | },
93 | cfvisitor: {
94 | type: "keyword",
95 | normalizer: "lowercase",
96 | },
97 | contentlength: {
98 | type: "integer",
99 | },
100 | contenttype: {
101 | type: "keyword",
102 | normalizer: "lowercase",
103 | },
104 | host: {
105 | type: "keyword",
106 | normalizer: "lowercase",
107 | },
108 | useragent: {
109 | type: "text",
110 | analyzer: "standard",
111 | },
112 | xforwardedfor: {
113 | type: "keyword",
114 | normalizer: "lowercase",
115 | },
116 | xforwardedhost: {
117 | type: "keyword",
118 | normalizer: "lowercase",
119 | },
120 | xforwardedport: {
121 | type: "keyword",
122 | normalizer: "lowercase",
123 | },
124 | xforwardedproto: {
125 | type: "keyword",
126 | normalizer: "lowercase",
127 | },
128 | xoriginalforwardedfor: {
129 | type: "keyword",
130 | normalizer: "lowercase",
131 | },
132 | xoriginaluri: {
133 | type: "keyword",
134 | normalizer: "lowercase",
135 | },
136 | xrealip: {
137 | type: "keyword",
138 | normalizer: "lowercase",
139 | },
140 | xrequestid: {
141 | type: "keyword",
142 | normalizer: "lowercase",
143 | },
144 | xscheme: {
145 | type: "keyword",
146 | normalizer: "lowercase",
147 | },
148 | },
149 | },
150 | httpVersion: {
151 | type: "keyword",
152 | },
153 | method: {
154 | type: "keyword",
155 | },
156 | originalUrl: {
157 | type: "keyword",
158 | },
159 | route: {
160 | properties: {
161 | path: {
162 | type: "keyword",
163 | },
164 | },
165 | },
166 | path: {
167 | type: "keyword",
168 | },
169 | query: {
170 | type: "object",
171 | enabled: false,
172 | },
173 | body: {
174 | type: "object",
175 | enabled: false,
176 | },
177 | },
178 | },
179 | response: {
180 | properties: {
181 | sent: {
182 | type: "object",
183 | enabled: false,
184 | },
185 | statusCode: {
186 | type: "integer",
187 | },
188 | },
189 | },
190 | process: {
191 | properties: {
192 | totalmem: {
193 | type: "integer",
194 | },
195 | freemem: {
196 | type: "integer",
197 | },
198 | loadavg: {
199 | type: "integer",
200 | },
201 | },
202 | },
203 | error: {
204 | properties: {
205 | errors: {
206 | type: "object",
207 | enabled: false,
208 | },
209 | code: {
210 | type: "text",
211 | fields: {
212 | keyword: {
213 | type: "keyword",
214 | normalizer: "lowercase",
215 | },
216 | },
217 | },
218 | error: {
219 | type: "text",
220 | },
221 | error_description: {
222 | type: "text",
223 | },
224 | message: {
225 | type: "text",
226 | analyzer: "standard",
227 | },
228 | name: {
229 | type: "keyword",
230 | normalizer: "lowercase",
231 | },
232 | stack: {
233 | type: "keyword",
234 | },
235 | type: {
236 | type: "keyword",
237 | },
238 | },
239 | },
240 | },
241 | }
242 |
243 | const defaultWhiteList = {
244 | request: [
245 | "userId",
246 | "body",
247 | "email",
248 | "httpVersion",
249 | "headers",
250 | "method",
251 | "originalUrl",
252 | "path",
253 | "query",
254 | ],
255 | response: ["statusCode", "sent", "took"],
256 | error: [
257 | "message",
258 | "stack",
259 | "type",
260 | "name",
261 | "code",
262 | "errors",
263 | "error",
264 | "error_description",
265 | ],
266 | }
267 |
268 | const defaultCensor = ["password"]
269 |
270 | module.exports = {
271 | defaultMapping,
272 | defaultWhiteList,
273 | defaultCensor,
274 | indexSettings,
275 | }
276 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # express-elasticsearch-logger [](http://travis-ci.org/alexmingoia/express-elasticsearch-logger) [](https://coveralls.io/r/alexmingoia/express-elasticsearch-logger) [](https://www.npmjs.org/package/express-elasticsearch-logger) [](https://david-dm.org/alexmingoia/express-elasticsearch-logger)
2 |
3 | > Log Express app requests to ElasticSearch.
4 |
5 | ## Installation
6 |
7 | Install using [npm](https://www.npmjs.org/):
8 |
9 | ```sh
10 | npm install express-elasticsearch-logger
11 | ```
12 |
13 | ## API Reference
14 |
15 |
16 | ## express-elasticsearch-logger
17 |
18 | * [express-elasticsearch-logger](#module_express-elasticsearch-logger)
19 | * [.doc](#module_express-elasticsearch-logger.doc) : Object
20 | * [.requestHandler(config, [client])](#module_express-elasticsearch-logger.requestHandler) ⇒ elasticsearchLoggerMiddleware
21 | * [.errorHandler(err, req, res, next)](#module_express-elasticsearch-logger.errorHandler)
22 | * [.skipLog(req, res, next)](#module_express-elasticsearch-logger.skipLog)
23 |
24 |
25 |
26 | ### express-elasticsearch-logger.doc : Object
27 | Document indexed with ElasticSearch. `request` and `response` properties
28 | are included if they are whitelisted by `config.whitelist`.
29 |
30 | **Kind**: static constant of [express-elasticsearch-logger](#module_express-elasticsearch-logger)
31 | **Properties**
32 |
33 | | Name | Type | Description |
34 | | --- | --- | --- |
35 | | env | String | defaults to "development" |
36 | | [error] | Error | error object passed to `next()` |
37 | | duration | Number | milliseconds between request and response |
38 | | request | Object | requst object detail of express |
39 | | request.httpVersion | String | |
40 | | request.headers | Object | |
41 | | request.method | String | |
42 | | request.originalUrl | String | |
43 | | request.route.path | String | |
44 | | request.path | String | |
45 | | request.query | Object | |
46 | | response | Object | |
47 | | response.statusCode | Number | |
48 | | os | Object | |
49 | | os.totalmem | Number | OS total memory in bytes |
50 | | os.freemem | Number | OS free memory in bytes |
51 | | os.loadavg | Array.<Number> | Array of 5, 10, and 15 min averages |
52 | | process | Object | |
53 | | process.memoryUsage | Number | process memory in bytes |
54 | | @timestamp | String | ISO time of request |
55 |
56 |
57 |
58 | ### express-elasticsearch-logger.requestHandler(config, [client]) ⇒ elasticsearchLoggerMiddleware
59 | Returns Express middleware configured according to given `options`.
60 |
61 | Middleware must be mounted before all other middleware to ensure accurate
62 | capture of requests. The error handler must be mounted before other error
63 | handler middleware.
64 |
65 | **Kind**: static method of [express-elasticsearch-logger](#module_express-elasticsearch-logger)
66 | **Returns**: elasticsearchLoggerMiddleware - express middleware
67 |
68 | | Param | Type | Default | Description |
69 | | --- | --- | --- | --- |
70 | | config | Object | | elasticsearch configuration |
71 | | [config.host] | String | "http://localhost:9200" | elasticsearch host to connect |
72 | | [config.index] | String | "log_[YYYY]-h[1\|2]" | elasticsearch index (default: log_YYYY-h1 or log_YYYY-h2 as bi-annually) |
73 | | config.whitelist | Object | | |
74 | | [config.whitelist.request] | Array.<String> | ["userId","body","email","httpVersion","headers","method","originalUrl","path","query"] | request properties to log |
75 | | [config.whitelist.response] | Array.<String> | ["statusCode", "sent", "took"] | response properties to log |
76 | | [config.censor] | Array.<String> | ["password"] | list of request body properties to censor |
77 | | [config.includeDefault] | Boolean | true | include default whitelist and censor the the given config |
78 | | [config.indexPrefix] | String | "log" | elasticsearch index prefix for running index |
79 | | [config.indexSuffixBy] | String | "halfYear" | elasticsearch index suffix for running index, one of m M month (Monthly) q Q quarter (Quarterly) h H halfYear (Bi-annually) |
80 | | [config.indexSettings] | Object |
{index: { number_of_shards: "3", number_of_replicas: "2", refresh_interval: "60s", analysis: { normalizer: { lowercase: { type: "custom", char_filter: [], filter: ["lowercase"], }, }, }, },} | settings in the mapping to be created |
81 | | [client] | elasticsearch.Client | | @elastic/elasticsearch client to be injected |
82 |
83 | **Example**
84 | ```javascript
85 | const express = require('express');
86 | const logger = require('express-elasticsearch-logger');
87 |
88 | const app = express();
89 |
90 | app
91 | .use(logger.requestHandler({
92 | host: 'http://localhost:9200'
93 | })
94 | .get('/', function (req, res, next) {
95 | res.sendStatus(204);
96 | })
97 | .use(logger.errorHandler);
98 | ```
99 |
100 | * [.requestHandler(config, [client])](#module_express-elasticsearch-logger.requestHandler) ⇒ elasticsearchLoggerMiddleware
101 |
102 |
103 | ### express-elasticsearch-logger.errorHandler(err, req, res, next)
104 | Error handler middleware exposes error to `Response#end`
105 |
106 | This middleware is used in combination with
107 | [requestHandler](#module_express-elasticsearch-logger.requestHandler) to capture request
108 | errors.
109 |
110 | **Kind**: static method of [express-elasticsearch-logger](#module_express-elasticsearch-logger)
111 |
112 | | Param | Type |
113 | | --- | --- |
114 | | err | Error |
115 | | req | express.Request |
116 | | res | express.Response |
117 | | next | express.Request.next |
118 |
119 |
120 |
121 | ### express-elasticsearch-logger.skipLog(req, res, next)
122 | This middleware will mark for skip log
123 | use this middleware for endpoint that is called too often and did not need to log
124 | like healthcheck
125 |
126 | **Kind**: static method of [express-elasticsearch-logger](#module_express-elasticsearch-logger)
127 |
128 | | Param | Type |
129 | | --- | --- |
130 | | req | express.Request |
131 | | res | express.Response |
132 | | next | express.Request.next |
133 |
134 |
135 | ## Contributing
136 |
137 | Please submit all issues and pull requests to the [alexmingoia/express-elasticsearch-logger](http://github.com/alexmingoia/express-elasticsearch-logger) repository!
138 |
139 | ## Tests
140 |
141 | Run tests using `npm test`.
142 |
143 | ## Support
144 |
145 | If you have any problem or suggestion please open an issue [here](https://github.com/alexmingoia/express-elasticsearch-logger/issues).
146 |
--------------------------------------------------------------------------------
/lib/express-elasticsearch-logger.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /*!
3 | * express-elasticsearch-logger
4 | * https://github.com/alexmingoia/express-elasticsearch-logger
5 | */
6 |
7 | const elasticsearch = require("@elastic/elasticsearch")
8 | const os = require("os")
9 | const cloneDeep = require("clone-deep")
10 | const {
11 | deepMerge,
12 | censor,
13 | indexDateHalfYear,
14 | indexDateQuarter,
15 | indexDateMonth,
16 | } = require("./helper")
17 | const { ensureIndexExists, log } = require("./elasticsearch")
18 | const {
19 | defaultCensor,
20 | defaultMapping,
21 | defaultWhiteList,
22 | indexSettings,
23 | } = require("./config")
24 |
25 | const SUFFIX_MAP = {
26 | m: indexDateMonth,
27 | month: indexDateMonth,
28 | M: indexDateMonth,
29 | q: indexDateQuarter,
30 | quarter: indexDateQuarter,
31 | Q: indexDateQuarter,
32 | h: indexDateHalfYear,
33 | halfYear: indexDateHalfYear,
34 | H: indexDateHalfYear,
35 | }
36 | /**
37 | * @module express-elasticsearch-logger
38 | * @alias logger
39 | */
40 |
41 | /**
42 | * Returns Express middleware configured according to given `options`.
43 | *
44 | * Middleware must be mounted before all other middleware to ensure accurate
45 | * capture of requests. The error handler must be mounted before other error
46 | * handler middleware.
47 | *
48 | * @example
49 | *
50 | * ```javascript
51 | * const express = require('express');
52 | * const logger = require('express-elasticsearch-logger');
53 | *
54 | * const app = express();
55 | *
56 | * app
57 | * .use(logger.requestHandler({
58 | * host: 'http://localhost:9200'
59 | * })
60 | * .get('/', function (req, res, next) {
61 | * res.sendStatus(204);
62 | * })
63 | * .use(logger.errorHandler);
64 | * ```
65 | *
66 | * @param {Object} config elasticsearch configuration
67 | * @param {String} [config.host="http://localhost:9200"] elasticsearch host to connect
68 | * @param {String} [config.index="log_[YYYY]-h[1|2]"] elasticsearch index (default: log_YYYY-h1 or log_YYYY-h2 bi-annually)
69 | * @param {Object} config.whitelist
70 | * @param {Array.