diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..22a77f3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 +chaset = utf-8 +trim_trailing_whitespace = true +ij_javascript_use_semicolon_after_statement = true +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false + +[test/*.js] +indent_size = 2 + +[package.json] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..b1a3d6b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "extends": ["eslint:recommended"], + "env": { + "es6": true, + "node": true + }, + "parserOptions": { + "ecmaVersion": 6 + }, + "rules": { + "no-unused-vars": [2, { "args": "none" }] + }, + "overrides": [ + { + "files": "test/**", + "env": { + "mocha": true + } + } + ] +} diff --git a/.gitignore b/.gitignore index a4f66c1..3e509d2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ################# /node_modules npm-debug.log +package-lock.json ################# ## Jetbrains @@ -16,5 +17,6 @@ npm-debug.log ################# .DS_Store lib/.DS_Store -coverage.html +coverage +.nyc_output .vimrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml index a87f582..c60f507 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,6 @@ sudo: false language: node_js node_js: - - "0.10" - - "0.11" - - "0.12" - - "4" - "6" - - "7" - -script: - - "test $TRAVIS_NODE_VERSION = '0.6' || npm test" - - "test $TRAVIS_NODE_VERSION != '0.6' || npm run-script test-coverage" + - "8" + - "10" diff --git a/AUTHORS b/AUTHORS index ecbeb9e..70945bf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,3 +7,6 @@ Damian Kaczmarek Robbie Trencheny (http://robbie.io) Ross Brandes Kévin Maschtaler (https://www.kmaschta.me) +Matthew Blasius (https://expel.io) +Maxime David +Matt Morrissette (https://github.com/yinzara) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87727bc..d4c36ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +## 4.0.4 +* Update lodash to 4.17.19 ([#245](https://github.com/bithavoc/express-winston/pull/245)) +* Update statusLevels to accept Boolean or StatusLevel object in TypeScript definition ([#244](https://github.com/bithavoc/express-winston/pull/244)) + +## 4.0.3 +* Update lodash to 4.17.15 ([#232](https://github.com/bithavoc/express-winston/pull/232)) +* Add missing exceptionToMeta and skip to TypeScript definition ([#234](https://github.com/bithavoc/express-winston/pull/234)) + +## 4.0.2 +* Add blacklist fields to BaseErrorLoggerOptions interface ([#230](https://github.com/bithavoc/express-winston/pull/230)) + +## 4.0.1 +* Added `headerBlacklist` to BaseLoggerOptions for better typescript support ([#228](https://github.com/bithavoc/express-winston/pull/228)) + +## 4.0.0 +* Changed `metaField` configuration property functionality (see Readme.md) ([#209](https://github.com/bithavoc/express-winston/issues/209)) - BREAKING CHANGE +* Moved type definitions to be embedded inside library ([#123](https://github.com/bithavoc/express-winston/issues/123)) +* Added "files" to package.json to reduce unnecessary files in released package +* Added StackDriver/Google Cloud Logging specific instructions to Readme.md +* Added `requestField` and `responseField` options to allow storing the request and response in different metadata fields (or not at all) +* Fixed `meta` configuration option on `errorLogger` (was not functioning at all) +* Added .editorconfig and reformatted library to match + +## 3.4.0 +* Added: Nested Whitelists ([#225](https://github.com/bithavoc/express-winston/pull/225), @kapalex) + +## 3.3.0 +* Added: options.headerBlacklist ([#217](https://github.com/bithavoc/express-winston/pull/217), @maxday) + +## 3.2.1 +* Added: options.skip ([#214](https://github.com/bithavoc/express-winston/pull/214), [#147](https://github.com/bithavoc/express-winston/pull/147), @ahnkee) + +## 3.2.0 +* Replaced: _header -> getHeader ([#210](https://github.com/bithavoc/express-winston/pull/210), @Gregoirevda) +* Replaced coverage tool blanket with nyc ([#211](https://github.com/bithavoc/express-winston/pull/211), @golopot) +* Add eslint and fix lint errors ([#212](https://github.com/bithavoc/express-winston/pull/212), @golopot) + +## 3.1.0 +* Fix large _.template memory consumption ([#203](https://github.com/bithavoc/express-winston/pull/203), @slickmb) + +## 3.0.1 +* Add `format` to `options` to allow user-specified formatting following winston@3 conventions ([#190](https://github.com/bithavoc/express-winston/pull/190), @crellison) + +## 3.0.0 +express-winston@3 shouldn't have any breaking changes _of its own_, but there are breaking changes as a result of upgrading winston and Node.js. + +express-winston@2.6.0 will be the last version to support winston@2. + +#### Breaking changes +* Drop support for winston < 3. winston@3 includes quite a few breaking changes. Check their [changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md) and [upgrade guide](https://github.com/winstonjs/winston/blob/master/UPGRADE-3.0.md) to get an idea of winston's breaking changes. +* Drop support for Node.js < 6. v6 is the oldest version of Node.js [currently supported by the Node.js team](https://github.com/nodejs/Release). + +## 2.6.0 +* Add `exceptionToMeta` and `blacklistedMetaFields` for filtering returned meta + object ([#173](https://github.com/bithavoc/express-winston/pull/173), @cubbuk) + +## 2.5.1 +* Allow `msg` to be a function ([#160](https://github.com/bithavoc/express-winston/pull/160), @brendancwood) + +## 2.5.0 +* Reduce memory usage ([#164](https://github.com/bithavoc/express-winston/pull/164), @Kmaschta) + ## 2.4.0 * Allow `options.level` to be a function for dynamic level setting ([#148](https://github.com/bithavoc/express-winston/pull/148), @CryptArchy) diff --git a/LICENSE b/LICENSE index 1eee682..91599e0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2014 Bithavoc.io - http://bithavoc.io +Copyright (c) 2012 Bithavoc.io - http://bithavoc.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Readme.md b/Readme.md index 94bc2cb..091a64b 100644 --- a/Readme.md +++ b/Readme.md @@ -3,11 +3,15 @@ > [winston](https://github.com/winstonjs/winston) middleware for express.js +[Changelog](CHANGELOG.md) + +[CALL FOR MAINTAINERS](https://github.com/bithavoc/express-winston/issues/192) + ## Installation npm install winston express-winston -(supports node >= 0.10) +(supports node >= 6) ## Usage @@ -21,8 +25,8 @@ In `package.json`: { "dependencies": { "...": "...", - "winston": "^2.0.0", - "express-winston": "^2.0.0", + "winston": "^3.0.0", + "express-winston": "^3.0.0", "...": "..." } } @@ -44,11 +48,12 @@ Use `expressWinston.logger(options)` to create a middleware to log your HTTP req app.use(expressWinston.logger({ transports: [ - new winston.transports.Console({ - json: true, - colorize: true - }) + new winston.transports.Console() ], + format: winston.format.combine( + winston.format.colorize(), + winston.format.json() + ), meta: true, // optional: control whether you want to log the meta data about the request (default to true) msg: "HTTP {{req.method}} {{req.url}}", // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}" expressFormat: true, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true @@ -63,25 +68,28 @@ Use `expressWinston.logger(options)` to create a middleware to log your HTTP req ``` js transports: [], // list of all winston transports instances to use. - winstonInstance: , // a winston logger instance. If this is provided the transports option is ignored. + format: [], // formatting desired for log output. + winstonInstance: , // a winston logger instance. If this is provided the transports and formats options are ignored. level: String or function(req, res) { return String; }, // log level to use, the default is "info". Assign a function to dynamically set the level based on request and response, or a string to statically set it always at that level. statusLevels must be false for this setting to be used. - msg: String // customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}", "HTTP {{req.method}} {{req.url}}". + msg: String or function, // customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}", "HTTP {{req.method}} {{req.url}}" or function(req, res) { return `${res.statusCode} - ${req.method}`. Warning: while supported, returning mustache style interpolation from an options.msg function has performance and memory implications under load. expressFormat: Boolean, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors when colorize set to true colorize: Boolean, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red). meta: Boolean, // control whether you want to log the meta data about the request (default to true). baseMeta: Object, // default meta data to be added to log, this will be merged with the meta data. - metaField: String, // if defined, the meta data will be added in this field instead of the meta root object. - statusLevels: Boolean or Object // different HTTP status codes caused log messages to be logged at different levels (info/warn/error), the default is false. Use an object to control the levels various status codes are logged at. Using an object for statusLevels overrides any setting of options.level. - ignoreRoute: function (req, res) { return false; } // A function to determine if logging is skipped, defaults to returning false. Called _before_ any later middleware. - skip: function(req, res) { return false; } // A function to determine if logging is skipped, defaults to returning false. Called _after_ response has already been sent. - requestFilter: function (req, propName) { return req[propName]; } // A function to filter/return request values, defaults to returning all values allowed by whitelist. If the function returns undefined, the key/value will not be included in the meta. - responseFilter: function (res, propName) { return res[propName]; } // A function to filter/return response values, defaults to returning all values allowed by whitelist. If the function returns undefined, the key/value will not be included in the meta. - requestWhitelist: [String] // Array of request properties to log. Overrides global requestWhitelist for this instance - responseWhitelist: [String] // Array of response properties to log. Overrides global responseWhitelist for this instance - bodyWhitelist: [String] // Array of body properties to log. Overrides global bodyWhitelist for this instance - bodyBlacklist: [String] // Array of body properties to omit from logs. Overrides global bodyBlacklist for this instance - ignoredRoutes: [String] // Array of paths to ignore/skip logging. Overrides global ignoredRoutes for this instance + metaField: String, // if defined, the meta data will be added in this field instead of the meta root object. Defaults to 'meta'. Set to `null` to store metadata at the root of the log entry. + requestField: [String] // the property of the metadata to store the request under (default 'req'). Set to null to exclude request from metadata + statusLevels: Boolean or Object, // different HTTP status codes caused log messages to be logged at different levels (info/warn/error), the default is false. Use an object to control the levels various status codes are logged at. Using an object for statusLevels overrides any setting of options.level. + ignoreRoute: function (req, res) { return false; }, // A function to determine if logging is skipped, defaults to returning false. Called _before_ any later middleware. + skip: function(req, res) { return false; }, // A function to determine if logging is skipped, defaults to returning false. Called _after_ response has already been sent. + requestFilter: function (req, propName) { return req[propName]; }, // A function to filter/return request values, defaults to returning all values allowed by whitelist. If the function returns undefined, the key/value will not be included in the meta. + responseFilter: function (res, propName) { return res[propName]; }, // A function to filter/return response values, defaults to returning all values allowed by whitelist. If the function returns undefined, the key/value will not be included in the meta. + requestWhitelist: [String], // Array of request properties to log. Overrides global requestWhitelist for this instance + responseWhitelist: [String], // Array of response properties to log. Overrides global responseWhitelist for this instance + bodyWhitelist: [String], // Array of body properties to log. Overrides global bodyWhitelist for this instance + bodyBlacklist: [String], // Array of body properties to omit from logs. Overrides global bodyBlacklist for this instance + ignoredRoutes: [String], // Array of paths to ignore/skip logging. Overrides global ignoredRoutes for this instance dynamicMeta: function(req, res) { return [Object]; } // Extract additional meta data from request or response (typically req.user data if using passport). meta must be true for this function to be activated + headerBlacklist: [String], // Array of headers to omit from logs. Applied after any previous filters. ``` @@ -95,11 +103,12 @@ Use `expressWinston.errorLogger(options)` to create a middleware that log the er app.use(router); // notice how the router goes first. app.use(expressWinston.errorLogger({ transports: [ - new winston.transports.Console({ - json: true, - colorize: true - }) - ] + new winston.transports.Console() + ], + format: winston.format.combine( + winston.format.colorize(), + winston.format.json() + ) })); ``` @@ -109,20 +118,45 @@ The logger needs to be added AFTER the express router(`app.router)`) and BEFORE ``` js transports: [], // list of all winston transports instances to use. - winstonInstance: , // a winston logger instance. If this is provided the transports option is ignored - msg: String // customize the default logging message. E.g. "{{err.message}} {{res.statusCode}} {{req.method}}". + format: [], // formatting desired for log output + winstonInstance: , // a winston logger instance. If this is provided the transports and formats options are ignored. + msg: String or function // customize the default logging message. E.g. "{{err.message}} {{res.statusCode}} {{req.method}}" or function(req, res) { return `${res.statusCode} - ${req.method}` } baseMeta: Object, // default meta data to be added to log, this will be merged with the error data. - metaField: String, // if defined, the meta data will be added in this field instead of the meta root object. + meta: Boolean, // control whether you want to log the meta data about the request (default to true). + metaField: String, // if defined, the meta data will be added in this field instead of the meta root object. Defaults to 'meta'. Set to `null` to store metadata at the root of the log entry. + requestField: [String] // the property of the metadata to store the request under (default 'req'). Set to null to exclude request from metadata + responseField: [String] // the property of the metadata to store the response under (default 'res'). If set to the same as 'requestField', filtered response and request properties will be merged. Set to null to exclude request from metadata requestFilter: function (req, propName) { return req[propName]; } // A function to filter/return request values, defaults to returning all values allowed by whitelist. If the function returns undefined, the key/value will not be included in the meta. requestWhitelist: [String] // Array of request properties to log. Overrides global requestWhitelist for this instance + headerBlacklist: [String], // Array of headers to omit from logs. Applied after any previous filters. level: String or function(req, res, err) { return String; }// custom log level for errors (default is 'error'). Assign a function to dynamically set the log level based on request, response, and the exact error. dynamicMeta: function(req, res, err) { return [Object]; } // Extract additional meta data from request or response (typically req.user data if using passport). meta must be true for this function to be activated + exceptionToMeta: function(error){return Object; } // Function to format the returned meta information on error log. If not given `winston.exception.getAllInfo` will be used by default + blacklistedMetaFields: [String] // fields to blacklist from meta data + skip: function(req, res, err) { return false; } // A function to determine if logging is skipped, defaults to returning false. ``` To use winston's existing transports, set `transports` to the values (as in key-value) of the `winston.default.transports` object. This may be done, for example, by using underscorejs: `transports: _.values(winston.default.transports)`. Alternatively, if you're using a winston logger instance elsewhere and have already set up levels and transports, pass the instance into expressWinston with the `winstonInstance` option. The `transports` option is then ignored. +#### `metaField` option + +In versions of `express-winston` prior to 4.0.0, this field functioned differently. + +Previously the log entry would always have a "meta" field which would be set to the metadata of the request/error. +If `metaField` was set, this information would be stored as an object with the given property on the "meta" object of +the log entry. This prevented the use case where the metadata should be located at the root of the log entry. + +In this version, `metaField` defaults to "meta" which maintains the prior versions behavior of storing the metadata at +a "meta" property of the log entry. + +Explicitly setting the `metaField` to `null` or "null" causes the metadata to be stored at the root of the log entry. + +The `metaField` option now also supports dot separated and array values to store the metadata at a nested location in the log entry. + +

Upgrade Note: For those upgrading from a version of `express-winston` prior to 4.0.0 that use the `metaField` property, to keep the same behavior, prepend `meta.` to your current `metaField` configuration. (i.e. 'foo' would become 'meta.foo')

+ ## Examples ``` js @@ -141,19 +175,20 @@ Alternatively, if you're using a winston logger instance elsewhere and have alre return next(new Error("This is an error and it should be logged to the console")); }); - app.get('/', function(req, res, next) { + router.get('/', function(req, res, next) { res.write('This is a normal request, it should be logged to the console too'); res.end(); }); - // express-winston logger makes sense BEFORE the router. + // express-winston logger makes sense BEFORE the router app.use(expressWinston.logger({ transports: [ - new winston.transports.Console({ - json: true, - colorize: true - }) - ] + new winston.transports.Console() + ], + format: winston.format.combine( + winston.format.colorize(), + winston.format.json() + ) })); // Now we can tell the app to use our routing code: @@ -162,11 +197,12 @@ Alternatively, if you're using a winston logger instance elsewhere and have alre // express-winston errorLogger makes sense AFTER the router. app.use(expressWinston.errorLogger({ transports: [ - new winston.transports.Console({ - json: true, - colorize: true - }) - ] + new winston.transports.Console() + ], + format: winston.format.combine( + winston.format.colorize(), + winston.format.json() + ) })); // Optionally you can include your custom error handler after the logging. @@ -283,6 +319,60 @@ Browse `/error` will show you how express-winston handles and logs the errors in "message": "middlewareError" } +### StackDriver/Google Cloud Logging + +If using this library with `@google-cloud/logging-winston`, use the following configuration to properly store httpRequest information. + +See https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry + +```javascript +var express = require('express'); +var expressWinston = require('express-winston'); +var LoggingWinston = require('@google-cloud/logging-winston').LoggingWinston; + +const app = express() + +app.use(expressWinston.logger({ + transports: [new LoggingWinston({})], + metaField: null, //this causes the metadata to be stored at the root of the log entry + responseField: null, // this prevents the response from being included in the metadata (including body and status code) + requestWhitelist: ['headers', 'query'], //these are not included in the standard StackDriver httpRequest + responseWhitelist: ['body'], // this populates the `res.body` so we can get the response size (not required) + dynamicMeta: (req, res) => { + const httpRequest = {} + const meta = {} + if (req) { + meta.httpRequest = httpRequest + httpRequest.requestMethod = req.method + httpRequest.requestUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}` + httpRequest.protocol = `HTTP/${req.httpVersion}` + // httpRequest.remoteIp = req.ip // this includes both ipv6 and ipv4 addresses separated by ':' + httpRequest.remoteIp = req.ip.indexOf(':') >= 0 ? req.ip.substring(req.ip.lastIndexOf(':') + 1) : req.ip // just ipv4 + httpRequest.requestSize = req.socket.bytesRead + httpRequest.userAgent = req.get('User-Agent') + httpRequest.referrer = req.get('Referrer') + } + + if (res) { + meta.httpRequest = httpRequest + httpRequest.status = res.statusCode + httpRequest.latency = { + seconds: Math.floor(res.responseTime / 1000), + nanos: ( res.responseTime % 1000 ) * 1000000 + } + if (res.body) { + if (typeof res.body === 'object') { + httpRequest.responseSize = JSON.stringify(res.body).length + } else if (typeof res.body === 'string') { + httpRequest.responseSize = res.body.length + } + } + } + return meta + } +})); +``` + ## Global Whitelists and Blacklists Express-winston exposes three whitelists that control which properties of the `request`, `body`, and `response` are logged: @@ -312,6 +402,40 @@ Note that you can log the whole request and/or response body: expressWinston.requestWhitelist.push('body'); expressWinston.responseWhitelist.push('body'); + +### Nested Whitelists + +`requestWhitelist` and `responseWhitelist` also support nested whitelist values, allowing access to parts of an object. + +For example, using the following during logger setup: + + expressWinston.responseWhitelist.push('body.import.value'); + +A response that looks like this : + + { + body: { + important: { + value: 5 + }, + notImportant: { + value: 7 + } + }, + other: { + value: 3 + } + } + +Would only log the following value : + + { + body: { + important: { + value: 5 + } + } + } ## Route-Specific Whitelists and Blacklists @@ -368,7 +492,7 @@ Blacklisting supports only the `body` property. }); ``` -If both `req._bodyWhitelist.body` and `req._bodyBlacklist.body` are set the result will be the white listed properties +If both `req._routeWhitelists.body` and `req._routeBlacklists.body` are set the result will be the white listed properties excluding any black listed ones. In the above example, only 'email' and 'age' would be included. @@ -427,13 +551,9 @@ Run the basic Mocha tests: npm test -Run the Travis-CI tests (which will fail with < 100% coverage): - - npm run test-travis - -Generate the `coverage.html` coverage report: +View the coverage report: - npm run test-coverage + npx http-server coverage/lcov-report ## Issues and Collaboration @@ -445,12 +565,14 @@ If you ran into any problems, please use the project [Issues section](https://gi * [Lars Jacob](https://github.com/jaclar) (https://github.com/jaclar) * [Jonathan Lomas](https://github.com/floatingLomas) (https://github.com/floatingLomas) * [Ross Brandes](https://github.com/rosston) (https://github.com/rosston) +* [Alex Kaplan](https://github.com/kapalex) (https://github.com/kapalex) +* [Matt Morrissette](https://github.com/yinzara) (https://github.com/yinzara) Also see AUTHORS file, add yourself if you are missing. ## MIT License -Copyright (c) 2012-2014 Bithavoc.io and Contributors - http://bithavoc.io +Copyright (c) 2012 Bithavoc.io and Contributors - http://bithavoc.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..4d5d400 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,124 @@ +// Type definitions for express-winston 4.0 +// Project: https://github.com/bithavoc/express-winston#readme +// Definitions by: Alex Brick +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +import { ErrorRequestHandler, Handler, Request, Response } from 'express'; +import { Format } from 'logform'; +import * as winston from 'winston'; +import * as Transport from 'winston-transport'; + +export interface FilterRequest extends Request { + [other: string]: any; +} + +export interface FilterResponse extends Response { + [other: string]: any; +} + +export type ExceptionToMetaFunction = (err: Error) => object; +export type DynamicMetaFunction = (req: Request, res: Response, err: Error) => object; +export type DynamicLevelFunction = (req: Request, res: Response, err: Error) => string; +export type RequestFilter = (req: FilterRequest, propName: string) => any; +export type ResponseFilter = (res: FilterResponse, propName: string) => any; +export type RouteFilter = (req: Request, res: Response) => boolean; +export type ErrorRouteFilter = (req: Request, res: Response, err: Error) => boolean; +export type MessageTemplate = string | ((req: Request, res: Response) => string); + +export interface StatusLevels { + error?: string; + success?: string; + warn?: string; +}; + +export interface BaseLoggerOptions { + baseMeta?: object; + bodyBlacklist?: string[]; + bodyWhitelist?: string[]; + colorize?: boolean; + dynamicMeta?: DynamicMetaFunction; + expressFormat?: boolean; + format?: Format; + ignoreRoute?: RouteFilter; + ignoredRoutes?: string[]; + level?: string | DynamicLevelFunction; + meta?: boolean; + metaField?: string; + requestField?: string; + responseField?: string; + msg?: MessageTemplate; + requestFilter?: RequestFilter; + requestWhitelist?: string[]; + responseFilter?: ResponseFilter; + responseWhitelist?: string[]; + headerBlacklist?: string[]; + skip?: RouteFilter; + statusLevels?: Boolean | StatusLevels; +} + +export interface LoggerOptionsWithTransports extends BaseLoggerOptions { + transports: Transport[]; +} + +export interface LoggerOptionsWithWinstonInstance extends BaseLoggerOptions { + winstonInstance: winston.Logger; +} + +export type LoggerOptions = LoggerOptionsWithTransports | LoggerOptionsWithWinstonInstance; + +export function logger(options: LoggerOptions): Handler; + +export interface BaseErrorLoggerOptions { + baseMeta?: object; + dynamicMeta?: DynamicMetaFunction; + exceptionToMeta?: ExceptionToMetaFunction; + format?: Format; + level?: string | DynamicLevelFunction; + meta?: boolean; + metaField?: string; + requestField?: string; + responseField?: string; + msg?: MessageTemplate; + requestFilter?: RequestFilter; + requestWhitelist?: string[]; + headerBlacklist?: string[]; + blacklistedMetaFields?: string[]; + skip?: ErrorRouteFilter; +} + +export interface ErrorLoggerOptionsWithTransports extends BaseErrorLoggerOptions { + transports: Transport[]; +} + +export interface ErrorLoggerOptionsWithWinstonInstance extends BaseErrorLoggerOptions { + winstonInstance: winston.Logger; +} + +export type ErrorLoggerOptions = ErrorLoggerOptionsWithTransports | ErrorLoggerOptionsWithWinstonInstance; + +export function errorLogger(options: ErrorLoggerOptions): ErrorRequestHandler; + +export let requestWhitelist: string[]; + +export let bodyWhitelist: string[]; + +export let bodyBlacklist: string[]; + +export let responseWhitelist: string[]; + +export let ignoredRoutes: string[]; + +export let defaultRequestFilter: RequestFilter; + +export let defaultResponseFilter: ResponseFilter; + +export function defaultSkip(): boolean; + +export interface ExpressWinstonRequest extends Request { + _routeWhitelists: { + body: string[]; + req: string[]; + res: string[]; + }; +} diff --git a/index.js b/index.js index 1a1bab7..e8d5c88 100644 --- a/index.js +++ b/index.js @@ -19,7 +19,6 @@ // THE SOFTWARE. // var winston = require('winston'); -var util = require('util'); var chalk = require('chalk'); var _ = require('lodash'); @@ -67,9 +66,15 @@ exports.ignoredRoutes = []; * @return {*} */ exports.defaultRequestFilter = function (req, propName) { - return req[propName]; + return _.get(req, propName); }; +/** + * A default list of headers in the request object that are not allowed to be logged. + * @type {Array} + */ +exports.defaultHeaderBlacklist = []; + /** * A default function to filter the properties of the res object. * @param res @@ -77,32 +82,83 @@ exports.defaultRequestFilter = function (req, propName) { * @return {*} */ exports.defaultResponseFilter = function (res, propName) { - return res[propName]; + return _.get(res, propName); }; /** * A default function to decide whether skip logging of particular request. Doesn't skip anything (i.e. log all requests). * @return always false */ -exports.defaultSkip = function() { - return false; +exports.defaultSkip = function () { + return false; }; -function filterObject(originalObj, whiteList, initialFilter) { +/** + * The property of the metadata of the log entry that the filtered HTTP request is stored in (default 'req') + * @type {string} + */ +exports.requestField = 'req'; + +/** + * The property of the metadata of the log entry that the filtered HTTP response is stored in (default 'res') + * @type {string} + */ +exports.responseField = 'res'; + +function filterObject(originalObj, whiteList, headerBlacklist, initialFilter) { var obj = {}; var fieldsSet = false; [].concat(whiteList).forEach(function (propName) { var value = initialFilter(originalObj, propName); - if(typeof (value) !== 'undefined') { - obj[propName] = value; + _.set(obj, propName, value); fieldsSet = true; - }; + if (propName === 'headers') { + [].concat(headerBlacklist).forEach(function (headerName) { + var lowerCaseHeaderName = headerName ? headerName.toLowerCase() : null; + if (obj[propName].hasOwnProperty(lowerCaseHeaderName)) { + delete obj[propName][lowerCaseHeaderName]; + } + }); + } + } }); - return fieldsSet?obj:undefined; + return fieldsSet ? obj : undefined; +} + +function getTemplate(loggerOptions, templateOptions) { + if (loggerOptions.expressFormat) { + var expressMsgFormat = '{{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms'; + if (loggerOptions.colorize) { + expressMsgFormat = chalk.grey('{{req.method}} {{req.url}}') + + ' {{res.statusCode}} ' + + chalk.grey('{{res.responseTime}}ms'); + } + + return _.template(expressMsgFormat, templateOptions); + } + + if (!_.isFunction(loggerOptions.msg)) { + return _.template(loggerOptions.msg, templateOptions); + } + + return function (data) { + data = data || {}; + var m = loggerOptions.msg(data.req, data.res); + + // if there is no interpolation, don't waste resources creating a template. + // this quick regex is still way faster than just blindly compiling a new template. + if (!/\{\{/.test(m)) { + return m; + } + // since options.msg was a function, and the results seem to contain moustache + // interpolation, we'll compile a new template for each request. + // Warning: this eats a ton of memory under heavy load. + return _.template(m, templateOptions)(data); + }; } // @@ -110,48 +166,74 @@ function filterObject(originalObj, whiteList, initialFilter) { // #### @options {Object} options to initialize the middleware. // - exports.errorLogger = function errorLogger(options) { ensureValidOptions(options); options.requestWhitelist = options.requestWhitelist || exports.requestWhitelist; options.requestFilter = options.requestFilter || exports.defaultRequestFilter; - options.winstonInstance = options.winstonInstance || (new winston.Logger ({ transports: options.transports })); + options.headerBlacklist = options.headerBlacklist || exports.defaultHeaderBlacklist; + options.winstonInstance = options.winstonInstance || (winston.createLogger({ + transports: options.transports, + format: options.format + })); options.msg = options.msg || 'middlewareError'; options.baseMeta = options.baseMeta || {}; - options.metaField = options.metaField || null; + options.metaField = options.metaField === null || options.metaField === 'null' ? null : options.metaField || 'meta'; options.level = options.level || 'error'; - options.dynamicMeta = options.dynamicMeta || function(req, res, err) { return null; }; + options.dynamicMeta = options.dynamicMeta || function (req, res, err) { return null; }; + const exceptionHandler = new winston.ExceptionHandler(options.winstonInstance); + options.exceptionToMeta = options.exceptionToMeta || exceptionHandler.getAllInfo.bind(exceptionHandler); + options.blacklistedMetaFields = options.blacklistedMetaFields || []; + options.skip = options.skip || exports.defaultSkip; + options.requestField = options.requestField === null || options.requestField === 'null' ? null : options.requestField || exports.requestField; + + // backwards comparability. + // just in case they're using the same options object as exports.logger. + options = _.omit(options, 'expressFormat'); // Using mustache style templating - var template = _.template(options.msg, { - interpolate: /\{\{([\s\S]+?)\}\}/g - }); + var template = getTemplate(options, { interpolate: /\{\{([\s\S]+?)\}\}/g }); return function (err, req, res, next) { + // Let winston gather all the error data + var exceptionMeta = _.omit(options.exceptionToMeta(err), options.blacklistedMetaFields); + if (options.meta !== false) { + if (options.requestField !== null) { + exceptionMeta[options.requestField] = filterObject(req, options.requestWhitelist, options.headerBlacklist, options.requestFilter); + } - // Let winston gather all the error data. - var exceptionMeta = winston.exception.getAllInfo(err); - exceptionMeta.req = filterObject(req, options.requestWhitelist, options.requestFilter); - - if(options.dynamicMeta) { - var dynamicMeta = options.dynamicMeta(req, res, err); - exceptionMeta = _.assign(exceptionMeta, dynamicMeta); + if (options.dynamicMeta) { + var dynamicMeta = options.dynamicMeta(req, res, err); + exceptionMeta = _.assign(exceptionMeta, dynamicMeta); + } } if (options.metaField) { - var newMeta = {}; - newMeta[options.metaField] = exceptionMeta; - exceptionMeta = newMeta; + var fields; + if (Array.isArray(options.metaField)) { + fields = options.metaField; + } else { + fields = options.metaField.split('.'); + } + _(fields).reverse().forEach(field => { + var newMeta = {}; + newMeta[field] = exceptionMeta; + exceptionMeta = newMeta; + }); } exceptionMeta = _.assign(exceptionMeta, options.baseMeta); var level = _.isFunction(options.level) ? options.level(req, res, err) : options.level; - // This is fire and forget, we don't want logging to hold up the request so don't wait for the callback - options.winstonInstance.log(level, template({err: err, req: req, res: res}), exceptionMeta); + if (!options.skip(req, res, err)) { + // This is fire and forget, we don't want logging to hold up the request so don't wait for the callback + options.winstonInstance.log(_.merge(exceptionMeta, { + level, + message: template({ err: err, req: req, res: res }), + })); + } next(err); }; @@ -159,12 +241,12 @@ exports.errorLogger = function errorLogger(options) { function levelFromStatus(options) { return function (req, res) { - var level = ""; - if (res.statusCode >= 100) { level = options.statusLevels.success || "info"; } - if (res.statusCode >= 400) { level = options.statusLevels.warn || "warn"; } - if (res.statusCode >= 500) { level = options.statusLevels.error || "error"; } + var level = ''; + if (res.statusCode >= 100) { level = options.statusLevels.success || 'info'; } + if (res.statusCode >= 400) { level = options.statusLevels.warn || 'warn'; } + if (res.statusCode >= 500) { level = options.statusLevels.error || 'error'; } return level; - } + }; } // @@ -179,40 +261,37 @@ exports.logger = function logger(options) { options.requestWhitelist = options.requestWhitelist || exports.requestWhitelist; options.bodyWhitelist = options.bodyWhitelist || exports.bodyWhitelist; options.bodyBlacklist = options.bodyBlacklist || exports.bodyBlacklist; + options.headerBlacklist = options.headerBlacklist || exports.defaultHeaderBlacklist; options.responseWhitelist = options.responseWhitelist || exports.responseWhitelist; options.requestFilter = options.requestFilter || exports.defaultRequestFilter; options.responseFilter = options.responseFilter || exports.defaultResponseFilter; options.ignoredRoutes = options.ignoredRoutes || exports.ignoredRoutes; - options.winstonInstance = options.winstonInstance || (new winston.Logger ({ transports: options.transports })); + options.winstonInstance = options.winstonInstance || (winston.createLogger({ + transports: options.transports, + format: options.format + })); options.statusLevels = options.statusLevels || false; - options.level = options.statusLevels ? levelFromStatus(options) : (options.level || "info"); - options.msg = options.msg || "HTTP {{req.method}} {{req.url}}"; + options.level = options.statusLevels ? levelFromStatus(options) : (options.level || 'info'); + options.msg = options.msg || 'HTTP {{req.method}} {{req.url}}'; options.baseMeta = options.baseMeta || {}; - options.metaField = options.metaField || null; + options.metaField = options.metaField === null || options.metaField === 'null' ? null : options.metaField || 'meta'; options.colorize = options.colorize || false; options.expressFormat = options.expressFormat || false; options.ignoreRoute = options.ignoreRoute || function () { return false; }; options.skip = options.skip || exports.defaultSkip; - options.dynamicMeta = options.dynamicMeta || function(req, res) { return null; }; - - var expressMsgFormat = "{{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms"; - if (options.colorize) { - expressMsgFormat = chalk.grey("{{req.method}} {{req.url}}") + - " {{res.statusCode}} " + - chalk.grey("{{res.responseTime}}ms"); - } - - var msgFormat = !options.expressFormat ? options.msg : expressMsgFormat; + options.dynamicMeta = options.dynamicMeta || function (req, res) { return null; }; + options.requestField = options.requestField === null || options.requestField === 'null' ? null : options.requestField || exports.requestField; + options.responseField = options.responseField === null || options.responseField === 'null' ? null : options.responseField || exports.responseField; // Using mustache style templating - var template = _.template(msgFormat, { - interpolate: /\{\{(.+?)\}\}/g + var template = getTemplate(options, { + interpolate: /\{\{(.+?)\}\}/g }); return function (req, res, next) { var coloredRes = {}; - var currentUrl = req.originalUrl || req.url; + var currentUrl = req.originalUrl || req.url; if (currentUrl && _.includes(options.ignoredRoutes, currentUrl)) return next(); if (options.ignoreRoute(req, res)) return next(); @@ -230,97 +309,119 @@ exports.logger = function logger(options) { // Manage to get information from the response too, just like Connect.logger does: var end = res.end; - res.end = function(chunk, encoding) { + res.end = function (chunk, encoding) { res.responseTime = (new Date) - req._startTime; res.end = end; res.end(chunk, encoding); - req.url = req.originalUrl || req.url; + req.url = req.originalUrl || req.url; var meta = {}; - if(options.meta !== false) { - var logData = {}; - - var requestWhitelist = options.requestWhitelist.concat(req._routeWhitelists.req || []); - var responseWhitelist = options.responseWhitelist.concat(req._routeWhitelists.res || []); - - logData.res = res; + if (options.meta !== false) { + var logData = {}; + + if (options.requestField !== null) { + var requestWhitelist = options.requestWhitelist.concat(req._routeWhitelists.req || []); + var filteredRequest = filterObject(req, requestWhitelist, options.headerBlacklist, options.requestFilter); + + var bodyWhitelist = _.union(options.bodyWhitelist, (req._routeWhitelists.body || [])); + var blacklist = _.union(options.bodyBlacklist, (req._routeBlacklists.body || [])); + + var filteredBody = null; + + if (req.body !== undefined) { + if (blacklist.length > 0 && bodyWhitelist.length === 0) { + var whitelist = _.difference(Object.keys(req.body), blacklist); + filteredBody = filterObject(req.body, whitelist, options.headerBlacklist, options.requestFilter); + } else if ( + requestWhitelist.indexOf('body') !== -1 && + bodyWhitelist.length === 0 && + blacklist.length === 0 + ) { + filteredBody = filterObject(req.body, Object.keys(req.body), options.headerBlacklist, options.requestFilter); + } else { + filteredBody = filterObject(req.body, bodyWhitelist, options.headerBlacklist, options.requestFilter); + } + } + + if (filteredRequest) { + if (filteredBody) { + filteredRequest.body = filteredBody; + } else { + delete filteredRequest.body; + } + } + + logData[options.requestField] = filteredRequest; + } - if (_.includes(responseWhitelist, 'body')) { - if (chunk) { - var isJson = (res._headers && res._headers['content-type'] - && res._headers['content-type'].indexOf('json') >= 0); + var responseWhitelist = options.responseWhitelist.concat(req._routeWhitelists.res || []); + if (_.includes(responseWhitelist, 'body')) { + if (chunk) { + var isJson = (res.getHeader('content-type') + && res.getHeader('content-type').indexOf('json') >= 0); + const body = chunk.toString(); + res.body = bodyToString(body, isJson); + } + } - logData.res.body = bodyToString(chunk, isJson); + if (options.responseField !== null) { + var filteredResponse = filterObject(res, responseWhitelist, options.headerBlacklist, options.responseFilter); + if (filteredResponse) { + if (options.requestField === options.responseField) { + logData[options.requestField] = _.assign(filteredRequest, filteredResponse); + } else { + logData[options.responseField] = filteredResponse; + } + } } - } - - logData.req = filterObject(req, requestWhitelist, options.requestFilter); - logData.res = filterObject(res, responseWhitelist, options.responseFilter); - - var bodyWhitelist = _.union(options.bodyWhitelist, (req._routeWhitelists.body || [])); - var blacklist = _.union(options.bodyBlacklist, (req._routeBlacklists.body || [])); - - var filteredBody = null; - - if ( req.body !== undefined ) { - if (blacklist.length > 0 && bodyWhitelist.length === 0) { - var whitelist = _.difference(Object.keys(req.body), blacklist); - filteredBody = filterObject(req.body, whitelist, options.requestFilter); - } else if ( - requestWhitelist.indexOf('body') !== -1 && - bodyWhitelist.length === 0 && - blacklist.length === 0 - ) { - filteredBody = filterObject(req.body, Object.keys(req.body), options.requestFilter); - } else { - filteredBody = filterObject(req.body, bodyWhitelist, options.requestFilter); - } - } - - if (logData.req) { - if (filteredBody) { - logData.req.body = filteredBody; - } else { - delete logData.req.body; + + if (!responseWhitelist.includes('responseTime')) { + logData.responseTime = res.responseTime; } - } - logData.responseTime = res.responseTime; + if (options.dynamicMeta) { + var dynamicMeta = options.dynamicMeta(req, res); + logData = _.assign(logData, dynamicMeta); + } - if(options.dynamicMeta) { - var dynamicMeta = options.dynamicMeta(req, res); - logData = _.assign(logData, dynamicMeta); - } + meta = _.assign(meta, logData); + } - if (options.metaField) { - var newMeta = {}; - newMeta[options.metaField] = logData; - logData = newMeta; - } - meta = _.assign(meta, logData); + if (options.metaField) { + var fields; + if (Array.isArray(options.metaField)) { + fields = options.metaField; + } else { + fields = options.metaField.split('.'); + } + _(fields).reverse().forEach(field => { + var newMeta = {}; + newMeta[field] = meta; + meta = newMeta; + }); } meta = _.assign(meta, options.baseMeta); if (options.colorize) { - // Palette from https://github.com/expressjs/morgan/blob/master/index.js#L205 - var statusColor = 'green'; - if (res.statusCode >= 500) statusColor = 'red'; - else if (res.statusCode >= 400) statusColor = 'yellow'; - else if (res.statusCode >= 300) statusColor = 'cyan'; + // Palette from https://github.com/expressjs/morgan/blob/master/index.js#L205 + var statusColor = 'green'; + if (res.statusCode >= 500) statusColor = 'red'; + else if (res.statusCode >= 400) statusColor = 'yellow'; + else if (res.statusCode >= 300) statusColor = 'cyan'; - coloredRes.statusCode = chalk[statusColor](res.statusCode); + coloredRes.statusCode = chalk[statusColor](res.statusCode); } - var msg = template({req: req, res: _.assign({}, res, coloredRes)}); + var msg = template({ req: req, res: _.assign({}, res, coloredRes) }); // This is fire and forget, we don't want logging to hold up the request so don't wait for the callback if (!options.skip(req, res)) { - var level = _.isFunction(options.level) ? options.level(req, res) : options.level; - options.winstonInstance.log(level, msg, meta); + var level = _.isFunction(options.level) ? options.level(req, res) : options.level; + options.winstonInstance.log(_.merge(meta, { level, message: msg })); } }; @@ -345,17 +446,17 @@ function bodyToString(body, isJSON) { } function ensureValidOptions(options) { - if(!options) throw new Error("options are required by express-winston middleware"); - if(!((options.transports && (options.transports.length > 0)) || options.winstonInstance)) - throw new Error("transports or a winstonInstance are required by express-winston middleware"); + if (!options) throw new Error('options are required by express-winston middleware'); + if (!((options.transports && (options.transports.length > 0)) || options.winstonInstance)) + throw new Error('transports or a winstonInstance are required by express-winston middleware'); if (options.dynamicMeta && !_.isFunction(options.dynamicMeta)) { - throw new Error("`dynamicMeta` express-winston option should be a function"); + throw new Error('`dynamicMeta` express-winston option should be a function'); } } function ensureValidLoggerOptions(options) { if (options.ignoreRoute && !_.isFunction(options.ignoreRoute)) { - throw new Error("`ignoreRoute` express-winston option should be a function"); + throw new Error('`ignoreRoute` express-winston option should be a function'); } } diff --git a/package.json b/package.json index 76a9c77..fe05d98 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "middleware", "colors" ], - "version": "2.5.0", + "version": "4.0.4", "repository": { "type": "git", "url": "https://github.com/bithavoc/express-winston.git" @@ -28,43 +28,39 @@ }, "main": "index.js", "scripts": { - "test": "node_modules/.bin/mocha --reporter spec", - "test-travis": "node_modules/.bin/mocha --require blanket --reporter travis-cov", - "test-coverage": "node_modules/.bin/mocha --require blanket --reporter html-cov > coverage.html || true" - }, - "config": { - "travis-cov": { - "threshold": 100 - }, - "blanket": { - "pattern": [ - "index.js" - ], - "data-cover-never": [ - "node_modules", - "test" - ] - } + "pretest": "eslint .", + "test": "node_modules/.bin/mocha --reporter spec" }, "dependencies": { - "chalk": "~0.4.0", - "lodash": "~4.17.5" + "chalk": "^2.4.1", + "lodash": "^4.17.19" }, "devDependencies": { - "blanket": "^1.2.2", - "mocha": "^2.4.5", + "@types/express": "^4.17.1", + "@types/logform": "^1.2.0", + "eslint": "^5.16.0", + "mocha": "^5.2.0", "node-mocks-http": "^1.5.1", - "promise": "^7.1.1", - "should": "^8.2.2", - "travis-cov": "^0.2.5", - "winston": ">=1.x" + "nyc": "^14.1.1", + "should": "^13.2.3", + "winston": ">=3.x <4", + "winston-transport": "^4.2.0" }, "peerDependencies": { - "winston": ">=1.x" + "winston": ">=3.x <4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" }, + "files": [ + "index.js", + "index.d.ts", + "AUTHORS", + "LICENSE", + "CHANGELOG.md", + "Readme.md", + ".npmrc" + ], "license": "MIT", "contributors": [ { @@ -87,6 +83,12 @@ "name": "Robbie Trencheny", "email": "me@robbiet.us", "url": "http://robbie.io" + }, + { + "name": "Matt Morrissette", + "email": "yinzara@gmail.com", + "url": "https://github.com/yinzara" } - ] + ], + "types": "index.d.ts" } diff --git a/test/express-winston-tests.ts b/test/express-winston-tests.ts new file mode 100644 index 0000000..6f80263 --- /dev/null +++ b/test/express-winston-tests.ts @@ -0,0 +1,110 @@ +// Not actually used for tests. Used to test the `index.d.ts` typescript definition matches known examples + +import expressWinston = require('..'); +import * as winston from 'winston'; +import express = require('express'); +import { Format } from 'logform'; + +const app = express(); + +// Logger with all options +app.use(expressWinston.logger({ + baseMeta: { foo: 'foo', nested: { bar: 'baz' } }, + bodyBlacklist: ['foo'], + bodyWhitelist: ['bar'], + colorize: true, + dynamicMeta: (req, res, err) => ({ foo: 'bar' }), + expressFormat: true, + format: new Format(), + ignoreRoute: (req, res) => true, + ignoredRoutes: ['foo'], + level: (req, res) => 'level', + meta: true, + metaField: 'metaField', + msg: 'msg', + requestFilter: (req, prop) => req[prop], + requestWhitelist: ['foo', 'bar'], + skip: (req, res) => false, + statusLevels: ({ error: 'error', success: 'success', warn: 'warn' }), + transports: [ + new winston.transports.Console({}) + ] +})); + +// Logger with minimum options (transport) +app.use(expressWinston.logger({ + transports: [ + new winston.transports.Console({}) + ], +})); + +const logger = winston.createLogger(); + +// Logger with minimum options (winstonInstance) +app.use(expressWinston.logger({ + winstonInstance: logger, +})); + +// Error Logger with all options +app.use(expressWinston.errorLogger({ + baseMeta: { foo: 'foo', nested: { bar: 'baz' } }, + dynamicMeta: (req, res, err) => ({ foo: 'bar' }), + exceptionToMeta: function(error){return {}; }, + format: new Format(), + level: (req, res) => 'level', + meta: true, + metaField: 'metaField', + requestField: 'requestField', + responseField: 'responseField', + msg: 'msg', + requestFilter: (req, prop) => true, + requestWhitelist: ['foo', 'bar'], + headerBlacklist: ['foo', 'bar'], + blacklistedMetaFields: ['foo', 'bar'], + skip: (req, res) => false, + transports: [ + new winston.transports.Console({}) + ] +})); + +// Error Logger with min options (transports) +app.use(expressWinston.errorLogger({ + transports: [ + new winston.transports.Console({}) + ], +})); + +// Error Logger with min options (winstonInstance) +app.use(expressWinston.errorLogger({ + winstonInstance: logger, +})); + +// Request and error logger with function type msg +app.use(expressWinston.logger({ + msg: (req, res) => `HTTP ${req.method} ${req.url} - ${res.statusCode}`, + transports: [ + new winston.transports.Console({}) + ], +})); + +app.use(expressWinston.errorLogger({ + msg: (req, res) => `HTTP ${req.method} ${req.url} - ${res.statusCode}`, + winstonInstance: logger, +})); + +expressWinston.bodyBlacklist.push('potato'); +expressWinston.bodyWhitelist.push('apple'); +expressWinston.defaultRequestFilter = (req: expressWinston.FilterRequest, prop: string) => req[prop]; +expressWinston.defaultResponseFilter = (res: expressWinston.FilterResponse, prop: string) => res[prop]; +expressWinston.defaultSkip = () => true; +expressWinston.ignoredRoutes.push('/ignored'); +expressWinston.responseWhitelist.push('body'); + +const router = express.Router(); + +router.post('/user/register', (req, res, next) => { + const expressWinstonReq = req as expressWinston.ExpressWinstonRequest; + expressWinstonReq._routeWhitelists.body = ['username', 'email', 'age']; + expressWinstonReq._routeWhitelists.req = ['userId']; + expressWinstonReq._routeWhitelists.res = ['_headers']; +}); diff --git a/test/test.js b/test/test.js index a611dd2..e3efa4f 100644 --- a/test/test.js +++ b/test/test.js @@ -1,10 +1,7 @@ -var util = require('util'); - var mocks = require('node-mocks-http'); -var Promise = require('promise/lib/es6-extensions'); var should = require('should'); var _ = require('lodash'); -var winston = require('winston'); +var Transport = require('winston-transport'); var expressWinston = require('../index.js'); @@ -12,28 +9,34 @@ expressWinston.ignoredRoutes.push('/ignored'); expressWinston.responseWhitelist.push('body'); expressWinston.bodyBlacklist.push('potato'); -var MockTransport = function (test, options) { - test.transportInvoked = false; - - winston.Transport.call(this, options || {}); - - this.log = function (level, msg, meta, cb) { - test.transportInvoked = true; - test.log.level = level; - test.log.msg = msg; - test.log.meta = meta; +class MockTransport extends Transport { + constructor(test, options) { + super(options || {}); + + this._test = test; + this._test.transportInvoked = false; + } + + log(info, cb) { + this._test.transportInvoked = true; + this._test.log.level = info.level; + this._test.log.msg = info.message; + this._test.log.meta = info.meta; + this._test.log.metaField = info.metaField; + this._test.log.foobar = info.foobar; this.emit('logged'); return cb(); - }; -}; -util.inherits(MockTransport, winston.Transport); + } +} function mockReq(reqMock) { var reqSpec = _.extend({ method: 'GET', url: '/hello', headers: { - 'header-1': 'value 1' + 'header-1': 'value 1', + 'header-2': 'value 2', + 'header-3': 'value 3' }, query: { val: '1' @@ -150,12 +153,12 @@ describe('express-winston', function () { middleware.length.should.eql(4); }); - it('should use the exported requestWhitelist', function() { + it('should use the exported requestWhitelist', function () { var originalWhitelist = expressWinston.requestWhitelist; expressWinston.requestWhitelist = ['foo']; var options = { - req: {foo: "bar"} + req: { foo: 'bar' } }; return errorLoggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -166,23 +169,6 @@ describe('express-winston', function () { }); }); - it('should use the exported defaultRequestFilter', function() { - var originalRequestFilter = expressWinston.defaultRequestFilter; - expressWinston.defaultRequestFilter = function() { - return 'foo'; - }; - - var options = { - req: {foo: "bar"} - }; - return errorLoggerTestHelper(options).then(function (result) { - // Return to the original value for later tests - expressWinston.defaultRequestFilter = originalRequestFilter; - - result.log.meta.req.url.should.equal('foo'); - }); - }); - describe('when middleware function encounters an error in the pipeline', function () { it('should invoke the transport', function () { return errorLoggerTestHelper().then(function (result) { @@ -197,7 +183,7 @@ describe('express-winston', function () { }); it('should find a custom level of "warn"', function () { - var testHelperOptions = {loggerOptions: {level:'warn'}}; + var testHelperOptions = { loggerOptions: { level: 'warn' } }; return errorLoggerTestHelper(testHelperOptions).then(function (result) { result.log.level.should.eql('warn'); }); @@ -229,11 +215,56 @@ describe('express-winston', function () { }); }); + describe('exceptionToMeta option', function () { + it('should, use exceptionToMeta function when given', function () { + function exceptionToMeta(error) { + return { + stack: error.stack && error.stack.split('\n') + }; + } + + var testHelperOptions = { loggerOptions: { exceptionToMeta: exceptionToMeta } }; + return errorLoggerTestHelper(testHelperOptions).then(function (result) { + result.log.meta.stack.should.be.ok(); + result.log.meta.should.not.have.property('trace'); + }); + }); + + it('should, use getAllInfo function when not given', function () { + var testHelperOptions = { loggerOptions: {} }; + return errorLoggerTestHelper(testHelperOptions).then(function (result) { + result.log.meta.should.have.property('date'); + result.log.meta.should.have.property('process'); + result.log.meta.should.have.property('os'); + result.log.meta.should.have.property('trace'); + result.log.meta.should.have.property('stack'); + }); + }); + }); + + describe('blacklistedMetaFields option', function () { + it('should, remove given fields from the meta result', function () { + var testHelperOptionsWithBlacklist = { loggerOptions: { blacklistedMetaFields: ['trace'] } }; + return errorLoggerTestHelper(testHelperOptionsWithBlacklist).then(function (result) { + result.log.meta.should.not.have.property('trace'); + }); + }); + }); + describe('metaField option', function () { it('should, when using a custom metaField, log the custom metaField', function () { - var testHelperOptions = {loggerOptions: {metaField: 'metaField'}}; + var testHelperOptions = { loggerOptions: { metaField: 'metaField' } }; return errorLoggerTestHelper(testHelperOptions).then(function (result) { - result.log.meta.metaField.req.should.be.ok(); + result.log.metaField.req.should.be.ok(); + }); + }); + }); + + describe('requestField option', function () { + it('should, when using custom requestField, have the request in the alternative property', function () { + var testHelperOptions = { loggerOptions: { requestField: 'httpRequest' } }; + return errorLoggerTestHelper(testHelperOptions).then(function (result) { + result.log.meta.httpRequest.should.be.ok(); }); }); }); @@ -241,7 +272,7 @@ describe('express-winston', function () { describe('requestWhitelist option', function () { it('should default to global requestWhitelist', function () { var options = { - req: {foo: "bar"} + req: { foo: 'bar' } }; return errorLoggerTestHelper(options).then(function (result) { result.log.meta.req.should.not.have.property('foo'); @@ -250,7 +281,7 @@ describe('express-winston', function () { it('should use specified requestWhitelist', function () { var options = { - req: {foo: "bar"}, + req: { foo: 'bar' }, loggerOptions: { requestWhitelist: ['foo'] } @@ -260,36 +291,53 @@ describe('express-winston', function () { result.log.meta.req.should.not.have.property('method'); }); }); + + it('should work with nested requestWhitelist', function () { + var options = { + req: {foo: {test: "bar"}}, + loggerOptions: { + requestWhitelist: ['foo.test'] + } + }; + return errorLoggerTestHelper(options).then(function (result) { + result.log.meta.req.should.have.property('foo'); + result.log.meta.req.foo.should.have.property('test'); + }); + }); }); describe('dynamicMeta option', function () { - var testHelperOptions = { - req: { - body: { - age: 42, - potato: 'Russet' + var testHelperOptions; + + beforeEach(function () { + testHelperOptions = { + req: { + body: { + age: 42, + potato: 'Russet' + }, + user: { + username: 'john@doe.com', + role: 'operator' + } }, - user: { - username: "john@doe.com", - role: "operator" - } - }, - res: { - custom: 'custom response runtime field' - }, - originalError: new Error('FOO'), - loggerOptions: { - meta: true, - dynamicMeta: function(req, res, err) { - return { - user: req.user.username, - role: req.user.role, - custom: res.custom, - errMessage: err.message + res: { + custom: 'custom response runtime field' + }, + originalError: new Error('FOO'), + loggerOptions: { + meta: true, + dynamicMeta: function (req, res, err) { + return { + user: req.user.username, + role: req.user.role, + custom: res.custom, + errMessage: err.message + }; } } - } - }; + }; + }); it('should contain dynamic meta data if meta and dynamicMeta activated', function () { return errorLoggerTestHelper(testHelperOptions).then(function (result) { @@ -303,6 +351,28 @@ describe('express-winston', function () { it('should work with metaField option', function () { testHelperOptions.loggerOptions.metaField = 'metaField'; + return errorLoggerTestHelper(testHelperOptions).then(function (result) { + result.log.metaField.req.should.be.ok(); + result.log.metaField.user.should.equal('john@doe.com'); + result.log.metaField.role.should.equal('operator'); + result.log.metaField.custom.should.equal('custom response runtime field'); + result.log.metaField.errMessage.should.equal('FOO'); + }); + }); + + it('should work with metaField . separated option', function () { + testHelperOptions.loggerOptions.metaField = 'meta.metaField'; + return errorLoggerTestHelper(testHelperOptions).then(function (result) { + result.log.meta.metaField.req.should.be.ok(); + result.log.meta.metaField.user.should.equal('john@doe.com'); + result.log.meta.metaField.role.should.equal('operator'); + result.log.meta.metaField.custom.should.equal('custom response runtime field'); + result.log.meta.metaField.errMessage.should.equal('FOO'); + }); + }); + + it('should work with metaField array option', function () { + testHelperOptions.loggerOptions.metaField = ['meta', 'metaField']; return errorLoggerTestHelper(testHelperOptions).then(function (result) { result.log.meta.metaField.req.should.be.ok(); result.log.meta.metaField.user.should.equal('john@doe.com'); @@ -324,10 +394,35 @@ describe('express-winston', function () { }); it('should throw an error if dynamicMeta is not a function', function () { - var loggerFn = expressWinston.errorLogger.bind(expressWinston, {dynamicMeta: 12}); + var loggerFn = expressWinston.errorLogger.bind(expressWinston, { dynamicMeta: 12 }); loggerFn.should.throw(); }); }); + + describe('skip option', function () { + it('should log error by default', function () { + var options = { + req: { foo: 'bar' } + }; + + return errorLoggerTestHelper(options).then(function (result) { + result.transportInvoked.should.eql(true); + }); + }); + + it('should not log error when function returns true', function () { + var options = { + req: { foo: 'bar' }, + loggerOptions: { + skip: function () {return true;} + } + }; + + return errorLoggerTestHelper(options).then(function (result) { + result.transportInvoked.should.eql(false); + }); + }); + }); }); describe('.logger()', function () { @@ -364,6 +459,7 @@ describe('express-winston', function () { function next(req, res, next) { res.end(); } + var testHelperOptions = { next: next, req: { @@ -381,6 +477,7 @@ describe('express-winston', function () { function next(req, res, next) { res.end(); } + var testHelperOptions = { next: next, req: { @@ -412,6 +509,7 @@ describe('express-winston', function () { res.end('{ "message": "Hi! I\'m a chunk!" }'); } + var testHelperOptions = { next: next, loggerOptions: { @@ -435,12 +533,12 @@ describe('express-winston', function () { }); }); - it('should use the exported requestWhitelist', function() { + it('should use the exported requestWhitelist', function () { var originalWhitelist = expressWinston.requestWhitelist; expressWinston.requestWhitelist = ['foo']; var options = { - req: {foo: "bar"} + req: { foo: 'bar' } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -451,12 +549,12 @@ describe('express-winston', function () { }); }); - it('should use the exported bodyWhitelist', function() { + it('should use the exported bodyWhitelist', function () { var originalWhitelist = expressWinston.bodyWhitelist; expressWinston.bodyWhitelist = ['foo']; var options = { - req: {body: {foo: 'bar', baz: 'qux'}} + req: { body: { foo: 'bar', baz: 'qux' } } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -467,12 +565,12 @@ describe('express-winston', function () { }); }); - it('should use the exported bodyBlacklist', function() { + it('should use the exported bodyBlacklist', function () { var originalBlacklist = expressWinston.bodyBlacklist; expressWinston.bodyBlacklist = ['foo']; var options = { - req: {body: {foo: 'bar', baz: 'qux'}} + req: { body: { foo: 'bar', baz: 'qux' } } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -483,12 +581,12 @@ describe('express-winston', function () { }); }); - it('should use the exported responseWhitelist', function() { + it('should use the exported responseWhitelist', function () { var originalWhitelist = expressWinston.responseWhitelist; expressWinston.responseWhitelist = ['foo']; var options = { - res: {foo: 'bar', baz: 'qux'} + res: { foo: 'bar', baz: 'qux' } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -499,14 +597,14 @@ describe('express-winston', function () { }); }); - it('should use the exported defaultRequestFilter', function() { + it('should use the exported defaultRequestFilter', function () { var originalRequestFilter = expressWinston.defaultRequestFilter; - expressWinston.defaultRequestFilter = function() { + expressWinston.defaultRequestFilter = function () { return 'foo'; }; var options = { - req: {foo: "bar"} + req: { foo: 'bar' } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -516,14 +614,14 @@ describe('express-winston', function () { }); }); - it('should use the exported defaultResponseFilter', function() { + it('should use the exported defaultResponseFilter', function () { var originalResponseFilter = expressWinston.defaultResponseFilter; - expressWinston.defaultResponseFilter = function() { + expressWinston.defaultResponseFilter = function () { return 'foo'; }; var options = { - req: {foo: "bar"} + req: { foo: 'bar' } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -533,14 +631,14 @@ describe('express-winston', function () { }); }); - it('should use the exported defaultSkip', function() { + it('should use the exported defaultSkip', function () { var originalSkip = expressWinston.defaultSkip; - expressWinston.defaultSkip = function() { + expressWinston.defaultSkip = function () { return true; }; var options = { - req: {foo: "bar"} + req: { foo: 'bar' } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -550,12 +648,12 @@ describe('express-winston', function () { }); }); - it('should use the exported ignoredRoutes', function() { + it('should use the exported ignoredRoutes', function () { var originalIgnoredRoutes = expressWinston.ignoredRoutes; expressWinston.ignoredRoutes = ['/foo-route']; var options = { - req: {url: '/foo-route'} + req: { url: '/foo-route' } }; return loggerTestHelper(options).then(function (result) { // Return to the original value for later tests @@ -577,12 +675,13 @@ describe('express-winston', function () { res.end('{ "message": "Hi! I\'m a chunk!" }'); } + var testHelperOptions = { next: next, req: { body: { - username: "bobby", - password: "top-secret", + username: 'bobby', + password: 'top-secret', age: 42, potato: 'Russet' }, @@ -631,9 +730,10 @@ describe('express-winston', function () { }); }); - describe('when middleware function is invoked on a route that returns JSON', function() { - it('should parse JSON in response body', function() { - var bodyObject = { "message": "Hi! I\'m a chunk!" }; + describe('when middleware function is invoked on a route that returns JSON', function () { + it('should parse JSON in response body', function () { + var bodyObject = { 'message': 'Hi! I\'m a chunk!' }; + function next(req, res, next) { // Set Content-Type in a couple different case types, just in case. // Seems like the mock response doesn't quite handle the case @@ -642,12 +742,52 @@ describe('express-winston', function () { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(bodyObject)); } - return loggerTestHelper({next: next}).then(function(result) { - result.log.meta.res.body.should.eql(bodyObject); + + return loggerTestHelper({ next: next }).then(function (result) { + result.log.meta.res.body.should.eql(bodyObject); }); }); - it('should not blow up when response body is invalid JSON', function() { + it('should be string in response body with alternative property name', function () { + function next(req, res, next) { + // Set Content-Type in a couple different case types, just in case. + // Seems like the mock response doesn't quite handle the case + // translation on these right. + res.end('foo'); + } + + return loggerTestHelper({ next: next, loggerOptions: { responseField: 'response' } }) + .then(function (result) { + result.log.meta.response.body.should.eql('foo'); + }); + }); + + it('should merge filtered request/response under property', function () { + function next(req, res, next) { + res.end('foo'); + } + + return loggerTestHelper( + { + next, + loggerOptions: + { + responseField: 'httpRequest', + requestField: 'httpRequest', + responseWhitelist: [...expressWinston.responseWhitelist, 'responseTime'] + } + }) + .then(function (result) { + result.log.meta.httpRequest.body.should.eql('foo'); + result.log.meta.httpRequest.statusCode.should.eql(200); + should.exist(result.log.meta.httpRequest.responseTime); + should.exist(result.log.meta.httpRequest.url); + should.exist(result.log.meta.httpRequest.headers); + should.exist(result.log.meta.httpRequest.method); + }); + }); + + it('should not blow up when response body is invalid JSON', function () { function next(req, res, next) { // Set Content-Type in a couple different case types, just in case. // Seems like the mock response doesn't quite handle the case @@ -656,13 +796,14 @@ describe('express-winston', function () { res.setHeader('content-type', 'application/json'); res.end('}'); } - return loggerTestHelper({next: next}); + + return loggerTestHelper({ next: next }); }); }); describe('when middleware function is invoked on a route that should be ignored (by .ignoredRoutes)', function () { var testHelperOptions = { - req: {url: '/ignored'} + req: { url: '/ignored' } }; it('should not invoke the transport', function () { @@ -696,7 +837,7 @@ describe('express-winston', function () { }); }); - it('should not emit colors when colorize option is false', function() { + it('should not emit colors when colorize option is false', function () { var testHelperOptions = { loggerOptions: { colorize: false, @@ -713,7 +854,7 @@ describe('express-winston', function () { }); }); - it('should not emit colors when colorize option is not present', function() { + it('should not emit colors when colorize option is not present', function () { var testHelperOptions = { loggerOptions: { colorize: false, @@ -831,6 +972,34 @@ describe('express-winston', function () { result.log.msg.should.eql('Foo GET /all-the-things'); }); }); + + it('can be a function', function () { + var testHelperOptions = { + loggerOptions: { + msg: function (req) { return 'fn ' + req.url; } + }, + req: { + url: '/all-the-things' + } + }; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.msg.should.eql('fn /all-the-things'); + }); + }); + + it('can be interpolated when it is a function', function () { + var testHelperOptions = { + loggerOptions: { + msg: function () { return 'fn {{req.url}}'; } + }, + req: { + url: '/all-the-things' + } + }; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.msg.should.eql('fn /all-the-things'); + }); + }); }); describe('ignoreRoute option', function () { @@ -881,7 +1050,7 @@ describe('express-winston', function () { } }; return loggerTestHelper(testHelperOptions).then(function (result) { - result.log.meta.foobar.req.should.be.ok(); + result.log.foobar.req.should.be.ok(); }); }); }); @@ -891,7 +1060,7 @@ describe('express-winston', function () { var testHelperOptions = { loggerOptions: { skip: function (req, res) { - return req.url.indexOf('sandwich') != -1 + return req.url.indexOf('sandwich') != -1; } }, req: { @@ -907,7 +1076,7 @@ describe('express-winston', function () { var testHelperOptions = { loggerOptions: { skip: function (req, res) { - return req.url.indexOf('sandwich') != -1 + return req.url.indexOf('sandwich') != -1; } }, req: { @@ -986,7 +1155,7 @@ describe('express-winston', function () { res.status(200).end('{ "message": "Hi! I\'m a chunk!" }'); }, loggerOptions: { - statusLevels: {success: 'silly'} + statusLevels: { success: 'silly' } }, transportOptions: { level: 'silly' @@ -1003,7 +1172,7 @@ describe('express-winston', function () { res.status(403).end('{ "message": "Hi! I\'m a chunk!" }'); }, loggerOptions: { - statusLevels: {warn: 'debug'} + statusLevels: { warn: 'debug' } }, transportOptions: { level: 'silly' @@ -1020,7 +1189,7 @@ describe('express-winston', function () { res.status(500).end('{ "message": "Hi! I\'m a chunk!" }'); }, loggerOptions: { - statusLevels: {error: 'verbose'} + statusLevels: { error: 'verbose' } }, transportOptions: { level: 'silly' @@ -1034,62 +1203,112 @@ describe('express-winston', function () { }); describe('when levels set to a function', function () { - it('should have custom status level provided by the function when 100 <= statusCode < 400', function () { - var testHelperOptions = { - next: function (req, res, next) { - res.status(200).end('{ "message": "Hi! I\'m a chunk!" }'); - }, - loggerOptions: { - level: function(req,res) { return 'silly'; } - }, - transportOptions: { - level: 'silly' - } - }; - return loggerTestHelper(testHelperOptions).then(function (result) { - result.log.level.should.equal('silly'); - }); + it('should have custom status level provided by the function when 100 <= statusCode < 400', function () { + var testHelperOptions = { + next: function (req, res, next) { + res.status(200).end('{ "message": "Hi! I\'m a chunk!" }'); + }, + loggerOptions: { + level: function (req, res) { return 'silly'; } + }, + transportOptions: { + level: 'silly' + } + }; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.level.should.equal('silly'); }); + }); - it('should have custom status level provided by the function when 400 <= statusCode < 500', function () { - var testHelperOptions = { - next: function (req, res, next) { - res.status(403).end('{ "message": "Hi! I\'m a chunk!" }'); - }, - loggerOptions: { - level: function(req,res) { return 'silly'; } - }, - transportOptions: { - level: 'silly' - } - }; - return loggerTestHelper(testHelperOptions).then(function (result) { - result.log.level.should.equal('silly'); - }); + it('should have custom status level provided by the function when 400 <= statusCode < 500', function () { + var testHelperOptions = { + next: function (req, res, next) { + res.status(403).end('{ "message": "Hi! I\'m a chunk!" }'); + }, + loggerOptions: { + level: function (req, res) { return 'silly'; } + }, + transportOptions: { + level: 'silly' + } + }; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.level.should.equal('silly'); }); + }); - it('should have custom status level provided by the function when 500 <= statusCode', function () { - var testHelperOptions = { - next: function (req, res, next) { - res.status(500).end('{ "message": "Hi! I\'m a chunk!" }'); - }, - loggerOptions: { - level: function(req,res) { return 'silly'; } - }, - transportOptions: { - level: 'silly' - } - }; - return loggerTestHelper(testHelperOptions).then(function (result) { - result.log.level.should.equal('silly'); - }); + it('should have custom status level provided by the function when 500 <= statusCode', function () { + var testHelperOptions = { + next: function (req, res, next) { + res.status(500).end('{ "message": "Hi! I\'m a chunk!" }'); + }, + loggerOptions: { + level: function (req, res) { return 'silly'; } + }, + transportOptions: { + level: 'silly' + } + }; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.level.should.equal('silly'); }); + }); + }); + + describe('headerBlacklist option', function () { + it('should default to global defaultHeaderBlackList', function () { + return loggerTestHelper().then(function (result) { + result.log.meta.req.headers.should.have.property('header-1'); + result.log.meta.req.headers.should.have.property('header-2'); + result.log.meta.req.headers.should.have.property('header-3'); + }); + }); + + it('should use specified headerBlackList', function () { + var options = { + loggerOptions: { + headerBlacklist: ['header-1', 'Header-3'] + } + }; + return loggerTestHelper(options).then(function (result) { + result.log.meta.req.headers.should.not.have.property('header-1'); + result.log.meta.req.headers.should.have.property('header-2'); + result.log.meta.req.headers.should.not.have.property('header-3'); + }); + }); + + it('should not use specified headerBlackList since the requestWhiteList is empty', function () { + var options = { + loggerOptions: { + requestWhitelist: ['url'], + headerBlacklist: ['header-1'] + } + }; + return loggerTestHelper(options).then(function (result) { + result.log.meta.req.should.not.have.property('headers'); + }); + }); + + it('should not headerBlackList but since a requestFilter is set', function () { + const customRequestFilter = (req, propName) => { + return (propName !== 'headers') ? req[propName] : undefined; + }; + var options = { + loggerOptions: { + requestFilter: customRequestFilter, + headerBlacklist: ['header-1'] + } + }; + return loggerTestHelper(options).then(function (result) { + result.log.meta.req.should.not.have.property('headers'); + }); + }); }); describe('requestWhitelist option', function () { it('should default to global requestWhitelist', function () { var options = { - req: {foo: "bar"} + req: { foo: 'bar' } }; return loggerTestHelper(options).then(function (result) { result.log.meta.req.should.not.have.property('foo'); @@ -1098,7 +1317,7 @@ describe('express-winston', function () { it('should use specified requestWhitelist', function () { var options = { - req: {foo: "bar"}, + req: { foo: 'bar' }, loggerOptions: { requestWhitelist: ['foo'] } @@ -1109,7 +1328,7 @@ describe('express-winston', function () { }); }); - it('should not include a req in the log when there is no request whitelist', function() { + it('should not include a req in the log when there is no request whitelist', function () { var options = { loggerOptions: { requestWhitelist: [], @@ -1122,14 +1341,14 @@ describe('express-winston', function () { }); describe('bodyBlacklist option', function () { - it('should remove the body if it is requestWhitelisted and the bodyBlacklist removes all properties', function() { + it('should remove the body if it is requestWhitelisted and the bodyBlacklist removes all properties', function () { var options = { loggerOptions: { bodyBlacklist: ['foo', 'baz'], requestWhitelist: ['body'], }, req: { - body: {foo: 'bar', baz: 'qux'} + body: { foo: 'bar', baz: 'qux' } } }; return loggerTestHelper(options).then(function (result) { @@ -1141,7 +1360,7 @@ describe('express-winston', function () { describe('responseWhitelist option', function () { it('should default to global responseWhitelist', function () { var options = { - res: {foo: "bar"} + res: { foo: 'bar' } }; return loggerTestHelper(options).then(function (result) { result.log.meta.res.should.not.have.property('foo'); @@ -1150,7 +1369,7 @@ describe('express-winston', function () { it('should use specified responseWhitelist', function () { var options = { - res: {foo: "bar"}, + res: { foo: 'bar' }, loggerOptions: { responseWhitelist: ['foo'] } @@ -1160,12 +1379,25 @@ describe('express-winston', function () { result.log.meta.res.should.not.have.property('method'); }); }); + + it('should work with nested responseWhitelist', function () { + var options = { + res: {foo: {test: "bar"}}, + loggerOptions: { + responseWhitelist: ['foo.test'] + } + }; + return loggerTestHelper(options).then(function (result) { + result.log.meta.res.should.have.property('foo'); + result.log.meta.res.foo.should.have.property('test'); + }); + }); }); describe('ignoredRoutes option', function () { it('should default to global ignoredRoutes', function () { var options = { - req: {url: "/ignored"} + req: { url: '/ignored' } }; return loggerTestHelper(options).then(function (result) { result.transportInvoked.should.eql(false); @@ -1174,7 +1406,7 @@ describe('express-winston', function () { it('should use specified ignoredRoutes', function () { var options = { - req: {url: "/ignored-option"}, + req: { url: '/ignored-option' }, loggerOptions: { ignoredRoutes: ['/ignored-option'] } @@ -1186,31 +1418,34 @@ describe('express-winston', function () { }); describe('dynamicMeta option', function () { - var testHelperOptions = { - req: { - body: { - age: 42, - potato: 'Russet' + var testHelperOptions; + beforeEach(() => { + testHelperOptions = { + req: { + body: { + age: 42, + potato: 'Russet' + }, + user: { + username: 'john@doe.com', + role: 'operator' + } }, - user: { - username: "john@doe.com", - role: "operator" - } - }, - res: { - custom: 'custom response runtime field' - }, - loggerOptions: { - meta: true, - dynamicMeta: function(req, res) { - return { - user: req.user.username, - role: req.user.role, - custom: res.custom + res: { + custom: 'custom response runtime field' + }, + loggerOptions: { + meta: true, + dynamicMeta: function (req, res) { + return { + user: req.user.username, + role: req.user.role, + custom: res.custom + }; } } - } - }; + }; + }); it('should contain dynamic meta data if meta and dynamicMeta activated', function () { return loggerTestHelper(testHelperOptions).then(function (result) { @@ -1221,8 +1456,35 @@ describe('express-winston', function () { }); }); + it('should contain request under alternative property if defined', function () { + testHelperOptions.loggerOptions.requestField = 'httpRequest'; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.meta.httpRequest.should.be.ok(); + }); + }); + it('should work with metaField option', function () { testHelperOptions.loggerOptions.metaField = 'metaField'; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.metaField.req.should.be.ok(); + result.log.metaField.user.should.equal('john@doe.com'); + result.log.metaField.role.should.equal('operator'); + result.log.metaField.custom.should.equal('custom response runtime field'); + }); + }); + + it('should work with metaField option with . syntax', function () { + testHelperOptions.loggerOptions.metaField = 'meta.metaField'; + return loggerTestHelper(testHelperOptions).then(function (result) { + result.log.meta.metaField.req.should.be.ok(); + result.log.meta.metaField.user.should.equal('john@doe.com'); + result.log.meta.metaField.role.should.equal('operator'); + result.log.meta.metaField.custom.should.equal('custom response runtime field'); + }); + }); + + it('should work with metaField option with array syntax', function () { + testHelperOptions.loggerOptions.metaField = ['meta', 'metaField']; return loggerTestHelper(testHelperOptions).then(function (result) { result.log.meta.metaField.req.should.be.ok(); result.log.meta.metaField.user.should.equal('john@doe.com'); @@ -1242,7 +1504,7 @@ describe('express-winston', function () { }); it('should throw an error if dynamicMeta is not a function', function () { - var loggerFn = expressWinston.logger.bind(expressWinston, {dynamicMeta: 12}); + var loggerFn = expressWinston.logger.bind(expressWinston, { dynamicMeta: 12 }); loggerFn.should.throw(); }); }); @@ -1270,6 +1532,12 @@ describe('express-winston', function () { }); }); + describe('.defaultHeaderBlacklist', function () { + it('should be an array with all the header which are prevented to be logged', function () { + expressWinston.defaultHeaderBlacklist.should.be.an.Array(); + }); + }); + describe('.defaultRequestFilter', function () { it('should be a function', function () { expressWinston.defaultRequestFilter.should.be.a.Function(); @@ -1293,4 +1561,8 @@ describe('express-winston', function () { expressWinston.ignoredRoutes.should.be.an.Array(); }); }); + + describe('google-logging configuration', () => { + + }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2e8b7cf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "test/express-winston-tests.ts" + ] +}