├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── express-logger.js ├── logger-helper.js └── utils.js ├── package-lock.json ├── package.json └── test ├── express-logger-test.js ├── logger-helper-test.js └── utils-test.js /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | package-lock-lint: 11 | name: package-lock lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 18 18 | - name: lint package-lock 19 | run: npx lockfile-lint --path package-lock.json --allowed-hosts npm --validate-https 20 | 21 | test: 22 | strategy: 23 | matrix: 24 | platform: [ubuntu-latest] 25 | node: ['14', '16', '18'] 26 | needs: [package-lock-lint] 27 | name: test (node V${{ matrix.node}}) 28 | runs-on: ${{ matrix.platform }} 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node }} 34 | - name: Install Dependencies 35 | run: npm ci 36 | - name: Run Tests & Collect Coverage 37 | run: npm run test:coveralls 38 | - name: Coveralls 39 | uses: coverallsapp/github-action@master 40 | if: matrix.node == '18' 41 | with: 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [ workflow_dispatch ] 4 | 5 | jobs: 6 | release: 7 | name: Release 8 | runs-on: ubuntu-latest 9 | if: github.ref == 'refs/heads/master' 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: 20 15 | - name: Install Dependencies 16 | run: npm ci --ignore-scripts 17 | - name: Release 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | run: npx semantic-release 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | /.history 40 | /.vscode 41 | 42 | # IntelliJ files 43 | /.idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [4.0.3] - 2025-04-14 9 | 10 | ### Fix 11 | - use of undefined res object 12 | 13 | 14 | ## [4.0.2] - 2025-02-25 15 | 16 | ### Fix 17 | - handle nest js edge case 18 | 19 | ## [4.0.0] - 2023-03-01 20 | 21 | ### Breaking 22 | - 5xx status code log message level is now ERROR [@ugolas](https://github.com/ugolas). 23 | - Remove official support for node versions older than 16 [@kobik](https://github.com/kobik). 24 | 25 | ### Added 26 | - Accepts a user function to control whether to print the log message [@ugolas](https://github.com/ugolas). 27 | 28 | 29 | ## [3.0.3] - 2021-03-08 30 | 31 | ### Changed 32 | 33 | - Fix known reported vulnerabilities from [@kobik](https://github.com/kobik). 34 | 35 | ## [3.0.2] - 2020-08-16 36 | 37 | ### Changed 38 | 39 | - Fix known reported vulnerabilities from [@ugolas](https://github.com/ugolas). 40 | 41 | ## [3.0.1] - 2019-07-01 42 | 43 | ### Changed 44 | 45 | - Update bunyan dependencies to fix vulnerabilities from [@ugolas](https://github.com/ugolas). 46 | 47 | ## [3.0.0] - 2019-02-15 48 | 49 | ### Added 50 | 51 | - Add CHANGELOG.md from [@ugolas](https://github.com/ugolas). 52 | 53 | ### Changed 54 | 55 | - Removed support for objects configuration in array fields from [@ugolas](https://github.com/ugolas). 56 | - Fixed vulnerabilities from [@ugolas](https://github.com/ugolas). 57 | - Replaced coverage reporter from istanbul to nyc from [@ugolas](https://github.com/ugolas). 58 | - Removed support for node 6 from [@ugolas](https://github.com/ugolas). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version][npm-image]][npm-url] 2 | [![Build Status][travis-image]][travis-url] 3 | [![Test Coverage][coveralls-image]][coveralls-url] 4 | [![NPM Downloads][downloads-image]][downloads-url] 5 | [![MIT License][license-image]][license-url] 6 | 7 | # express-request-logger 8 | Middleware for logging request/responses in Express apps 9 | 10 | ## Supported features 11 | - Logging request 12 | - Logging response 13 | - Mask request body fields 14 | - Exclude request body fields 15 | - Exclude request specific headers 16 | - Mask response body fields 17 | - Exclude response body fields 18 | - Exclude response specific headers 19 | - Exclude specific URLs from logging 20 | - Supported by Node v8 and above. 21 | 22 | ## Installation 23 | 24 | This is a [Node.js](https://nodejs.org/en/) module available through the 25 | [npm registry](https://www.npmjs.com/). Installation is done using the 26 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 27 | 28 | ```sh 29 | $ npm install express-requests-logger 30 | ``` 31 | 32 | ## API 33 | 34 | ```js 35 | var audit = require('express-requests-logger') 36 | ``` 37 | 38 | ### audit(options) 39 | 40 | Create an audit middleware with ther given `options`. 41 | 42 | #### options 43 | 44 | the `express-requests-logger` accepts the following properties in the options object. 45 | 46 | #### logger 47 | 48 | The logger to use for logging the request/response. 49 | Package tested only with [bunyan](https://github.com/trentm/node-bunyan) logger, but should work with any logger which has a `info` method which takes an object. 50 | 51 | #### shouldSkipAuditFunc 52 | 53 | Should be a function, that returns boolean value to indicate whether to skip the audit for the current request. Usually the logic should be around the request/response params. Useful to provide a custom logic for cases we would want to skip logging specific request. 54 | 55 | The default implementation of the function returns false. 56 | 57 | Example, skipping logging of all success responses: 58 | ```js 59 | shouldSkipAuditFunc: function(req, res){ 60 | let shouldSkip = false; 61 | if (res.statusCode === 200){ 62 | // _bodyJson is added by this package 63 | if (res._bodyJson.result === "success"){ 64 | shouldSkip = true; 65 | } 66 | } 67 | 68 | return shouldSkip; 69 | } 70 | ``` 71 | #### doubleAudit 72 | 73 | `true` - log once the request arrives (request details), and log after response is sent (both request and response). - Useful if there is a concern that the server will crash during the request and there is a need to log the request before it's processed. 74 | 75 | `false` - log only after the response is sent. 76 | #### excludeURLs 77 | 78 | Array of strings - if the request url matches one of the values in the array, the request/response won't be logged. 79 | For example: if there is a path `/v1/health` that we do not want to log, add: 80 | ```js 81 | excludeURLs: ['health'] 82 | ``` 83 | #### request 84 | 85 | Specific configuration for requests 86 | ##### audit 87 | 88 | Boolean - `true` - include request in audit, `false` - don't. 89 | 90 | ##### excludeBody 91 | 92 | Array of strings - pass the fields you wish to exclude in the body of the requests (sensitive data like passwords, credit cards numbers etc..). 93 | `*` field - exclude all body 94 | 95 | ##### maskBody 96 | 97 | Array of strings - pass the fields you wish to mask in the body of the requests (sensitive data like passwords, credit cards numbers etc..). 98 | 99 | ##### maskQuery 100 | 101 | Array of strings - pass the fields you wish to mask in the query of the requests (sensitive data like passwords, credit cards numbers etc..). 102 | ##### excludeHeaders 103 | 104 | Array of strings - pass the header names you wish to exclude from the audit (senstitive data like authorization headers etc..). 105 | `*` field - exclude all headers 106 | 107 | ##### maskHeaders 108 | 109 | Array of strings - pass the fields you wish to mask in the headers of the requests (senstitive data like authorization headers etc..). 110 | 111 | ##### maxBodyLength 112 | 113 | Restrict request body's logged content length (inputs other than positive integers will be ignored). 114 | 115 | ##### customMaskBodyFunc 116 | 117 | Additional to mask options, you can add your own functionality to mask request body. This function will execute 118 | as a masking function before the package functions. 119 | The custom function gets the full express request and should return the masked body. 120 | 121 | #### response 122 | 123 | Specific configuration for responses 124 | 125 | **Doesn't print headers for Node below v6.9.2** 126 | 127 | **Non JSON responses are not masked, and are logged as is. This is deducted from the response header `content-type`** 128 | 129 | ##### audit 130 | 131 | Boolean - `true` - include response in audit, `false` - don't. 132 | 133 | ##### excludeBody 134 | 135 | Array of strings - pass the fields you wish to exclude in the body of the responses (sensitive data like passwords, credit cards numbers etc..). 136 | `*` field - exclude all body 137 | 138 | ##### maskBody 139 | 140 | Array of strings - pass the fields you wish to mask in the body of the responses (sensitive data like passwords, credit cards numbers etc..). 141 | 142 | ##### excludeHeaders 143 | 144 | Array of strings - pass the header names you wish to exclude from the audit (senstitive data like authorization headers etc..). 145 | `*` field - exclude all headers 146 | 147 | ##### maskHeaders 148 | 149 | Array of strings - pass the fields you wish to mask in the headers of the responses (senstitive data like authorization headers etc..). 150 | 151 | ##### levels 152 | 153 | Map of statusCodes to log levels. By default the audit is logged with level 'info'. It is possible to override it by configuration according to the statusCode of the response: 154 | 155 | - Key: status code, or status code group: '2xx', '401', etc.. First we try to match by exact match (for example 400), if no key found by exact match we fallback to match bu group (4xx). 156 | - Value: log level, valid values: 'trace', 'debug', 'info', 'warn', 'error'. 157 | - Configuration errors are ignored and the log is info by default. 158 | 159 | ##### maxBodyLength 160 | 161 | Restrict response body's logged content length (inputs other than positive integers will be ignored). 162 | 163 | 164 | Example: 165 | ``` 166 | levels: { 167 | "2xx":"info", // All 2xx responses are info 168 | "401":"warn", // 401 are warn 169 | "4xx':info", // All 4xx except 401 are info 170 | "503":"warn", 171 | "5xx":"error" // All 5xx except 503 are errors, 503 is warn, 172 | } 173 | ``` 174 | 175 | 176 | ### Example 177 | 178 | ```js 179 | app.use(audit({ 180 | logger: logger, // Existing bunyan logger 181 | excludeURLs: [‘health’, ‘metrics’], // Exclude paths which enclude 'health' & 'metrics' 182 | request: { 183 | maskBody: [‘password’], // Mask 'password' field in incoming requests 184 | excludeHeaders: [‘authorization’], // Exclude 'authorization' header from requests 185 | excludeBody: [‘creditCard’], // Exclude 'creditCard' field from requests body 186 | maskHeaders: [‘header1’], // Mask 'header1' header in incoming requests 187 | maxBodyLength: 50 // limit length to 50 chars + '...' 188 | }, 189 | response: { 190 | maskBody: [‘session_token’] // Mask 'session_token' field in response body 191 | excludeHeaders: [‘*’], // Exclude all headers from responses, 192 | excludeBody: [‘*’], // Exclude all body from responses 193 | maskHeaders: [‘header1’], // Mask 'header1' header in incoming requests 194 | maxBodyLength: 50 // limit length to 50 chars + '...' 195 | }, 196 | shouldSkipAuditFunc: function(req, res){ 197 | // Custom logic here.. i.e: return res.statusCode === 200 198 | return false; 199 | } 200 | })); 201 | ``` 202 | 203 | [npm-image]: https://img.shields.io/npm/v/express-requests-logger.svg?style=flat 204 | [npm-url]: https://npmjs.org/package/express-requests-logger 205 | [travis-image]: https://travis-ci.org/PayU/express-request-logger.svg?branch=master 206 | [travis-url]: https://travis-ci.org/PayU/express-request-logger 207 | [coveralls-image]: https://coveralls.io/repos/github/PayU/express-request-logger/badge.svg?branch=master 208 | [coveralls-url]: https://coveralls.io/github/PayU/express-request-logger?branch=master 209 | [downloads-image]: http://img.shields.io/npm/dm/express-requests-logger.svg?style=flat 210 | [downloads-url]: https://npmjs.org/package/express-requests-logger 211 | [license-image]: https://img.shields.io/badge/License-Apache%202.0-blue.svg 212 | [license-url]: https://opensource.org/licenses/Apache-2.0 213 | [nsp-image]: https://nodesecurity.io/orgs/zooz/projects/ca2387c7-874c-4f5d-bd4e-0aa2874a1ae1/badge 214 | [nsp-url]: https://nodesecurity.io/orgs/zooz/projects/ca2387c7-874c-4f5d-bd4e-0aa2874a1ae1 215 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/express-logger.js'); 2 | -------------------------------------------------------------------------------- /lib/express-logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | logger = require('bunyan').createLogger({ name: 'ExpressLogger' }), 5 | loggerHelper = require('./logger-helper'), 6 | setupOptions, 7 | flatten = require('flat'); 8 | 9 | var audit = function (req, res, next) { 10 | var oldWrite = res.write; 11 | var oldEnd = res.end; 12 | var oldJson = res.json; 13 | var chunks = []; 14 | 15 | // Log start time of request processing 16 | req.timestamp = new Date(); 17 | 18 | if (setupOptions.doubleAudit) { 19 | loggerHelper.auditRequest(req, setupOptions); 20 | } 21 | 22 | res.write = function (chunk) { 23 | chunks.push(Buffer.from(chunk)); 24 | oldWrite.apply(res, arguments); 25 | }; 26 | 27 | // decorate response#json method from express 28 | res.json = function (bodyJson) { 29 | res._bodyJson = bodyJson; 30 | oldJson.apply(res, arguments); 31 | }; 32 | 33 | // decorate response#end method from express 34 | res.end = function (chunk) { 35 | res.timestamp = new Date(); 36 | if (chunk) { 37 | chunks.push(Buffer.from(chunk)); 38 | } 39 | 40 | res._bodyStr = Buffer.concat(chunks).toString('utf8'); 41 | 42 | // call to original express#res.end() 43 | oldEnd.apply(res, arguments); 44 | loggerHelper.auditResponse(req, res, setupOptions); 45 | }; 46 | 47 | next(); 48 | }; 49 | 50 | module.exports = function (options) { 51 | options = options || {}; 52 | var defaults = { 53 | logger: logger, 54 | request: { 55 | audit: true, 56 | maskBody: [], 57 | maskQuery: [], 58 | maskHeaders: [], 59 | excludeBody: [], 60 | excludeHeaders: [] 61 | }, 62 | response: { 63 | audit: true, 64 | maskBody: [], 65 | maskHeaders: [], 66 | excludeBody: [], 67 | excludeHeaders: [] 68 | }, 69 | doubleAudit: false, 70 | excludeURLs: [], 71 | levels: { 72 | '2xx': 'info', 73 | '3xx': 'info', 74 | '4xx': 'info', 75 | '5xx': 'error' 76 | }, 77 | shouldSkipAuditFunc: function(req, res){ 78 | return false; 79 | } 80 | }; 81 | 82 | _.defaultsDeep(options, defaults); 83 | setupOptions = validateArrayFields(options, defaults); 84 | setBodyLengthFields(setupOptions); 85 | return audit; 86 | }; 87 | 88 | // Convert all options fields that need to be array by default 89 | function validateArrayFields(options, defaults) { 90 | let defaultsCopy = Object.assign({}, defaults); 91 | delete defaultsCopy.logger; 92 | 93 | Object.keys(flatten(defaultsCopy)).forEach((key) => { 94 | let optionValue = _.get(options, key); 95 | let defaultValue = _.get(defaultsCopy, key) 96 | if (_.isArray(defaultValue) && !_.isArray(optionValue)) { 97 | // throw error - wrong type passed 98 | let errMsg = `Invalid value specified for field: ${key}, expected array`; 99 | throw new Error(errMsg); 100 | } 101 | }); 102 | 103 | return options; 104 | } 105 | 106 | function setBodyLengthFields(options) { 107 | const isValid = field => field && !isNaN(field) && field > 0; 108 | options.request.maxBodyLength = !isValid(options.request.maxBodyLength) ? undefined : options.request.maxBodyLength; 109 | options.response.maxBodyLength = !isValid(options.response.maxBodyLength) ? undefined : options.response.maxBodyLength; 110 | } -------------------------------------------------------------------------------- /lib/logger-helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('./utils'); 4 | var _ = require('lodash'); 5 | var ALL_FIELDS = '*'; 6 | const NA = 'N/A'; 7 | const DEFAULT_LOG_LEVEL = 'info'; 8 | const START = 'start'; 9 | const END = 'end'; 10 | 11 | var auditRequest = function (req, options) { 12 | var shouldAudit = utils.shouldAuditURL(options.excludeURLs, req); 13 | 14 | if (shouldAudit) { 15 | var request; 16 | 17 | if (options.setupFunc) { 18 | options.setupFunc(req); 19 | } 20 | 21 | if (options.request.audit) { 22 | request = getRequestAudit(req, options); 23 | } 24 | 25 | var auditObject = { 26 | request: request, 27 | 'millis-timestamp': Date.now(), 28 | 'utc-timestamp': new Date().toISOString(), 29 | stage: START 30 | }; 31 | 32 | // Add additional audit fields 33 | if (req && req.additionalAudit) { 34 | auditObject = Object.assign(auditObject, req.additionalAudit); 35 | } 36 | 37 | options.logger.info(auditObject, 'Inbound Transaction'); 38 | } 39 | }; 40 | 41 | var auditResponse = function (req, res, options) { 42 | var request; 43 | var response; 44 | 45 | var shouldAudit = utils.shouldAuditURL(options.excludeURLs, req); 46 | let shouldSkipAuditCustomConditions = options.shouldSkipAuditFunc ? options.shouldSkipAuditFunc(req, res) : false; 47 | if (shouldAudit && !shouldSkipAuditCustomConditions) { 48 | if (options.setupFunc) { 49 | options.setupFunc(req, res); 50 | } 51 | 52 | if (options.request.audit) { 53 | request = getRequestAudit(req, options); 54 | } 55 | 56 | if (options.response.audit) { 57 | response = getResponseAudit(req, res, options); 58 | } 59 | 60 | var auditObject = { 61 | response: response, 62 | request: request, 63 | 'millis-timestamp': Date.now(), 64 | 'utc-timestamp': new Date().toISOString(), 65 | stage: END 66 | }; 67 | 68 | // Add additional audit fields 69 | if (req && req.additionalAudit) { 70 | auditObject = Object.assign(auditObject, req.additionalAudit); 71 | } 72 | 73 | let level = DEFAULT_LOG_LEVEL; // Default 74 | if (res) { 75 | // Make sure the resolved log level is supported by our logger: 76 | let resolvedLogLevel = utils.getLogLevel(res.statusCode, options.levels); 77 | level = options.logger[resolvedLogLevel] ? resolvedLogLevel : level; 78 | } 79 | options.logger[level](auditObject, 'Inbound Transaction'); 80 | } 81 | }; 82 | 83 | function getRequestAudit(req, options) { 84 | var headers = _.get(req, 'headers'); 85 | var requestFullURL = utils.getUrl(req); 86 | var requestRoute = utils.getRoute(req); 87 | var queryParams = req && req.query !== {} ? req.query : NA; 88 | var method = req && req.method ? req.method : NA; 89 | var URLParams = req && req.params ? req.params : NA; 90 | var timestamp = req && req.timestamp ? req.timestamp.toISOString() : NA; 91 | var timestamp_ms = req && req.timestamp ? req.timestamp.valueOf() : NA; 92 | var requestBody = _.get(req, 'body'); //handle body clone the original body 93 | 94 | if (options.request.customMaskBodyFunc) { 95 | requestBody = options.request.customMaskBodyFunc(req); 96 | } 97 | 98 | if (isJsonBody(headers)) { 99 | requestBody = handleJson(requestBody, options.logger, options.request.excludeBody, options.request.maskBody); 100 | } 101 | 102 | queryParams = getMaskedQuery(queryParams, options.request.maskQuery); 103 | 104 | headers = handleJson(headers, options.logger, options.request.excludeHeaders, options.request.maskHeaders); 105 | 106 | var auditObject = { 107 | method: method, 108 | url_params: URLParams, 109 | url: requestFullURL, 110 | url_route: requestRoute, 111 | query: queryParams, 112 | headers: _.isEmpty(headers) ? NA : headers, 113 | timestamp: timestamp, 114 | timestamp_ms: timestamp_ms, 115 | body: utils.getBodyStr(requestBody, options.request.maxBodyLength) 116 | }; 117 | 118 | return auditObject; 119 | } 120 | 121 | function handleResponseJson(objJson, objStr, logger, excludeFields, maskFields) { 122 | let result; 123 | if (shouldBeParsed(maskFields, excludeFields)) { 124 | result = objJson || objStr 125 | } else { 126 | result = objStr || objJson 127 | } 128 | return handleJson(result, logger, excludeFields, maskFields); 129 | } 130 | 131 | function handleJson(obj, logger, excludeFields, maskFields) { 132 | 133 | let result = obj; 134 | if (_.includes(excludeFields, ALL_FIELDS)) { 135 | result = undefined; 136 | } 137 | else if (obj) { 138 | if (shouldBeParsed(maskFields, excludeFields)) { 139 | try { 140 | let jsonObj; 141 | if (typeof obj === 'string') { 142 | jsonObj = JSON.parse(obj); 143 | } 144 | else if (typeof obj !== 'object') { 145 | throw new Error('only json obj can be exclude/masked'); 146 | } else { 147 | jsonObj = obj; 148 | } 149 | //order is important because body is clone first 150 | let maskedClonedObj = utils.maskJson(jsonObj, maskFields); 151 | result = utils.cleanOmitKeys(maskedClonedObj, excludeFields); 152 | } catch (err) { 153 | logger.warn({ 154 | error: { 155 | message: err.message, 156 | stack: err.stack 157 | } 158 | }, 'Error parsing json'); 159 | result = undefined; 160 | } 161 | } 162 | } 163 | return result; 164 | } 165 | 166 | function shouldBeParsed(maskFields, excludeFields) { 167 | return !_.includes(excludeFields, ALL_FIELDS) && (!_.isEmpty(maskFields) || !_.isEmpty(excludeFields)); 168 | } 169 | 170 | function getResponseAudit(req, res, options) { 171 | var headers = res && 'function' === typeof res.getHeaders ? res.getHeaders() : _.get(res, '_headers'); 172 | var elapsed = req && res ? res.timestamp - req.timestamp : 0; 173 | var timestamp = res && res.timestamp ? res.timestamp.toISOString() : NA; 174 | var timestamp_ms = res && res.timestamp ? res.timestamp.valueOf() : NA; 175 | var statusCode = res && res.statusCode ? res.statusCode : NA; 176 | var responseBodyStr = _.get(res, '_bodyStr'); //no need to clone because its not the original body 177 | var responseBodyJson = _.get(res, '_bodyJson'); //no need to clone because its not the original body 178 | 179 | let responseBody; 180 | if (isJsonBody(headers)) { 181 | // Handle JSON only for json responses: 182 | responseBody = handleResponseJson( 183 | responseBodyJson, responseBodyStr, options.logger, options.response.excludeBody, options.response.maskBody); 184 | } else { 185 | responseBody = responseBodyStr; 186 | } 187 | 188 | headers = handleJson(headers, options.logger, options.response.excludeHeaders, options.response.maskHeaders); 189 | 190 | var auditObject = { 191 | status_code: statusCode, 192 | timestamp: timestamp, 193 | timestamp_ms: timestamp_ms, 194 | elapsed: elapsed, 195 | headers: _.isEmpty(headers) ? NA : headers, 196 | body: utils.getBodyStr(responseBody, options.response.maxBodyLength) 197 | }; 198 | 199 | return auditObject; 200 | } 201 | 202 | function isJsonBody(headers) { 203 | return headers && headers['content-type'] && headers['content-type'].includes('application/json') 204 | } 205 | 206 | function getMaskedQuery(query, fieldsToMask) { 207 | if (query) { 208 | return !_.isEmpty(fieldsToMask) ? utils.maskJson(query, fieldsToMask) : query 209 | } else { 210 | return NA; 211 | } 212 | } 213 | 214 | module.exports = { 215 | auditRequest: auditRequest, 216 | auditResponse: auditResponse 217 | }; 218 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | const MASK = 'XXXXX'; 5 | const NA = 'N/A'; 6 | const VALID_LEVELS = ['trace', 'debug', 'info', 'warn', 'error'] 7 | const DEFAULT_LEVEL = 'info'; 8 | 9 | var getUrl = function (req) { 10 | var url = req && req.url || NA; 11 | 12 | return url; 13 | }; 14 | 15 | var getRoute = function (req) { 16 | var url = NA; 17 | 18 | if (req) { 19 | var route = req.baseUrl; 20 | if (req.route && route) { 21 | url = route + req.route.path; 22 | } else if (req.route) { 23 | url = req.route.path; 24 | } 25 | } 26 | 27 | return url; 28 | }; 29 | 30 | function cleanOmitKeys(obj, omitKeys) { 31 | if (obj && !_.isEmpty(omitKeys)) { 32 | Object.keys(obj).forEach(function (key) { 33 | if (_.some(omitKeys, omitKey => key === omitKey)) { 34 | delete obj[key]; 35 | } else { 36 | (obj[key] && typeof obj[key] === 'object') && cleanOmitKeys(obj[key]); 37 | } 38 | }); 39 | } 40 | return obj; 41 | }; 42 | 43 | var shouldAuditURL = function (excludeURLs, req) { 44 | return _.every(excludeURLs, function (path) { 45 | var url = getUrl(req); 46 | var route = getRoute(req); 47 | return !(url.includes(path) || route.includes(path)); 48 | }); 49 | }; 50 | 51 | var maskJson = function (jsonObj, fieldsToMask) { 52 | let jsonObjCopy = _.cloneDeepWith(jsonObj, function (value, key) { 53 | if (_.includes(fieldsToMask, key)) { 54 | return MASK 55 | } 56 | }) 57 | return jsonObjCopy; 58 | }; 59 | 60 | var getLogLevel = function (statusCode, levelsMap) { 61 | let level = DEFAULT_LEVEL; // Default 62 | 63 | if (levelsMap) { 64 | let status = statusCode.toString(); 65 | 66 | if (levelsMap[status]) { 67 | level = VALID_LEVELS.includes(levelsMap[status]) ? levelsMap[status] : level; 68 | } else { 69 | let statusGroup = `${status.substring(0, 1)}xx`; // 5xx, 4xx, 2xx, etc.. 70 | level = VALID_LEVELS.includes(levelsMap[statusGroup]) ? levelsMap[statusGroup] : level; 71 | } 72 | } 73 | 74 | return level; 75 | } 76 | 77 | var getBodyStr = function (body, maxBodyLength) { 78 | if (_.isEmpty(body)) { 79 | return NA 80 | } else { 81 | let bodyStr = (typeof body !== 'string') ? JSON.stringify(body) : body; 82 | let shouldShorten = maxBodyLength && maxBodyLength > 0 && bodyStr.length > maxBodyLength; 83 | return shouldShorten ? bodyStr.substr(0, maxBodyLength) + '...' : bodyStr; 84 | } 85 | } 86 | 87 | 88 | module.exports = { 89 | getRoute: getRoute, 90 | getUrl: getUrl, 91 | shouldAuditURL: shouldAuditURL, 92 | maskJson: maskJson, 93 | cleanOmitKeys: cleanOmitKeys, 94 | getLogLevel: getLogLevel, 95 | getBodyStr: getBodyStr 96 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-requests-logger", 3 | "version": "4.0.3", 4 | "description": "Middleware for logging request/responses in Express apps", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/_mocha -- --recursive ./test/*-test.js", 8 | "test:coverage": "./node_modules/.bin/nyc npm test", 9 | "test:coveralls": "npm run test:coverage && cat ./coverage/lcov.info" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ugolas/express-request-logger.git" 14 | }, 15 | "keywords": [ 16 | "logs", 17 | "requests", 18 | "audit", 19 | "express" 20 | ], 21 | "author": "Zooz", 22 | "license": "Apache-2.0", 23 | "bugs": { 24 | "url": "https://github.com/ugolas/express-request-logger/issues" 25 | }, 26 | "homepage": "https://github.com/ugolas/express-request-logger#readme", 27 | "nyc": { 28 | "check-coverage": true, 29 | "report-dir": "./coverage", 30 | "lines": 97, 31 | "statements": 97, 32 | "functions": 100, 33 | "branches": 96, 34 | "include": [ 35 | "lib" 36 | ], 37 | "reporter": [ 38 | "lcov", 39 | "text" 40 | ], 41 | "cache": true, 42 | "all": true 43 | }, 44 | "devDependencies": { 45 | "coveralls": "^3.1.0", 46 | "mocha": "^7.2.0", 47 | "node-mocks-http": "^1.10.1", 48 | "nyc": "^15.1.0", 49 | "rewire": "^3.0.2", 50 | "should": "^11.1.2", 51 | "sinon": "^1.17.7" 52 | }, 53 | "dependencies": { 54 | "bunyan": "^1.8.15", 55 | "flat": "^5.0.2", 56 | "lodash": "^4.17.21" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/express-logger-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var rewire = require('rewire'), 4 | expressLogger = rewire('../lib/express-logger'), 5 | httpMocks = require('node-mocks-http'), 6 | loggerHelper = require('../lib/logger-helper'), 7 | sinon = require('sinon'), 8 | should = require('should'); 9 | 10 | describe('express-logger tests', function(){ 11 | var sandbox, auditRequestStub, auditResponseStub; 12 | var resEndStub, resWriteStub, resJsonStub; 13 | var req, res, next; 14 | 15 | before(function(){ 16 | sandbox = sinon.sandbox.create(); 17 | next = sandbox.stub(); 18 | }); 19 | 20 | after(function(){ 21 | sandbox.restore(); 22 | }); 23 | 24 | beforeEach(function(){ 25 | auditRequestStub = sandbox.stub(loggerHelper, 'auditRequest'); 26 | auditResponseStub = sandbox.stub(loggerHelper, 'auditResponse'); 27 | 28 | req = httpMocks.createRequest(); 29 | res = httpMocks.createResponse(); 30 | 31 | resEndStub = sandbox.stub(res, 'end'); 32 | resWriteStub = sandbox.stub(res, 'write'); 33 | resJsonStub = sandbox.stub(res, 'json'); 34 | 35 | next = sandbox.stub(); 36 | }); 37 | 38 | afterEach(function(){ 39 | loggerHelper.auditRequest.restore(); 40 | loggerHelper.auditResponse.restore(); 41 | }); 42 | describe('When calling express-logger module', function(){ 43 | it('should correctly setup', function(){ 44 | let options = { 45 | request: { 46 | audit: false, 47 | maskBody: [10], 48 | excludeBody: ['s'], 49 | excludeHeaders: [2], 50 | maskHeaders: [3], 51 | maxBodyLength: 50 52 | }, 53 | response: { 54 | audit: false, 55 | maskBody: [false], 56 | excludeBody: ['t'], 57 | excludeHeaders: ['d'], 58 | maskHeaders: [2], 59 | maxBodyLength: 50 60 | }, 61 | doubleAudit: true, 62 | excludeURLs: ['a'] 63 | }; 64 | 65 | let expectedOptions = { 66 | request: { 67 | audit: false, 68 | maskBody: [10], 69 | maskQuery: [], 70 | excludeBody: ['s'], 71 | excludeHeaders: [2], 72 | maskHeaders: [3], 73 | maxBodyLength: 50 74 | }, 75 | response: { 76 | audit: false, 77 | maskBody: [false], 78 | excludeBody: ['t'], 79 | excludeHeaders: ['d'], 80 | maskHeaders: [2], 81 | maxBodyLength: 50 82 | }, 83 | doubleAudit: true, 84 | excludeURLs: ['a'] 85 | }; 86 | 87 | expressLogger(options); 88 | let convertedOptions = expressLogger.__get__('setupOptions'); 89 | delete convertedOptions.logger; 90 | (convertedOptions).should.containEql(expectedOptions); 91 | 92 | }); 93 | it('should correctly setup in case of wrong options', function(){ 94 | let options = { 95 | request: { 96 | audit: false, 97 | maskBody: 10, 98 | excludeBody: 's', 99 | excludeHeaders: 2, 100 | maskHeaders: 3, 101 | maxBodyLength: -5 102 | }, 103 | response: { 104 | audit: false, 105 | maskBody: false, 106 | excludeBody: 't', 107 | excludeHeaders: 'd', 108 | maskHeaders: 2, 109 | maxBodyLength: 'asd' 110 | }, 111 | doubleAudit: true, 112 | excludeURLs: 'a' 113 | }; 114 | 115 | let expectedOptions = { 116 | request: { 117 | audit: false, 118 | maskBody: [], 119 | maskQuery: [], 120 | excludeBody: [], 121 | excludeHeaders: [], 122 | maskHeaders: [], 123 | maxBodyLength: undefined 124 | }, 125 | response: { 126 | audit: false, 127 | maskBody: [], 128 | excludeBody: [], 129 | excludeHeaders: [], 130 | maskHeaders: [], 131 | maxBodyLength: undefined 132 | }, 133 | doubleAudit: true, 134 | excludeURLs: [] 135 | }; 136 | 137 | try { 138 | expressLogger(options); 139 | should.fail('Expected to throw an error'); 140 | } catch (err){ 141 | should(err.message).eql('Invalid value specified for field: request.maskBody, expected array'); 142 | } 143 | }); 144 | it('should audit response and call next', function(){ 145 | var auditMethod = expressLogger(); 146 | // Start request 147 | auditMethod(req, res, next); 148 | should(auditRequestStub.called).eql(false); 149 | should(next.calledOnce).eql(true); 150 | 151 | // End request 152 | res.end(); 153 | should(resEndStub.called).eql(true); 154 | should(auditResponseStub.called).eql(true); 155 | }); 156 | it('should call auditRequest if options.doubleAudit = true', function(){ 157 | var options = { 158 | doubleAudit: true 159 | }; 160 | 161 | var auditMethod = expressLogger(options); 162 | // Start request 163 | auditMethod(req, res, next); 164 | should(auditRequestStub.called).eql(true); 165 | should(next.calledOnce).eql(true); 166 | 167 | // End request 168 | res.end(); 169 | should(resEndStub.calledOnce).eql(true); 170 | should(auditResponseStub.calledOnce).eql(true); 171 | }); 172 | it('should call auditRequest if options.doubleAudit = true', function(){ 173 | var options = { 174 | doubleAudit: true 175 | }; 176 | 177 | var auditMethod = expressLogger(options); 178 | // Start request 179 | auditMethod(req, res, next); 180 | should(auditRequestStub.called).eql(true); 181 | should(next.calledOnce).eql(true); 182 | 183 | // End request 184 | res.end(); 185 | should(resEndStub.calledOnce).eql(true); 186 | should(auditResponseStub.calledOnce).eql(true); 187 | }); 188 | it('Should add body from write chunks to response', function(){ 189 | var auditMethod = expressLogger(); 190 | // Start request 191 | auditMethod(req, res, next); 192 | should(next.calledOnce).eql(true); 193 | 194 | // Write to res 195 | res.write('chunk'); 196 | should(resWriteStub.calledOnce).eql(true); 197 | 198 | // End request 199 | res.end(); 200 | should(resEndStub.calledOnce).eql(true); 201 | should(auditResponseStub.calledOnce).eql(true); 202 | should(res._bodyStr).eql('chunk'); 203 | }); 204 | it('Should add body from end chunk to response', function(){ 205 | var auditMethod = expressLogger(); 206 | // Start request 207 | auditMethod(req, res, next); 208 | should(next.calledOnce).eql(true); 209 | 210 | // End request 211 | res.end('chunk'); 212 | should(resEndStub.calledOnce).eql(true); 213 | should(auditResponseStub.calledOnce).eql(true); 214 | should(res._bodyStr).eql('chunk'); 215 | }); 216 | it('Should add bodyJson to response', function(){ 217 | var auditMethod = expressLogger(); 218 | // Start request 219 | auditMethod(req, res, next); 220 | should(next.calledOnce).eql(true); 221 | 222 | // End request 223 | res.json({ key: 'value'}); 224 | res.end('chunk'); 225 | should(resEndStub.calledOnce).eql(true); 226 | sinon.assert.calledOnce(resJsonStub); 227 | sinon.assert.calledWith(resJsonStub, {key: 'value'}); 228 | should(auditResponseStub.calledOnce).eql(true); 229 | should(res._bodyStr).eql('chunk'); 230 | should(res._bodyJson).eql({key: 'value'}); 231 | }); 232 | it('Should by default return false in shouldSkipAuditFunc', () => { 233 | expressLogger(); 234 | let convertedOptions = expressLogger.__get__('setupOptions'); 235 | let res = convertedOptions.shouldSkipAuditFunc() 236 | should(res).eql(false); 237 | }) 238 | }); 239 | }); -------------------------------------------------------------------------------- /test/logger-helper-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var httpMocks = require('node-mocks-http'), 4 | loggerHelper = require('../lib/logger-helper'), 5 | _ = require('lodash'), 6 | utils = require('../lib/utils'), 7 | sinon = require('sinon'), 8 | should = require('should'); 9 | 10 | var NA = 'N/A'; 11 | var MASK = 'XXXXX'; 12 | var ALL_FIELDS = '*'; 13 | var method = 'POST'; 14 | var url = 'somepath/123'; 15 | var startTime = new Date(); 16 | var endTime = new Date(); 17 | var elapsed = endTime - startTime; 18 | var body = { 19 | body: 'body' 20 | }; 21 | var params = { 22 | param1: '123' 23 | }; 24 | var query = { 25 | q1: 'something', 26 | q2: 'fishy' 27 | }; 28 | 29 | var expectedUTCTimestamp = '1970-01-01T00:00:00.000Z'; 30 | var expectedMillisTimestamp = 0; 31 | 32 | describe('logger-helpers tests', function () { 33 | var sandbox, clock, loggerInfoStub, shouldAuditURLStub, loggerWarnStub, loggerErrorStub, getLogLevelStub, maskJsonSpy; 34 | var request, response, options, expectedAuditRequest, getExpectedAuditRequest, expectedAuditResponse, getExpectedAuditResponse; 35 | 36 | before(function () { 37 | sandbox = sinon.sandbox.create(); 38 | clock = sinon.useFakeTimers(); 39 | shouldAuditURLStub = sandbox.stub(utils, 'shouldAuditURL'); 40 | getLogLevelStub = sandbox.stub(utils, 'getLogLevel'); 41 | maskJsonSpy = sandbox.spy(utils, 'maskJson'); 42 | }); 43 | 44 | beforeEach(function () { 45 | options = { 46 | request: { 47 | audit: true, 48 | excludeBody: [], 49 | maskBody: [], 50 | excludeHeaders: [] 51 | }, 52 | response: { 53 | audit: true, 54 | maskBody: [], 55 | excludeBody: [], 56 | excludeHeaders: [] 57 | }, 58 | logger: {} 59 | }; 60 | 61 | request = httpMocks.createRequest({ 62 | method: method, 63 | url: url, 64 | route: { 65 | path: '/:id' 66 | }, 67 | baseUrl: '/somepath', 68 | params: params, 69 | query: query, 70 | body: body, 71 | headers: { 72 | 'content-type': 'application/json', 73 | header1: 'some-value' 74 | } 75 | }); 76 | 77 | request.timestamp = startTime; 78 | response = httpMocks.createResponse(); 79 | response._bodyStr = JSON.stringify(body); 80 | response.timestamp = endTime; 81 | response.setHeader('header2', 'some-other-value') 82 | response.setHeader('content-type', 'application/json') 83 | 84 | options.logger.info = function () { }; 85 | options.logger.warn = function () { }; 86 | options.logger.error = function () { }; 87 | 88 | loggerInfoStub = sandbox.stub(options.logger, 'info'); 89 | loggerWarnStub = sandbox.stub(options.logger, 'warn'); 90 | loggerErrorStub = sandbox.stub(options.logger, 'error'); 91 | 92 | getLogLevelStub.returns('info'); 93 | expectedAuditRequest = { 94 | method: method, 95 | url: url, 96 | url_route: '/somepath/:id', 97 | query: query, 98 | headers: { 99 | 'content-type': 'application/json', 100 | header1: 'some-value' 101 | }, 102 | url_params: params, 103 | timestamp: startTime.toISOString(), 104 | timestamp_ms: startTime.valueOf(), 105 | body: JSON.stringify(body), 106 | }; 107 | getExpectedAuditRequest = () => expectedAuditRequest; 108 | expectedAuditResponse = { 109 | status_code: 200, 110 | timestamp: endTime.toISOString(), 111 | timestamp_ms: endTime.valueOf(), 112 | elapsed: elapsed, 113 | body: JSON.stringify(body), 114 | headers: { 115 | header2: 'some-other-value', 116 | 'content-type': 'application/json' 117 | }, 118 | }; 119 | getExpectedAuditResponse = () => expectedAuditResponse; 120 | }); 121 | 122 | afterEach(function () { 123 | sandbox.reset(); 124 | }); 125 | 126 | after(function () { 127 | sandbox.restore(); 128 | clock.restore(); 129 | }); 130 | 131 | describe('When calling auditRequest', function () { 132 | afterEach(function () { 133 | utils.shouldAuditURL.reset(); 134 | }); 135 | describe('And shouldAuditURL returns false', function () { 136 | it('Should not audit request', function () { 137 | shouldAuditURLStub.returns(false); 138 | 139 | loggerHelper.auditRequest(request, options); 140 | sinon.assert.notCalled(loggerInfoStub); 141 | }); 142 | }); 143 | describe('And shouldAuditURL returns true', function () { 144 | it('Should audit request if options.request.audit is true', function () { 145 | shouldAuditURLStub.returns(true); 146 | options.request.audit = true; 147 | loggerHelper.auditRequest(request, options); 148 | sinon.assert.calledOnce(loggerInfoStub); 149 | }); 150 | it('Should not audit request if options.request.audit is false', function () { 151 | shouldAuditURLStub.returns(true); 152 | options.request.audit = false; 153 | loggerHelper.auditRequest(request, options); 154 | sinon.assert.calledOnce(loggerInfoStub); 155 | sinon.assert.calledWith(loggerInfoStub, { 156 | stage: 'start', 157 | request: undefined, 158 | 'utc-timestamp': expectedUTCTimestamp, 159 | 'millis-timestamp': expectedMillisTimestamp 160 | }); 161 | }); 162 | }); 163 | describe('And additionalAudit is not empty', function () { 164 | beforeEach(function () { 165 | request.additionalAudit = { 166 | field1: 'field1', 167 | field2: 'field2' 168 | }; 169 | }); 170 | afterEach(function () { 171 | delete request.additionalAudit; 172 | delete expectedAuditRequest.field1; 173 | delete expectedAuditRequest.field2; 174 | }); 175 | it('Should add to audit the additional audit details', function () { 176 | shouldAuditURLStub.returns(true); 177 | 178 | loggerHelper.auditRequest(request, options); 179 | sinon.assert.calledOnce(loggerInfoStub); 180 | sinon.assert.calledWith(loggerInfoStub, { request: expectedAuditRequest, field1: 'field1', field2: 'field2', 'utc-timestamp': expectedUTCTimestamp, 'millis-timestamp': expectedMillisTimestamp, stage: 'start' }); 181 | }); 182 | 183 | it('Should not add to audit the additional audit details if its an empty object', function () { 184 | request.additionalAudit = {}; 185 | delete expectedAuditRequest.field1; 186 | delete expectedAuditRequest.field2; 187 | 188 | shouldAuditURLStub.returns(true); 189 | 190 | loggerHelper.auditRequest(request, options); 191 | sinon.assert.calledOnce(loggerInfoStub); 192 | sinon.assert.calledWith(loggerInfoStub, { stage: 'start', request: expectedAuditRequest, 'utc-timestamp': expectedUTCTimestamp, 'millis-timestamp': expectedMillisTimestamp }); 193 | }); 194 | }); 195 | describe('When handling non-json body with json content-type', function () { 196 | it('Should issue a warning and set the body to "N/A"', function () { 197 | shouldAuditURLStub.returns(true); 198 | options.request.maskBody = ['test']; 199 | request.body = 'body'; 200 | expectedAuditRequest.body = 'N/A'; 201 | 202 | loggerHelper.auditRequest(request, options); 203 | sinon.assert.calledOnce(loggerWarnStub); 204 | sinon.assert.calledWithMatch(loggerWarnStub, sinon.match.instanceOf(Object), sinon.match('Error parsing json')); 205 | 206 | sinon.assert.calledOnce(loggerInfoStub); 207 | sinon.assert.calledWith(loggerInfoStub, { stage: 'start', request: expectedAuditRequest, 'utc-timestamp': expectedUTCTimestamp, 'millis-timestamp': expectedMillisTimestamp }); 208 | }) 209 | 210 | }) 211 | describe('When handling non-json body', function () { 212 | it('Should not try to mask it and print the body as is', function () { 213 | shouldAuditURLStub.returns(true); 214 | options.request.maskBody = ['test']; 215 | request.body = 'body'; 216 | delete request.headers['content-type']; 217 | delete expectedAuditRequest.headers['content-type']; 218 | expectedAuditRequest.body = 'body'; 219 | 220 | loggerHelper.auditRequest(request, options); 221 | sinon.assert.notCalled(loggerWarnStub); 222 | 223 | sinon.assert.calledOnce(loggerInfoStub); 224 | sinon.assert.calledWith(loggerInfoStub, { stage: 'start', request: expectedAuditRequest, 'utc-timestamp': expectedUTCTimestamp, 'millis-timestamp': expectedMillisTimestamp }); 225 | }) 226 | 227 | }) 228 | describe('And mask query params that are set to be masked', function () { 229 | it('Should mask the query param', function () { 230 | var maskedQuery = 'q1'; 231 | options.request.maskQuery = [maskedQuery]; 232 | shouldAuditURLStub.returns(true); 233 | 234 | loggerHelper.auditRequest(request, options); 235 | sinon.assert.calledOnce(loggerInfoStub); 236 | 237 | let expected = _.cloneDeep(expectedAuditRequest); 238 | expected.query[maskedQuery] = MASK; 239 | sinon.assert.calledWithMatch(loggerInfoStub, { 240 | stage: 'start', 241 | request: expected, 242 | 'utc-timestamp': expectedUTCTimestamp, 243 | 'millis-timestamp': expectedMillisTimestamp 244 | }); 245 | sinon.assert.calledOnce(maskJsonSpy); 246 | 247 | // Clear created header for other tests 248 | }); 249 | it('Should mask all query params', function () { 250 | var maskedQuery1 = 'q1'; 251 | var maskedQuery2 = 'q2'; 252 | options.request.maskQuery = [maskedQuery1, maskedQuery2]; 253 | shouldAuditURLStub.returns(true); 254 | 255 | loggerHelper.auditRequest(request, options); 256 | sinon.assert.calledOnce(loggerInfoStub); 257 | 258 | let expected = _.cloneDeep(expectedAuditRequest); 259 | expected.query[maskedQuery1] = MASK; 260 | expected.query[maskedQuery2] = MASK; 261 | sinon.assert.calledWith(loggerInfoStub, { 262 | stage: 'start', 263 | request: expected, 264 | 'utc-timestamp': expectedUTCTimestamp, 265 | 'millis-timestamp': expectedMillisTimestamp 266 | }); 267 | }); 268 | it('should execute custom mask function for request',function () { 269 | options.request.customMaskBodyFunc = function(request){ 270 | should(request.body).eql({ 271 | body: "body" 272 | }); 273 | return {test:'MASKED'} 274 | }; 275 | 276 | shouldAuditURLStub.returns(true); 277 | 278 | loggerHelper.auditRequest(request, options); 279 | sinon.assert.calledOnce(loggerInfoStub); 280 | 281 | let expected = _.cloneDeep(expectedAuditRequest); 282 | expected.body = '{"test":"MASKED"}'; 283 | sinon.assert.calledWith(loggerInfoStub, { 284 | stage: 'start', 285 | request: expected, 286 | 'utc-timestamp': expectedUTCTimestamp, 287 | 'millis-timestamp': expectedMillisTimestamp 288 | }); 289 | 290 | 291 | }) 292 | }); 293 | describe('And exclude headers contains an header to exclude', function () { 294 | var headerToExclude = 'header-to-exclude'; 295 | beforeEach(function () { 296 | request.headers[headerToExclude] = 'other-value'; 297 | }); 298 | it('Should audit log without the specified header', function () { 299 | options.request.excludeHeaders = [headerToExclude]; 300 | shouldAuditURLStub.returns(true); 301 | let prevHeaders = _.cloneDeep(request.headers); 302 | loggerHelper.auditRequest(request, options); 303 | sinon.assert.calledOnce(loggerInfoStub); 304 | sinon.assert.calledWith(loggerInfoStub, { 305 | stage: 'start', 306 | request: expectedAuditRequest, 307 | 'utc-timestamp': expectedUTCTimestamp, 308 | 'millis-timestamp': expectedMillisTimestamp 309 | }); 310 | should.deepEqual(request.headers, prevHeaders, 'headers of request change'); 311 | }); 312 | it('Should audit log without the specified headers, if there are more than one', function () { 313 | var anotherHeaderToExclude = 'another'; 314 | options.request.excludeHeaders = [headerToExclude, anotherHeaderToExclude]; 315 | request.headers[anotherHeaderToExclude] = 'some value'; 316 | shouldAuditURLStub.returns(true); 317 | let prevHeaders = _.cloneDeep(request.headers); 318 | loggerHelper.auditRequest(request, options); 319 | sinon.assert.calledOnce(loggerInfoStub); 320 | sinon.assert.calledWith(loggerInfoStub, { 321 | stage: 'start', 322 | request: expectedAuditRequest, 323 | 'utc-timestamp': expectedUTCTimestamp, 324 | 'millis-timestamp': expectedMillisTimestamp 325 | }); 326 | should.deepEqual(request.headers, prevHeaders, 'headers of request change'); 327 | 328 | }); 329 | it('Should audit log with all headers, if exclude headers is an empty list', function () { 330 | options.request.excludeHeaders = ['other-header']; 331 | shouldAuditURLStub.returns(true); 332 | let prevHeaders = _.cloneDeep(request.headers); 333 | loggerHelper.auditRequest(request, options); 334 | sinon.assert.calledOnce(loggerInfoStub); 335 | 336 | expectedAuditRequest.headers[headerToExclude] = 'other-value'; 337 | sinon.assert.calledWith(loggerInfoStub, { 338 | stage: 'start', 339 | request: expectedAuditRequest, 340 | 'utc-timestamp': expectedUTCTimestamp, 341 | 'millis-timestamp': expectedMillisTimestamp 342 | }); 343 | should.deepEqual(request.headers, prevHeaders, 'headers of request change'); 344 | // Clear created header for other tests 345 | delete expectedAuditRequest.headers[headerToExclude]; 346 | }); 347 | }); 348 | describe('And exclude Body contains field to exclude', function () { 349 | before(function () { 350 | shouldAuditURLStub.returns(true); 351 | }); 352 | 353 | afterEach(function () { 354 | expectedAuditRequest.body = JSON.stringify(body); 355 | }); 356 | it('Should audit log with body, if no excludeBody was written in options', function () { 357 | loggerHelper.auditRequest(request, options); 358 | sinon.assert.calledOnce(loggerInfoStub); 359 | sinon.assert.calledWith(loggerInfoStub, { 360 | stage: 'start', 361 | request: expectedAuditRequest, 362 | 'utc-timestamp': expectedUTCTimestamp, 363 | 'millis-timestamp': expectedMillisTimestamp 364 | }); 365 | }); 366 | it('Should audit log without body, when excludeBody with \'*\'', function () { 367 | options.request.excludeBody = [ALL_FIELDS]; 368 | let prevBody = _.cloneDeep(request.body); 369 | loggerHelper.auditRequest(request, options); 370 | sinon.assert.calledOnce(loggerInfoStub); 371 | expectedAuditRequest.body = NA; 372 | sinon.assert.calledWith(loggerInfoStub, { 373 | stage: 'start', 374 | request: expectedAuditRequest, 375 | 'utc-timestamp': expectedUTCTimestamp, 376 | 'millis-timestamp': expectedMillisTimestamp 377 | }); 378 | should.deepEqual(request.body, prevBody, 'body of request change'); 379 | 380 | }); 381 | it('Should audit log without body, when excludeBody with \'*\' and body is plain text', function () { 382 | options.request.excludeBody = [ALL_FIELDS]; 383 | request.body = 'test'; 384 | 385 | loggerHelper.auditRequest(request, options); 386 | sinon.assert.calledOnce(loggerInfoStub); 387 | expectedAuditRequest.body = NA; 388 | sinon.assert.calledWith(loggerInfoStub, { 389 | stage: 'start', 390 | request: expectedAuditRequest, 391 | 'utc-timestamp': expectedUTCTimestamp, 392 | 'millis-timestamp': expectedMillisTimestamp 393 | }); 394 | }); 395 | it('Should audit log without body, when excludeBody by field and all body', function () { 396 | options.request.excludeBody = ['field1', ALL_FIELDS]; 397 | request.body = { 'field1': 1, 'field2': 'test' }; 398 | loggerHelper.auditRequest(request, options); 399 | sinon.assert.calledOnce(loggerInfoStub); 400 | expectedAuditRequest.body = NA; 401 | sinon.assert.calledWith(loggerInfoStub, { 402 | stage: 'start', 403 | request: expectedAuditRequest, 404 | 'utc-timestamp': expectedUTCTimestamp, 405 | 'millis-timestamp': expectedMillisTimestamp 406 | }); 407 | }); 408 | it('Should audit log body without specific field, when excludeBody by existing and unexisting field', function () { 409 | options.request.excludeBody = ['field3', 'field1']; 410 | request.body = { 'field1': 1, 'field2': 'test' }; 411 | let prevBody = _.cloneDeep(request.body); 412 | loggerHelper.auditRequest(request, options); 413 | sinon.assert.calledOnce(loggerInfoStub); 414 | expectedAuditRequest.body = JSON.stringify({ 'field2': 'test' }); 415 | sinon.assert.calledWith(loggerInfoStub, { 416 | stage: 'start', 417 | request: expectedAuditRequest, 418 | 'utc-timestamp': expectedUTCTimestamp, 419 | 'millis-timestamp': expectedMillisTimestamp 420 | }); 421 | should.deepEqual(request.body, prevBody, 'body of request change'); 422 | }); 423 | it('Should audit log without body, when no body in request and excludeBody by field', function () { 424 | options.request.excludeBody = ['field3', 'field1']; 425 | delete request.body; 426 | loggerHelper.auditRequest(request, options); 427 | sinon.assert.calledOnce(loggerInfoStub); 428 | expectedAuditRequest.body = NA; 429 | sinon.assert.calledWith(loggerInfoStub, { 430 | stage: 'start', 431 | request: expectedAuditRequest, 432 | 'utc-timestamp': expectedUTCTimestamp, 433 | 'millis-timestamp': expectedMillisTimestamp 434 | }); 435 | }); 436 | 437 | it('Should audit log without body, when body is number (not json)', function () { 438 | options.request.excludeBody = ['field3', 'field1']; 439 | request.body = 3; 440 | let prevBody = _.cloneDeep(request.body); 441 | loggerHelper.auditRequest(request, options); 442 | sinon.assert.calledOnce(loggerInfoStub); 443 | sinon.assert.calledOnce(loggerWarnStub); 444 | expectedAuditRequest.body = NA; 445 | sinon.assert.calledWith(loggerInfoStub, { 446 | stage: 'start', 447 | request: expectedAuditRequest, 448 | 'utc-timestamp': expectedUTCTimestamp, 449 | 'millis-timestamp': expectedMillisTimestamp 450 | }); 451 | should.deepEqual(request.body, prevBody, 'body of request change'); 452 | }); 453 | 454 | it('Should audit log without body, when body is string (not json)', function () { 455 | options.request.excludeBody = ['field3', 'field1']; 456 | request.body = 'test'; 457 | let prevBody = _.cloneDeep(request.body); 458 | loggerHelper.auditRequest(request, options); 459 | sinon.assert.calledOnce(loggerInfoStub); 460 | sinon.assert.calledOnce(loggerWarnStub); 461 | expectedAuditRequest.body = NA; 462 | sinon.assert.calledWith(loggerInfoStub, { 463 | stage: 'start', 464 | request: expectedAuditRequest, 465 | 'utc-timestamp': expectedUTCTimestamp, 466 | 'millis-timestamp': expectedMillisTimestamp 467 | }); 468 | should.deepEqual(request.body, prevBody, 'body of request change'); 469 | }); 470 | 471 | it('Should audit log without body, when body is json array', function () { 472 | options.request.excludeBody = ['field3', 'field1']; 473 | let newBody = ['a', 'b', 'c']; 474 | request.body = _.cloneDeep(newBody); 475 | expectedAuditRequest.body = JSON.stringify(newBody); 476 | let prevBody = _.cloneDeep(request.body); 477 | loggerHelper.auditRequest(request, options); 478 | sinon.assert.calledOnce(loggerInfoStub); 479 | sinon.assert.notCalled(loggerWarnStub); 480 | sinon.assert.calledWith(loggerInfoStub, { 481 | stage: 'start', 482 | request: expectedAuditRequest, 483 | 'utc-timestamp': expectedUTCTimestamp, 484 | 'millis-timestamp': expectedMillisTimestamp 485 | }); 486 | should.deepEqual(request.body, prevBody, 'body of request change'); 487 | }); 488 | }); 489 | }); 490 | 491 | describe('When calling auditResponse', function () { 492 | afterEach(function () { 493 | utils.shouldAuditURL.reset(); 494 | }); 495 | describe('And shouldAuditURL returns false', function () { 496 | it('Should not audit request/response', function () { 497 | shouldAuditURLStub.returns(false); 498 | 499 | loggerHelper.auditResponse(request, response, options); 500 | sinon.assert.notCalled(loggerInfoStub); 501 | }); 502 | }); 503 | describe('And shouldSkipAuditFunc returns true', () => { 504 | it('Should not audit the request', () => { 505 | shouldAuditURLStub.returns(true); 506 | options.shouldSkipAuditFunc =(req, res) => { 507 | return true; 508 | } 509 | 510 | loggerHelper.auditResponse(request, response, options); 511 | sinon.assert.notCalled(loggerInfoStub); 512 | }) 513 | it('Should pass req and res objects to the func', () => { 514 | shouldAuditURLStub.returns(true); 515 | options.shouldSkipAuditFunc =(req, res) => { 516 | should(req).eql(request); 517 | should(res).eql(response); 518 | return true; 519 | } 520 | loggerHelper.auditResponse(request, response, options); 521 | sinon.assert.notCalled(loggerInfoStub); 522 | }) 523 | }) 524 | describe('And shouldSkipAuditFunc returns false', () => { 525 | it('Should audit the request', () => { 526 | shouldAuditURLStub.returns(true); 527 | options.shouldSkipAuditFunc =(req, res) => { 528 | return false; 529 | } 530 | 531 | loggerHelper.auditResponse(request, response, options); 532 | sinon.assert.calledOnce(loggerInfoStub); 533 | }) 534 | it('Should pass req and res objects to the func', () => { 535 | shouldAuditURLStub.returns(true); 536 | options.shouldSkipAuditFunc =(req, res) => { 537 | should(req).eql(request); 538 | should(res).eql(response); 539 | return false; 540 | } 541 | loggerHelper.auditResponse(request, response, options); 542 | sinon.assert.calledOnce(loggerInfoStub); 543 | }) 544 | }) 545 | describe('And shouldAuditURL returns true', function () { 546 | it('Should audit request if options.request.audit is true', function () { 547 | shouldAuditURLStub.returns(true); 548 | options.request.audit = true; 549 | clock.tick(elapsed); 550 | loggerHelper.auditResponse(request, response, options); 551 | sinon.assert.calledOnce(loggerInfoStub); 552 | sinon.assert.calledWith(loggerInfoStub, { 553 | stage: 'end', 554 | request: expectedAuditRequest, 555 | response: expectedAuditResponse, 556 | 'utc-timestamp': expectedUTCTimestamp, 557 | 'millis-timestamp': expectedMillisTimestamp 558 | }); 559 | sinon.assert.notCalled(maskJsonSpy); 560 | }); 561 | it('Should use _bodyStr if masking is not used', function () { 562 | shouldAuditURLStub.returns(true); 563 | options.request.audit = true; 564 | let differentJsonBody = Object.assign({ key: 'value' }, body); 565 | response.json(differentJsonBody); 566 | clock.tick(elapsed); 567 | loggerHelper.auditResponse(request, response, options); 568 | sinon.assert.calledOnce(loggerInfoStub); 569 | sinon.assert.calledWith(loggerInfoStub, { 570 | stage: 'end', 571 | request: expectedAuditRequest, 572 | response: expectedAuditResponse, 573 | 'utc-timestamp': expectedUTCTimestamp, 574 | 'millis-timestamp': expectedMillisTimestamp 575 | }); 576 | sinon.assert.notCalled(maskJsonSpy); 577 | }); 578 | it('Should use _bodyJson if masking is used', function () { 579 | shouldAuditURLStub.returns(true); 580 | options.request.audit = true; 581 | options.response.excludeBody = ['someFile']; 582 | let differentJsonBody = Object.assign({ key: 'value' }, body); 583 | response._bodyJson = differentJsonBody; 584 | let expectedMaskedAuditResponse = _.cloneDeep(expectedAuditResponse); 585 | expectedMaskedAuditResponse.body = JSON.stringify(differentJsonBody) 586 | clock.tick(elapsed); 587 | loggerHelper.auditResponse(request, response, options); 588 | sinon.assert.calledOnce(loggerInfoStub); 589 | sinon.assert.calledWith(loggerInfoStub, { 590 | stage: 'end', 591 | request: expectedAuditRequest, 592 | response: expectedMaskedAuditResponse, 593 | 'utc-timestamp': expectedUTCTimestamp, 594 | 'millis-timestamp': expectedMillisTimestamp 595 | }); 596 | sinon.assert.calledOnce(maskJsonSpy); 597 | }); 598 | it('Should shorten response body if options.response.maxBodyLength < response body length', function () { 599 | shouldAuditURLStub.returns(true); 600 | options.request.audit = true; 601 | options.response.maxBodyLength = 5; 602 | clock.tick(elapsed); 603 | const expectedAuditResponse = getExpectedAuditResponse(); 604 | expectedAuditResponse.body = '{"bod...'; 605 | 606 | loggerHelper.auditResponse(request, response, options); 607 | sinon.assert.calledOnce(loggerInfoStub); 608 | sinon.assert.calledWith(loggerInfoStub, { 609 | stage: 'end', 610 | request: expectedAuditRequest, 611 | response: expectedAuditResponse, 612 | 'utc-timestamp': expectedUTCTimestamp, 613 | 'millis-timestamp': expectedMillisTimestamp 614 | 615 | }); 616 | }); 617 | it('Should audit request if options.request.audit is true and options.response.maxBodyLength is not a positive integer', function () { 618 | shouldAuditURLStub.returns(true); 619 | options.request.audit = true; 620 | options.response.maxBodyLength = -5; 621 | clock.tick(elapsed); 622 | loggerHelper.auditResponse(request, response, options); 623 | sinon.assert.calledOnce(loggerInfoStub); 624 | sinon.assert.calledWith(loggerInfoStub, { 625 | stage: 'end', 626 | request: expectedAuditRequest, 627 | response: expectedAuditResponse, 628 | 'utc-timestamp': expectedUTCTimestamp, 629 | 'millis-timestamp': expectedMillisTimestamp 630 | 631 | }); 632 | }); 633 | it('Should not shorten response body if options.response.maxBodyLength > response body length', function () { 634 | shouldAuditURLStub.returns(true); 635 | options.request.audit = true; 636 | options.response.maxBodyLength = 500000000; 637 | clock.tick(elapsed); 638 | loggerHelper.auditResponse(request, response, options); 639 | sinon.assert.calledOnce(loggerInfoStub); 640 | sinon.assert.calledWith(loggerInfoStub, { 641 | stage: 'end', 642 | request: expectedAuditRequest, 643 | response: expectedAuditResponse, 644 | 'utc-timestamp': expectedUTCTimestamp, 645 | 'millis-timestamp': expectedMillisTimestamp 646 | 647 | }); 648 | }); 649 | it('Should not shorten request body if options.request.maxBodyLength > request body length', function () { 650 | shouldAuditURLStub.returns(true); 651 | options.request.audit = true; 652 | options.response.maxBodyLength = 500000; 653 | clock.tick(elapsed); 654 | loggerHelper.auditResponse(request, response, options); 655 | sinon.assert.calledOnce(loggerInfoStub); 656 | sinon.assert.calledWith(loggerInfoStub, { 657 | stage: 'end', 658 | request: expectedAuditRequest, 659 | response: expectedAuditResponse, 660 | 'utc-timestamp': expectedUTCTimestamp, 661 | 'millis-timestamp': expectedMillisTimestamp 662 | 663 | }); 664 | }); 665 | it('Should shorten request body if options.request.maxBodyLength < request body length', function () { 666 | shouldAuditURLStub.returns(true); 667 | options.request.audit = true; 668 | options.request.maxBodyLength = 5; 669 | clock.tick(elapsed); 670 | const expectedAuditRequest = getExpectedAuditRequest(); 671 | expectedAuditRequest.body = '{"bod...'; 672 | 673 | loggerHelper.auditResponse(request, response, options); 674 | sinon.assert.calledOnce(loggerInfoStub); 675 | sinon.assert.calledWith(loggerInfoStub, { 676 | stage: 'end', 677 | request: expectedAuditRequest, 678 | response: expectedAuditResponse, 679 | 'utc-timestamp': expectedUTCTimestamp, 680 | 'millis-timestamp': expectedMillisTimestamp 681 | 682 | }); 683 | }); 684 | it('Should log as error if getLogLevel returns error', () => { 685 | getLogLevelStub.returns('error'); 686 | shouldAuditURLStub.returns(true); 687 | loggerHelper.auditResponse(request, response, options); 688 | 689 | sinon.assert.calledOnce(loggerErrorStub); 690 | sinon.assert.calledWith(loggerErrorStub, { 691 | stage: 'end', 692 | request: expectedAuditRequest, 693 | response: expectedAuditResponse, 694 | 'utc-timestamp': expectedUTCTimestamp, 695 | 'millis-timestamp': expectedMillisTimestamp 696 | }); 697 | }) 698 | it('Should log as info if getLogLevel returns garbage', () => { 699 | getLogLevelStub.returns('garbage'); 700 | shouldAuditURLStub.returns(true); 701 | loggerHelper.auditResponse(request, response, options); 702 | 703 | sinon.assert.calledOnce(loggerInfoStub); 704 | sinon.assert.calledWith(loggerInfoStub, { 705 | stage: 'end', 706 | request: expectedAuditRequest, 707 | response: expectedAuditResponse, 708 | 'utc-timestamp': expectedUTCTimestamp, 709 | 'millis-timestamp': expectedMillisTimestamp 710 | }); 711 | }) 712 | it('Should log as info if getLogLevel returns undefined', () => { 713 | getLogLevelStub.returns(undefined); 714 | shouldAuditURLStub.returns(true); 715 | loggerHelper.auditResponse(request, response, options); 716 | 717 | sinon.assert.calledOnce(loggerInfoStub); 718 | sinon.assert.calledWith(loggerInfoStub, { 719 | stage: 'end', 720 | request: expectedAuditRequest, 721 | response: expectedAuditResponse, 722 | 'utc-timestamp': expectedUTCTimestamp, 723 | 'millis-timestamp': expectedMillisTimestamp 724 | }); 725 | }) 726 | it('Should audit request if options.request.audit is true', function () { 727 | shouldAuditURLStub.returns(true); 728 | options.request.audit = true; 729 | clock.tick(elapsed); 730 | loggerHelper.auditResponse(request, response, options); 731 | sinon.assert.calledOnce(loggerInfoStub); 732 | sinon.assert.calledWith(loggerInfoStub, { 733 | stage: 'end', 734 | request: expectedAuditRequest, 735 | response: expectedAuditResponse, 736 | 'utc-timestamp': expectedUTCTimestamp, 737 | 'millis-timestamp': expectedMillisTimestamp 738 | }); 739 | }); 740 | it('Should not audit request if options.request.audit is false', function () { 741 | shouldAuditURLStub.returns(true); 742 | options.request.audit = false; 743 | options.request.maxBodyLength = 50; 744 | clock.tick(elapsed); 745 | loggerHelper.auditResponse(request, response, options); 746 | sinon.assert.calledOnce(loggerInfoStub); 747 | sinon.assert.calledWith(loggerInfoStub, { 748 | stage: 'end', 749 | request: undefined, 750 | response: expectedAuditResponse, 751 | 'utc-timestamp': expectedUTCTimestamp, 752 | 'millis-timestamp': expectedMillisTimestamp 753 | }); 754 | }); 755 | it('Should audit response if options.response.audit is true', function () { 756 | shouldAuditURLStub.returns(true); 757 | options.response.audit = true; 758 | clock.tick(elapsed); 759 | loggerHelper.auditResponse(request, response, options); 760 | sinon.assert.calledOnce(loggerInfoStub); 761 | sinon.assert.calledWith(loggerInfoStub, { 762 | stage: 'end', 763 | request: expectedAuditRequest, 764 | response: expectedAuditResponse, 765 | 'utc-timestamp': expectedUTCTimestamp, 766 | 'millis-timestamp': expectedMillisTimestamp 767 | }); 768 | }); 769 | it('Should not audit response if options.response.audit is false', function () { 770 | shouldAuditURLStub.returns(true); 771 | options.response.audit = false; 772 | options.response.maxBodyLength = 50; 773 | clock.tick(elapsed); 774 | loggerHelper.auditResponse(request, response, options); 775 | sinon.assert.calledOnce(loggerInfoStub); 776 | sinon.assert.calledWith(loggerInfoStub, { 777 | stage: 'end', 778 | request: expectedAuditRequest, 779 | response: undefined, 780 | 'utc-timestamp': expectedUTCTimestamp, 781 | 'millis-timestamp': expectedMillisTimestamp 782 | }); 783 | }); 784 | it('Should log empty values as N/A', function () { 785 | request = undefined; 786 | response = undefined; 787 | 788 | shouldAuditURLStub.returns(true); 789 | clock.tick(elapsed); 790 | loggerHelper.auditResponse(request, response, options); 791 | sinon.assert.calledOnce(loggerInfoStub); 792 | sinon.assert.calledWith(loggerInfoStub, { 793 | stage: 'end', 794 | request: { 795 | method: NA, 796 | url: NA, 797 | url_route: NA, 798 | query: NA, 799 | url_params: NA, 800 | headers: NA, 801 | timestamp: NA, 802 | timestamp_ms: NA, 803 | body: NA 804 | }, 805 | response: { 806 | status_code: NA, 807 | timestamp: NA, 808 | timestamp_ms: NA, 809 | elapsed: 0, 810 | headers: NA, 811 | body: NA 812 | }, 813 | 'utc-timestamp': expectedUTCTimestamp, 814 | 'millis-timestamp': expectedMillisTimestamp 815 | }); 816 | }); 817 | }); 818 | describe('And exclude Body contains field to exclude', function () { 819 | before(function () { 820 | shouldAuditURLStub.returns(true); 821 | }); 822 | 823 | afterEach(function () { 824 | expectedAuditResponse.body = JSON.stringify(body); 825 | }); 826 | it('Should audit log with body, if no excludeBody was written in options', function () { 827 | loggerHelper.auditResponse(request, response, options); 828 | sinon.assert.calledOnce(loggerInfoStub); 829 | sinon.assert.calledWith(loggerInfoStub, { 830 | stage: 'end', 831 | request: expectedAuditRequest, 832 | response: expectedAuditResponse, 833 | 'utc-timestamp': expectedUTCTimestamp, 834 | 'millis-timestamp': expectedMillisTimestamp 835 | }); 836 | }); 837 | it('Should audit log without body, when excludeBody with \'*\'', function () { 838 | options.response.excludeBody = [ALL_FIELDS]; 839 | let prevBody = _.cloneDeep(response.body); 840 | loggerHelper.auditResponse(request, response, options); 841 | sinon.assert.calledOnce(loggerInfoStub); 842 | expectedAuditResponse.body = NA; 843 | sinon.assert.calledWith(loggerInfoStub, { 844 | stage: 'end', 845 | request: expectedAuditRequest, 846 | response: expectedAuditResponse, 847 | 'utc-timestamp': expectedUTCTimestamp, 848 | 'millis-timestamp': expectedMillisTimestamp 849 | }); 850 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 851 | }); 852 | it('Should audit log without body, when excludeBody with \'*\' and body is plain text', function () { 853 | options.response.excludeBody = [ALL_FIELDS]; 854 | response.body = 'test'; 855 | let prevBody = _.cloneDeep(response.body); 856 | loggerHelper.auditResponse(request, response, options); 857 | sinon.assert.calledOnce(loggerInfoStub); 858 | expectedAuditResponse.body = NA; 859 | sinon.assert.calledWith(loggerInfoStub, { 860 | stage: 'end', 861 | request: expectedAuditRequest, 862 | response: expectedAuditResponse, 863 | 'utc-timestamp': expectedUTCTimestamp, 864 | 'millis-timestamp': expectedMillisTimestamp 865 | }); 866 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 867 | }); 868 | it('Should audit log without body, when excludeBody by field and all body', function () { 869 | options.response.excludeBody = ['field1', ALL_FIELDS]; 870 | response._bodyStr = JSON.stringify({ 'field1': 1, 'field2': 'test' }); 871 | let prevBody = _.cloneDeep(response.body); 872 | loggerHelper.auditResponse(request, response, options); 873 | sinon.assert.calledOnce(loggerInfoStub); 874 | expectedAuditResponse.body = NA; 875 | sinon.assert.calledWith(loggerInfoStub, { 876 | stage: 'end', 877 | request: expectedAuditRequest, 878 | response: expectedAuditResponse, 879 | 'utc-timestamp': expectedUTCTimestamp, 880 | 'millis-timestamp': expectedMillisTimestamp 881 | }); 882 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 883 | }); 884 | it('Should audit log body without specific field, when excludeBody by existing and unexisting field', function () { 885 | options.response.excludeBody = ['field3', 'field1']; 886 | response._bodyStr = JSON.stringify({ 'field1': 1, 'field2': 'test' }); 887 | let prevBody = _.cloneDeep(response.body); 888 | loggerHelper.auditResponse(request, response, options); 889 | sinon.assert.calledOnce(loggerInfoStub); 890 | expectedAuditResponse.body = JSON.stringify({ 'field2': 'test' }); 891 | sinon.assert.calledWith(loggerInfoStub, { 892 | stage: 'end', 893 | request: expectedAuditRequest, 894 | response: expectedAuditResponse, 895 | 'utc-timestamp': expectedUTCTimestamp, 896 | 'millis-timestamp': expectedMillisTimestamp 897 | }); 898 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 899 | }); 900 | it('Should audit log without body, when no body in response and excludeBody by field', function () { 901 | options.response.excludeBody = ['field3', 'field1']; 902 | delete response._bodyStr; 903 | let prevBody = _.cloneDeep(response.body); 904 | loggerHelper.auditResponse(request, response, options); 905 | sinon.assert.calledOnce(loggerInfoStub); 906 | expectedAuditResponse.body = NA; 907 | sinon.assert.calledWith(loggerInfoStub, { 908 | stage: 'end', 909 | request: expectedAuditRequest, 910 | response: expectedAuditResponse, 911 | 'utc-timestamp': expectedUTCTimestamp, 912 | 'millis-timestamp': expectedMillisTimestamp 913 | }); 914 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 915 | }); 916 | }); 917 | describe('And exclude headers contains an header to exclude', function () { 918 | var headerToExclude = 'header-to-exclude'; 919 | before(() => { 920 | shouldAuditURLStub.returns(true); 921 | }); 922 | 923 | beforeEach(function () { 924 | response.setHeader(headerToExclude, 'other-value'); 925 | }); 926 | 927 | it('Should audit log without the specified header', function () { 928 | options.response.excludeHeaders = [headerToExclude]; 929 | let prevHeaders = _.cloneDeep(response.getHeaders()); 930 | loggerHelper.auditResponse(request, response, options); 931 | sinon.assert.calledOnce(loggerInfoStub); 932 | sinon.assert.calledWith(loggerInfoStub, { 933 | stage: 'end', 934 | request: expectedAuditRequest, 935 | response: expectedAuditResponse, 936 | 'utc-timestamp': expectedUTCTimestamp, 937 | 'millis-timestamp': expectedMillisTimestamp 938 | }); 939 | 940 | should.deepEqual(response.getHeaders(), prevHeaders, 'headers of response change'); 941 | }); 942 | it('Should audit log without all headers', function () { 943 | options.response.excludeHeaders = [ALL_FIELDS]; 944 | let prevHeaders = _.cloneDeep(response.getHeaders()); 945 | loggerHelper.auditResponse(request, response, options); 946 | expectedAuditResponse.headers = NA; 947 | sinon.assert.calledOnce(loggerInfoStub); 948 | sinon.assert.calledWith(loggerInfoStub, { 949 | stage: 'end', 950 | request: expectedAuditRequest, 951 | response: expectedAuditResponse, 952 | 'utc-timestamp': expectedUTCTimestamp, 953 | 'millis-timestamp': expectedMillisTimestamp 954 | }); 955 | 956 | should.deepEqual(response.getHeaders(), prevHeaders, 'headers of response change'); 957 | }); 958 | it('Should audit log without the specified headers, if there are more than one', function () { 959 | var anotherHeaderToExclude = 'another'; 960 | options.response.excludeHeaders = [headerToExclude, anotherHeaderToExclude]; 961 | response.setHeader(anotherHeaderToExclude, 'some value'); 962 | 963 | let prevHeaders = _.cloneDeep(response.getHeaders()); 964 | loggerHelper.auditResponse(request, response, options); 965 | sinon.assert.calledOnce(loggerInfoStub); 966 | sinon.assert.calledWith(loggerInfoStub, { 967 | stage: 'end', 968 | request: expectedAuditRequest, 969 | response: expectedAuditResponse, 970 | 'utc-timestamp': expectedUTCTimestamp, 971 | 'millis-timestamp': expectedMillisTimestamp 972 | }); 973 | 974 | should.deepEqual(response.getHeaders(), prevHeaders, 'headers of response change'); 975 | }); 976 | it('Should audit log with all headers, if exclude headers is an empty list', function () { 977 | options.response.excludeHeaders = ['other-header']; 978 | 979 | loggerHelper.auditResponse(request, response, options); 980 | sinon.assert.calledOnce(loggerInfoStub); 981 | 982 | expectedAuditResponse.headers[headerToExclude] = 'other-value'; 983 | sinon.assert.calledWith(loggerInfoStub, { 984 | stage: 'end', 985 | request: expectedAuditRequest, 986 | response: expectedAuditResponse, 987 | 'utc-timestamp': expectedUTCTimestamp, 988 | 'millis-timestamp': expectedMillisTimestamp 989 | }); 990 | // Clear created header for other tests 991 | delete expectedAuditResponse.headers[headerToExclude]; 992 | }); 993 | }); 994 | describe('And mask Body', function () { 995 | before(function () { 996 | shouldAuditURLStub.returns(true); 997 | }); 998 | 999 | afterEach(function () { 1000 | expectedAuditResponse.body = JSON.stringify(body); 1001 | }); 1002 | it('Should audit log with body, if mask body specific field', function () { 1003 | options.response.maskBody = ['test1']; 1004 | let newBody = { 1005 | body: 'body', 1006 | test1: 'test2' 1007 | }; 1008 | response._bodyStr = _.cloneDeep(newBody); 1009 | let prevBody = _.cloneDeep(response.body); 1010 | loggerHelper.auditResponse(request, response, options); 1011 | sinon.assert.calledOnce(loggerInfoStub); 1012 | newBody.test1 = MASK; 1013 | expectedAuditResponse.body = JSON.stringify(newBody); 1014 | sinon.assert.calledWith(loggerInfoStub, { 1015 | stage: 'end', 1016 | request: expectedAuditRequest, 1017 | response: expectedAuditResponse, 1018 | 'utc-timestamp': expectedUTCTimestamp, 1019 | 'millis-timestamp': expectedMillisTimestamp 1020 | }); 1021 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 1022 | sinon.assert.calledOnce(maskJsonSpy); 1023 | }); 1024 | it('Should not mask body if response content type is not json', () => { 1025 | let testContentType = 'text/xml'; 1026 | options.response.maskBody = ['test1']; 1027 | let newBody = { 1028 | body: 'body', 1029 | test1: 'test2' 1030 | }; 1031 | response.setHeader('content-type', testContentType) 1032 | response._bodyStr = _.cloneDeep(newBody); 1033 | let prevBody = _.cloneDeep(response.body); 1034 | 1035 | loggerHelper.auditResponse(request, response, options); 1036 | 1037 | expectedAuditResponse.body = JSON.stringify(newBody); 1038 | expectedAuditResponse.headers['content-type'] = testContentType; 1039 | 1040 | sinon.assert.calledWith(loggerInfoStub, { 1041 | stage: 'end', 1042 | request: expectedAuditRequest, 1043 | response: expectedAuditResponse, 1044 | 'utc-timestamp': expectedUTCTimestamp, 1045 | 'millis-timestamp': expectedMillisTimestamp 1046 | }); 1047 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 1048 | sinon.assert.notCalled(maskJsonSpy); 1049 | }); 1050 | it('Should not mask body if no headers', () => { 1051 | options.response.maskBody = ['test1']; 1052 | let newBody = { 1053 | body: 'body', 1054 | test1: 'test2' 1055 | }; 1056 | 1057 | Object.keys(response.getHeaders()) 1058 | .map(headerName => response.removeHeader(headerName)); 1059 | 1060 | response._bodyStr = _.cloneDeep(newBody); 1061 | let prevBody = _.cloneDeep(response.body); 1062 | 1063 | loggerHelper.auditResponse(request, response, options); 1064 | 1065 | expectedAuditResponse.body = JSON.stringify(newBody); 1066 | 1067 | expectedAuditResponse.headers = NA; 1068 | 1069 | sinon.assert.calledWith(loggerInfoStub, { 1070 | stage: 'end', 1071 | request: expectedAuditRequest, 1072 | response: expectedAuditResponse, 1073 | 'utc-timestamp': expectedUTCTimestamp, 1074 | 'millis-timestamp': expectedMillisTimestamp 1075 | }); 1076 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 1077 | sinon.assert.notCalled(maskJsonSpy); 1078 | }); 1079 | it('Should not mask body if no content type header', () => { 1080 | options.response.maskBody = ['test1']; 1081 | let newBody = { 1082 | body: 'body', 1083 | test1: 'test2' 1084 | }; 1085 | response.removeHeader('content-type') ; 1086 | response._bodyStr = _.cloneDeep(newBody); 1087 | let prevBody = _.cloneDeep(response.body); 1088 | 1089 | loggerHelper.auditResponse(request, response, options); 1090 | 1091 | expectedAuditResponse.body = JSON.stringify(newBody); 1092 | delete expectedAuditResponse.headers['content-type']; 1093 | 1094 | sinon.assert.calledWith(loggerInfoStub, { 1095 | stage: 'end', 1096 | request: expectedAuditRequest, 1097 | response: expectedAuditResponse, 1098 | 'utc-timestamp': expectedUTCTimestamp, 1099 | 'millis-timestamp': expectedMillisTimestamp 1100 | }); 1101 | should.deepEqual(response.body, prevBody, 'body of resopnse change'); 1102 | sinon.assert.notCalled(maskJsonSpy); 1103 | }); 1104 | }); 1105 | }); 1106 | }); 1107 | -------------------------------------------------------------------------------- /test/utils-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var httpMocks = require('node-mocks-http'), 4 | utils = require('../lib/utils'), 5 | should = require('should'); 6 | 7 | describe('utils tests', function () { 8 | describe('When calling getUrl', function () { 9 | var request; 10 | var expectedPath = 'path/123'; 11 | beforeEach(function () { 12 | request = httpMocks.createRequest({ 13 | method: 'POST', 14 | url: expectedPath 15 | }); 16 | }); 17 | it('Should return url for a path with no route', function () { 18 | var url = utils.getUrl(request); 19 | should(url).eql(expectedPath); 20 | }); 21 | it('Should return url for a path - route doesn\'t change the url', function () { 22 | request.baseUrl = 'path'; 23 | request.route = { 24 | path: '/:id' 25 | }; 26 | 27 | var url = utils.getUrl(request); 28 | should(url).eql(expectedPath); 29 | }); 30 | it('Should N/A for not valid req obj', function () { 31 | var url = utils.getUrl(undefined); 32 | should(url).eql('N/A'); 33 | }); 34 | }); 35 | 36 | describe('When calling getRoute', function () { 37 | var request; 38 | var expectedRoute = '/path/:id'; 39 | beforeEach(function () { 40 | request = httpMocks.createRequest({ 41 | method: 'POST', 42 | route: { 43 | path: '/:id' 44 | }, 45 | baseUrl: '/path', 46 | }); 47 | }); 48 | it('Should return url_route when req.baseUrl is empty string', function () { 49 | request.baseUrl = ''; 50 | var url_route = utils.getRoute(request); 51 | should(url_route).eql('/:id'); 52 | }); 53 | it('Should return url_route for a path', function () { 54 | var url_route = utils.getRoute(request); 55 | should(url_route).eql(expectedRoute); 56 | }); 57 | it('Should N/A for not valid req obj', function () { 58 | var url = utils.getUrl(undefined); 59 | should(url).eql('N/A'); 60 | }); 61 | }); 62 | 63 | describe('When calling maskJsonValue', function () { 64 | var originalJsonObj; 65 | var expectedJsonObj; 66 | 67 | var fieldsToMask; 68 | var expectedMaskedValue = 'XXXXX'; 69 | 70 | beforeEach(function () { 71 | originalJsonObj = { 72 | secret: 'secret', 73 | public: 'public' 74 | }; 75 | 76 | expectedJsonObj = { 77 | secret: expectedMaskedValue, 78 | public: 'public' 79 | }; 80 | 81 | fieldsToMask = ['secret']; 82 | }); 83 | it('Should return original Json Object if specified field not found', function () { 84 | fieldsToMask = ['other_field']; 85 | var masked = utils.maskJson(originalJsonObj, fieldsToMask); 86 | should(masked).eql(originalJsonObj); 87 | }); 88 | it('Should return original Json Object with specified field masked', function () { 89 | var masked = utils.maskJson(originalJsonObj, fieldsToMask); 90 | should(masked).eql(expectedJsonObj); 91 | }); 92 | it('Should return original Json Object with more than one specified field masked', function () { 93 | fieldsToMask = ['secret', 'public']; 94 | expectedJsonObj = { 95 | secret: expectedMaskedValue, 96 | public: expectedMaskedValue 97 | }; 98 | 99 | var masked = utils.maskJson(originalJsonObj, fieldsToMask); 100 | should(masked).eql(expectedJsonObj); 101 | }); 102 | it('Should return null for null input', function () { 103 | var masked = utils.maskJson(null, fieldsToMask); 104 | should(masked).eql(null); 105 | }); 106 | it('Should return undefined for undefined input', function () { 107 | var masked = utils.maskJson(undefined, fieldsToMask); 108 | should(masked).eql(undefined); 109 | }); 110 | it('Should return empty object for empty object input', function () { 111 | var masked = utils.maskJson({}, fieldsToMask); 112 | should(masked).eql({}); 113 | }); 114 | it('Should mask all field occurrences', function () { 115 | let fieldsToMask = ['password']; 116 | let originalJsonObj = { 117 | password: 'password', 118 | user: { 119 | name: 'papa user', 120 | password: 'password to change' 121 | }, 122 | users: [ 123 | { 124 | name: 'name1', 125 | password: 'password to change' 126 | }, 127 | { 128 | name: 'name2', 129 | password: 'password to change' 130 | } 131 | ] 132 | }; 133 | let expectedJsonObj = { 134 | password: expectedMaskedValue, 135 | user: { 136 | name: 'papa user', 137 | password: expectedMaskedValue 138 | }, 139 | users: [ 140 | { 141 | name: 'name1', 142 | password: expectedMaskedValue 143 | }, 144 | { 145 | name: 'name2', 146 | password: expectedMaskedValue 147 | } 148 | ] 149 | }; 150 | var masked = utils.maskJson(originalJsonObj, fieldsToMask); 151 | should(masked).eql(expectedJsonObj); 152 | }); 153 | }); 154 | 155 | describe('When calling shouldAuditURL', function () { 156 | var urls = []; 157 | var request; 158 | var urlToExclude = 'exclude'; 159 | var urlNotToExclude = 'audit'; 160 | beforeEach(function () { 161 | urls = ['exclude']; 162 | request = httpMocks.createRequest({ 163 | method: 'POST', 164 | url: urlNotToExclude + '/123' 165 | }); 166 | }); 167 | it('Should return true if none of the specified urls match the current path', function () { 168 | var res = utils.shouldAuditURL(urls, request); 169 | should(res).eql(true); 170 | }); 171 | it('Should return false if one of the specified urls match the current path', function () { 172 | request.url = urlToExclude + '/123'; 173 | var res = utils.shouldAuditURL(urls, request); 174 | should(res).eql(false); 175 | }); 176 | it('Should return false if url matches the route and not the req.url', () => { 177 | request.url = '/'; 178 | request.baseUrl = '/exclude'; 179 | request.route = { 180 | path: '/' 181 | }; 182 | var res = utils.shouldAuditURL(urls, request); 183 | 184 | should(res).eql(false); 185 | }); 186 | }); 187 | describe('When calling getLogLevel', () => { 188 | it('Should return info if levels is undefined', () => { 189 | let result = utils.getLogLevel(200, undefined); 190 | should(result).eql('info'); 191 | }); 192 | it('Should return info if levels is empty array', () => { 193 | let result = utils.getLogLevel(200, []); 194 | should(result).eql('info'); 195 | }); 196 | it('Should return info if exact match but levelMap value is not valid level', () => { 197 | let result = utils.getLogLevel(200, { '200': 'not-valid' }); 198 | should(result).eql('info'); 199 | }) 200 | it('Should return correct exact match of status with level map', () => { 201 | let result = utils.getLogLevel(200, { '200': 'error' }); 202 | should(result).eql('error'); 203 | }); 204 | it('Should return correct exact match if group is configured as well', () => { 205 | let result = utils.getLogLevel(401, { 206 | '200': 'error', 207 | '4xx': 'debug', 208 | '401': 'error', 209 | '500': 'error' 210 | }); 211 | should(result).eql('error'); 212 | }) 213 | it('Should fallback to status group if exact match not found', () => { 214 | let result = utils.getLogLevel(404, { 215 | '200': 'error', 216 | '4xx': 'debug', 217 | '401': 'error', 218 | '500': 'error' 219 | }); 220 | should(result).eql('debug'); 221 | }) 222 | it('Should fallback to default info level if no match is found', () => { 223 | let result = utils.getLogLevel(302, { 224 | '200': 'error', 225 | '4xx': 'debug', 226 | '401': 'error', 227 | '500': 'error' 228 | }); 229 | should(result).eql('info'); 230 | }) 231 | }); 232 | }); --------------------------------------------------------------------------------