├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ ├── ci.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── .eslintrc.yml ├── fixtures ├── server.crt └── server.key └── morgan.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: standard 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | matrix: 12 | name: 13 | - Node.js 0.8 14 | - Node.js 0.10 15 | - Node.js 0.12 16 | - io.js 1.x 17 | - io.js 2.x 18 | - io.js 3.x 19 | - Node.js 4.x 20 | - Node.js 5.x 21 | - Node.js 6.x 22 | - Node.js 7.x 23 | - Node.js 8.x 24 | - Node.js 9.x 25 | - Node.js 10.x 26 | - Node.js 11.x 27 | - Node.js 12.x 28 | - Node.js 13.x 29 | - Node.js 14.x 30 | - Node.js 15.x 31 | - Node.js 16.x 32 | - Node.js 17.x 33 | - Node.js 18.x 34 | - Node.js 19.x 35 | - Node.js 20.x 36 | - Node.js 21.x 37 | - Node.js 22.x 38 | 39 | include: 40 | - name: Node.js 0.8 41 | node-version: "0.8" 42 | npm-i: mocha@2.5.3 supertest@1.1.0 43 | npm-rm: nyc 44 | 45 | - name: Node.js 0.10 46 | node-version: "0.10" 47 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 48 | 49 | - name: Node.js 0.12 50 | node-version: "0.12" 51 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 52 | 53 | - name: io.js 1.x 54 | node-version: "1.8" 55 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 56 | 57 | - name: io.js 2.x 58 | node-version: "2.5" 59 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 60 | 61 | - name: io.js 3.x 62 | node-version: "3.3" 63 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 64 | 65 | - name: Node.js 4.x 66 | node-version: "4.9" 67 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 68 | 69 | - name: Node.js 5.x 70 | node-version: "5.12" 71 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 72 | 73 | - name: Node.js 6.x 74 | node-version: "6.17" 75 | npm-i: mocha@6.2.2 nyc@14.1.1 76 | 77 | - name: Node.js 7.x 78 | node-version: "7.10" 79 | npm-i: mocha@6.2.2 nyc@14.1.1 80 | 81 | - name: Node.js 8.x 82 | node-version: "8.17" 83 | npm-i: mocha@7.1.2 nyc@14.1.1 84 | 85 | - name: Node.js 9.x 86 | node-version: "9.11" 87 | npm-i: mocha@7.1.2 nyc@14.1.1 88 | 89 | - name: Node.js 10.x 90 | node-version: "10.24" 91 | npm-i: mocha@8.4.0 92 | 93 | - name: Node.js 11.x 94 | node-version: "11.15" 95 | npm-i: mocha@8.4.0 96 | 97 | - name: Node.js 12.x 98 | node-version: "12.22" 99 | npm-i: mocha@9.2.2 100 | 101 | - name: Node.js 13.x 102 | node-version: "13.14" 103 | npm-i: mocha@9.2.2 104 | 105 | - name: Node.js 14.x 106 | node-version: "14.21" 107 | 108 | - name: Node.js 15.x 109 | node-version: "15.14" 110 | 111 | - name: Node.js 16.x 112 | node-version: "16.20" 113 | 114 | - name: Node.js 17.x 115 | node-version: "17.9" 116 | 117 | - name: Node.js 18.x 118 | node-version: "18.18" 119 | 120 | - name: Node.js 19.x 121 | node-version: "19.9" 122 | 123 | - name: Node.js 20.x 124 | node-version: "20.9" 125 | 126 | - name: Node.js 21.x 127 | node-version: "21.1" 128 | 129 | - name: Node.js 22.x 130 | node-version: "22.0" 131 | 132 | steps: 133 | - uses: actions/checkout@v4 134 | 135 | - name: Install Node.js ${{ matrix.node-version }} 136 | shell: bash -eo pipefail -l {0} 137 | run: | 138 | nvm install --default ${{ matrix.node-version }} 139 | if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then 140 | nvm install --alias=npm 0.10 141 | nvm use ${{ matrix.node-version }} 142 | if [[ "$(npm -v)" == 1.1.* ]]; then 143 | nvm exec npm npm install -g npm@1.1 144 | ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm" 145 | else 146 | sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")" 147 | fi 148 | npm config set strict-ssl false 149 | fi 150 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 151 | 152 | - name: Configure npm 153 | run: | 154 | if [[ "$(npm config get package-lock)" == "true" ]]; then 155 | npm config set package-lock false 156 | else 157 | npm config set shrinkwrap false 158 | fi 159 | 160 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 161 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 162 | if: matrix.npm-rm != '' 163 | 164 | - name: Install npm module(s) ${{ matrix.npm-i }} 165 | run: npm install --save-dev ${{ matrix.npm-i }} 166 | if: matrix.npm-i != '' 167 | 168 | - name: Setup Node.js version-specific dependencies 169 | shell: bash 170 | run: | 171 | # eslint for linting 172 | # - remove on Node.js < 12 173 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then 174 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 175 | grep -E '^eslint(-|$)' | \ 176 | sort -r | \ 177 | xargs -n1 npm rm --silent --save-dev 178 | fi 179 | 180 | - name: Install Node.js dependencies 181 | run: npm install 182 | 183 | - name: List environment 184 | id: list_env 185 | shell: bash 186 | run: | 187 | echo "node@$(node -v)" 188 | echo "npm@$(npm -v)" 189 | npm -s ls ||: 190 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 191 | 192 | - name: Run tests 193 | shell: bash 194 | run: | 195 | if npm -ps ls nyc | grep -q nyc; then 196 | npm run test-ci 197 | else 198 | npm test 199 | fi 200 | 201 | - name: Lint code 202 | if: steps.list_env.outputs.eslint != '' 203 | run: npm run lint 204 | 205 | - name: Collect code coverage 206 | uses: coverallsapp/github-action@master 207 | if: steps.list_env.outputs.nyc != '' 208 | with: 209 | github-token: ${{ secrets.GITHUB_TOKEN }} 210 | flag-name: run-${{ matrix.test_number }} 211 | parallel: true 212 | 213 | coverage: 214 | needs: test 215 | runs-on: ubuntu-latest 216 | steps: 217 | - name: Upload code coverage 218 | uses: coverallsapp/github-action@master 219 | with: 220 | github-token: ${{ secrets.GITHUB_TOKEN }} 221 | parallel-finished: true 222 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '16 21 * * 1' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 49 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard. 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@2f93e4319b2f04a2efc38fa7f78bd681bc3f7b2f # v2.23.2 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 1.10.0 / 2020-03-20 2 | =================== 3 | 4 | * Add `:total-time` token 5 | * Fix trailing space in colored status code for `dev` format 6 | * deps: basic-auth@~2.0.1 7 | - deps: safe-buffer@5.1.2 8 | * deps: depd@~2.0.0 9 | - Replace internal `eval` usage with `Function` constructor 10 | - Use instance methods on `process` to check for listeners 11 | * deps: on-headers@~1.0.2 12 | - Fix `res.writeHead` patch missing return value 13 | 14 | 1.9.1 / 2018-09-10 15 | ================== 16 | 17 | * Fix using special characters in format 18 | * deps: depd@~1.1.2 19 | - perf: remove argument reassignment 20 | 21 | 1.9.0 / 2017-09-26 22 | ================== 23 | 24 | * Use `res.headersSent` when available 25 | * deps: basic-auth@~2.0.0 26 | - Use `safe-buffer` for improved Buffer API 27 | * deps: debug@2.6.9 28 | * deps: depd@~1.1.1 29 | - Remove unnecessary `Buffer` loading 30 | 31 | 1.8.2 / 2017-05-23 32 | ================== 33 | 34 | * deps: debug@2.6.8 35 | - Fix `DEBUG_MAX_ARRAY_LENGTH` 36 | - deps: ms@2.0.0 37 | 38 | 1.8.1 / 2017-02-04 39 | ================== 40 | 41 | * deps: debug@2.6.1 42 | - Fix deprecation messages in WebStorm and other editors 43 | - Undeprecate `DEBUG_FD` set to `1` or `2` 44 | 45 | 1.8.0 / 2017-02-04 46 | ================== 47 | 48 | * Fix sending unnecessary `undefined` argument to token functions 49 | * deps: basic-auth@~1.1.0 50 | * deps: debug@2.6.0 51 | - Allow colors in workers 52 | - Deprecated `DEBUG_FD` environment variable 53 | - Fix error when running under React Native 54 | - Use same color for same namespace 55 | - deps: ms@0.7.2 56 | * perf: enable strict mode in compiled functions 57 | 58 | 1.7.0 / 2016-02-18 59 | ================== 60 | 61 | * Add `digits` argument to `response-time` token 62 | * deps: depd@~1.1.0 63 | - Enable strict mode in more places 64 | - Support web browser loading 65 | * deps: on-headers@~1.0.1 66 | - perf: enable strict mode 67 | 68 | 1.6.1 / 2015-07-03 69 | ================== 70 | 71 | * deps: basic-auth@~1.0.3 72 | 73 | 1.6.0 / 2015-06-12 74 | ================== 75 | 76 | * Add `morgan.compile(format)` export 77 | * Do not color 1xx status codes in `dev` format 78 | * Fix `response-time` token to not include response latency 79 | * Fix `status` token incorrectly displaying before response in `dev` format 80 | * Fix token return values to be `undefined` or a string 81 | * Improve representation of multiple headers in `req` and `res` tokens 82 | * Use `res.getHeader` in `res` token 83 | * deps: basic-auth@~1.0.2 84 | - perf: enable strict mode 85 | - perf: hoist regular expression 86 | - perf: parse with regular expressions 87 | - perf: remove argument reassignment 88 | * deps: on-finished@~2.3.0 89 | - Add defined behavior for HTTP `CONNECT` requests 90 | - Add defined behavior for HTTP `Upgrade` requests 91 | - deps: ee-first@1.1.1 92 | * pref: enable strict mode 93 | * pref: reduce function closure scopes 94 | * pref: remove dynamic compile on every request for `dev` format 95 | * pref: remove an argument reassignment 96 | * pref: skip function call without `skip` option 97 | 98 | 1.5.3 / 2015-05-10 99 | ================== 100 | 101 | * deps: basic-auth@~1.0.1 102 | * deps: debug@~2.2.0 103 | - deps: ms@0.7.1 104 | * deps: depd@~1.0.1 105 | * deps: on-finished@~2.2.1 106 | - Fix `isFinished(req)` when data buffered 107 | 108 | 1.5.2 / 2015-03-15 109 | ================== 110 | 111 | * deps: debug@~2.1.3 112 | - Fix high intensity foreground color for bold 113 | - deps: ms@0.7.0 114 | 115 | 1.5.1 / 2014-12-31 116 | ================== 117 | 118 | * deps: debug@~2.1.1 119 | * deps: on-finished@~2.2.0 120 | 121 | 1.5.0 / 2014-11-06 122 | ================== 123 | 124 | * Add multiple date formats 125 | - `clf` for the common log format 126 | - `iso` for the common ISO 8601 date time format 127 | - `web` for the common RFC 1123 date time format 128 | * Deprecate `buffer` option 129 | * Fix date format in `common` and `combined` formats 130 | * Fix token arguments to accept values with `"` 131 | 132 | 1.4.1 / 2014-10-22 133 | ================== 134 | 135 | * deps: on-finished@~2.1.1 136 | - Fix handling of pipelined requests 137 | 138 | 1.4.0 / 2014-10-16 139 | ================== 140 | 141 | * Add `debug` messages 142 | * deps: depd@~1.0.0 143 | 144 | 1.3.2 / 2014-09-27 145 | ================== 146 | 147 | * Fix `req.ip` integration when `immediate: false` 148 | 149 | 1.3.1 / 2014-09-14 150 | ================== 151 | 152 | * Remove un-used `bytes` dependency 153 | * deps: depd@0.4.5 154 | 155 | 1.3.0 / 2014-09-01 156 | ================== 157 | 158 | * Assert if `format` is not a function or string 159 | 160 | 1.2.3 / 2014-08-16 161 | ================== 162 | 163 | * deps: on-finished@2.1.0 164 | 165 | 1.2.2 / 2014-07-27 166 | ================== 167 | 168 | * deps: depd@0.4.4 169 | - Work-around v8 generating empty stack traces 170 | 171 | 1.2.1 / 2014-07-26 172 | ================== 173 | 174 | * deps: depd@0.4.3 175 | - Fix exception when global `Error.stackTraceLimit` is too low 176 | 177 | 1.2.0 / 2014-07-19 178 | ================== 179 | 180 | * Add `:remote-user` token 181 | * Add `combined` log format 182 | * Add `common` log format 183 | * Add `morgan(format, options)` function signature 184 | * Deprecate `default` format -- use `combined` format instead 185 | * Deprecate not providing a format 186 | * Remove non-standard grey color from `dev` format 187 | 188 | 1.1.1 / 2014-05-20 189 | ================== 190 | 191 | * simplify method to get remote address 192 | 193 | 1.1.0 / 2014-05-18 194 | ================== 195 | 196 | * "dev" format will use same tokens as other formats 197 | * `:response-time` token is now empty when immediate used 198 | * `:response-time` token is now monotonic 199 | * `:response-time` token has precision to 1 μs 200 | * fix `:status` + immediate output in node.js 0.8 201 | * improve `buffer` option to prevent indefinite event loop holding 202 | * deps: bytes@1.0.0 203 | - add negative support 204 | 205 | 1.0.1 / 2014-05-04 206 | ================== 207 | 208 | * Make buffer unique per morgan instance 209 | * deps: bytes@0.3.0 210 | * added terabyte support 211 | 212 | 1.0.0 / 2014-02-08 213 | ================== 214 | 215 | * Initial release 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Jonathan Ong 4 | Copyright (c) 2014-2017 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # morgan 2 | 3 | [![NPM Version][npm-version-image]][npm-url] 4 | [![NPM Downloads][npm-downloads-image]][npm-url] 5 | [![Build Status][ci-image]][ci-url] 6 | [![Coverage Status][coveralls-image]][coveralls-url] 7 | 8 | HTTP request logger middleware for node.js 9 | 10 | > Named after [Dexter](http://en.wikipedia.org/wiki/Dexter_Morgan), a show you should not watch until completion. 11 | 12 | ## Installation 13 | 14 | This is a [Node.js](https://nodejs.org/en/) module available through the 15 | [npm registry](https://www.npmjs.com/). Installation is done using the 16 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 17 | 18 | ```sh 19 | $ npm install morgan 20 | ``` 21 | 22 | ## API 23 | 24 | 25 | 26 | ```js 27 | var morgan = require('morgan') 28 | ``` 29 | 30 | ### morgan(format, options) 31 | 32 | Create a new morgan logger middleware function using the given `format` and `options`. 33 | The `format` argument may be a string of a predefined name (see below for the names), 34 | a string of a format string, or a function that will produce a log entry. 35 | 36 | The `format` function will be called with three arguments `tokens`, `req`, and `res`, 37 | where `tokens` is an object with all defined tokens, `req` is the HTTP request and `res` 38 | is the HTTP response. The function is expected to return a string that will be the log 39 | line, or `undefined` / `null` to skip logging. 40 | 41 | #### Using a predefined format string 42 | 43 | 44 | 45 | ```js 46 | morgan('tiny') 47 | ``` 48 | 49 | #### Using format string of predefined tokens 50 | 51 | 52 | 53 | ```js 54 | morgan(':method :url :status :res[content-length] - :response-time ms') 55 | ``` 56 | 57 | #### Using a custom format function 58 | 59 | 60 | 61 | ``` js 62 | morgan(function (tokens, req, res) { 63 | return [ 64 | tokens.method(req, res), 65 | tokens.url(req, res), 66 | tokens.status(req, res), 67 | tokens.res(req, res, 'content-length'), '-', 68 | tokens['response-time'](req, res), 'ms' 69 | ].join(' ') 70 | }) 71 | ``` 72 | 73 | #### Options 74 | 75 | Morgan accepts these properties in the options object. 76 | 77 | ##### immediate 78 | 79 | Write log line on request instead of response. This means that a requests will 80 | be logged even if the server crashes, _but data from the response (like the 81 | response code, content length, etc.) cannot be logged_. 82 | 83 | ##### skip 84 | 85 | Function to determine if logging is skipped, defaults to `false`. This function 86 | will be called as `skip(req, res)`. 87 | 88 | 89 | 90 | ```js 91 | // EXAMPLE: only log error responses 92 | morgan('combined', { 93 | skip: function (req, res) { return res.statusCode < 400 } 94 | }) 95 | ``` 96 | 97 | ##### stream 98 | 99 | Output stream for writing log lines, defaults to `process.stdout`. 100 | 101 | #### Predefined Formats 102 | 103 | There are various pre-defined formats provided: 104 | 105 | ##### combined 106 | 107 | Standard Apache combined log output. 108 | ``` 109 | :remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" 110 | # will output 111 | ::1 - - [27/Nov/2024:06:21:42 +0000] "GET /combined HTTP/1.1" 200 2 "-" "curl/8.7.1" 112 | ``` 113 | 114 | ##### common 115 | 116 | Standard Apache common log output. 117 | 118 | ``` 119 | :remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] 120 | # will output 121 | ::1 - - [27/Nov/2024:06:21:46 +0000] "GET /common HTTP/1.1" 200 2 122 | ``` 123 | 124 | ##### dev 125 | 126 | Concise output colored by response status for development use. The `:status` 127 | token will be colored green for success codes, red for server error codes, 128 | yellow for client error codes, cyan for redirection codes, and uncolored 129 | for information codes. 130 | 131 | ``` 132 | :method :url :status :response-time ms - :res[content-length] 133 | # will output 134 | GET /dev 200 0.224 ms - 2 135 | ``` 136 | 137 | ##### short 138 | 139 | Shorter than default, also including response time. 140 | 141 | ``` 142 | :remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms 143 | # will output 144 | ::1 - GET /short HTTP/1.1 200 2 - 0.283 ms 145 | ``` 146 | 147 | ##### tiny 148 | 149 | The minimal output. 150 | 151 | ``` 152 | :method :url :status :res[content-length] - :response-time ms 153 | # will output 154 | GET /tiny 200 2 - 0.188 ms 155 | ``` 156 | 157 | #### Tokens 158 | 159 | ##### Creating new tokens 160 | 161 | To define a token, simply invoke `morgan.token()` with the name and a callback function. 162 | This callback function is expected to return a string value. The value returned is then 163 | available as ":type" in this case: 164 | 165 | 166 | 167 | ```js 168 | morgan.token('type', function (req, res) { return req.headers['content-type'] }) 169 | ``` 170 | 171 | Calling `morgan.token()` using the same name as an existing token will overwrite that 172 | token definition. 173 | 174 | The token function is expected to be called with the arguments `req` and `res`, representing 175 | the HTTP request and HTTP response. Additionally, the token can accept further arguments of 176 | it's choosing to customize behavior. 177 | 178 | ##### :date[format] 179 | 180 | The current date and time in UTC. The available formats are: 181 | 182 | - `clf` for the common log format (`"10/Oct/2000:13:55:36 +0000"`) 183 | - `iso` for the common ISO 8601 date time format (`2000-10-10T13:55:36.000Z`) 184 | - `web` for the common RFC 1123 date time format (`Tue, 10 Oct 2000 13:55:36 GMT`) 185 | 186 | If no format is given, then the default is `web`. 187 | 188 | ##### :http-version 189 | 190 | The HTTP version of the request. 191 | 192 | ##### :method 193 | 194 | The HTTP method of the request. 195 | 196 | ##### :referrer 197 | 198 | The Referrer header of the request. This will use the standard mis-spelled Referer header if exists, otherwise Referrer. 199 | 200 | ##### :remote-addr 201 | 202 | The remote address of the request. This will use `req.ip`, otherwise the standard `req.connection.remoteAddress` value (socket address). 203 | 204 | ##### :remote-user 205 | 206 | The user authenticated as part of Basic auth for the request. 207 | 208 | ##### :req[header] 209 | 210 | The given `header` of the request. If the header is not present, the 211 | value will be displayed as `"-"` in the log. 212 | 213 | ##### :res[header] 214 | 215 | The given `header` of the response. If the header is not present, the 216 | value will be displayed as `"-"` in the log. 217 | 218 | ##### :response-time[digits] 219 | 220 | The time between the request coming into `morgan` and when the response 221 | headers are written, in milliseconds. 222 | 223 | The `digits` argument is a number that specifies the number of digits to 224 | include on the number, defaulting to `3`, which provides microsecond precision. 225 | 226 | ##### :status 227 | 228 | The status code of the response. 229 | 230 | If the request/response cycle completes before a response was sent to the 231 | client (for example, the TCP socket closed prematurely by a client aborting 232 | the request), then the status will be empty (displayed as `"-"` in the log). 233 | 234 | ##### :total-time[digits] 235 | 236 | The time between the request coming into `morgan` and when the response 237 | has finished being written out to the connection, in milliseconds. 238 | 239 | The `digits` argument is a number that specifies the number of digits to 240 | include on the number, defaulting to `3`, which provides microsecond precision. 241 | 242 | ##### :url 243 | 244 | The URL of the request. This will use `req.originalUrl` if exists, otherwise `req.url`. 245 | 246 | ##### :user-agent 247 | 248 | The contents of the User-Agent header of the request. 249 | 250 | ### morgan.compile(format) 251 | 252 | Compile a format string into a `format` function for use by `morgan`. A format string 253 | is a string that represents a single log line and can utilize token syntax. 254 | Tokens are references by `:token-name`. If tokens accept arguments, they can 255 | be passed using `[]`, for example: `:token-name[pretty]` would pass the string 256 | `'pretty'` as an argument to the token `token-name`. 257 | 258 | The function returned from `morgan.compile` takes three arguments `tokens`, `req`, and 259 | `res`, where `tokens` is object with all defined tokens, `req` is the HTTP request and 260 | `res` is the HTTP response. The function will return a string that will be the log line, 261 | or `undefined` / `null` to skip logging. 262 | 263 | Normally formats are defined using `morgan.format(name, format)`, but for certain 264 | advanced uses, this compile function is directly available. 265 | 266 | ## Examples 267 | 268 | ### express/connect 269 | 270 | Sample app that will log all request in the Apache combined format to STDOUT 271 | 272 | ```js 273 | var express = require('express') 274 | var morgan = require('morgan') 275 | 276 | var app = express() 277 | 278 | app.use(morgan('combined')) 279 | 280 | app.get('/', function (req, res) { 281 | res.send('hello, world!') 282 | }) 283 | ``` 284 | 285 | ### vanilla http server 286 | 287 | Sample app that will log all request in the Apache combined format to STDOUT 288 | 289 | ```js 290 | var finalhandler = require('finalhandler') 291 | var http = require('http') 292 | var morgan = require('morgan') 293 | 294 | // create "middleware" 295 | var logger = morgan('combined') 296 | 297 | http.createServer(function (req, res) { 298 | var done = finalhandler(req, res) 299 | logger(req, res, function (err) { 300 | if (err) return done(err) 301 | 302 | // respond to request 303 | res.setHeader('content-type', 'text/plain') 304 | res.end('hello, world!') 305 | }) 306 | }) 307 | ``` 308 | 309 | ### write logs to a file 310 | 311 | #### single file 312 | 313 | Sample app that will log all requests in the Apache combined format to the file 314 | `access.log`. 315 | 316 | ```js 317 | var express = require('express') 318 | var fs = require('fs') 319 | var morgan = require('morgan') 320 | var path = require('path') 321 | 322 | var app = express() 323 | 324 | // create a write stream (in append mode) 325 | var accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' }) 326 | 327 | // setup the logger 328 | app.use(morgan('combined', { stream: accessLogStream })) 329 | 330 | app.get('/', function (req, res) { 331 | res.send('hello, world!') 332 | }) 333 | ``` 334 | 335 | #### log file rotation 336 | 337 | Sample app that will log all requests in the Apache combined format to one log 338 | file per day in the `log/` directory using the 339 | [rotating-file-stream module](https://www.npmjs.com/package/rotating-file-stream). 340 | 341 | ```js 342 | var express = require('express') 343 | var morgan = require('morgan') 344 | var path = require('path') 345 | var rfs = require('rotating-file-stream') // version 2.x 346 | 347 | var app = express() 348 | 349 | // create a rotating write stream 350 | var accessLogStream = rfs.createStream('access.log', { 351 | interval: '1d', // rotate daily 352 | path: path.join(__dirname, 'log') 353 | }) 354 | 355 | // setup the logger 356 | app.use(morgan('combined', { stream: accessLogStream })) 357 | 358 | app.get('/', function (req, res) { 359 | res.send('hello, world!') 360 | }) 361 | ``` 362 | 363 | ### split / dual logging 364 | 365 | The `morgan` middleware can be used as many times as needed, enabling 366 | combinations like: 367 | 368 | * Log entry on request and one on response 369 | * Log all requests to file, but errors to console 370 | * ... and more! 371 | 372 | Sample app that will log all requests to a file using Apache format, but 373 | error responses are logged to the console: 374 | 375 | ```js 376 | var express = require('express') 377 | var fs = require('fs') 378 | var morgan = require('morgan') 379 | var path = require('path') 380 | 381 | var app = express() 382 | 383 | // log only 4xx and 5xx responses to console 384 | app.use(morgan('dev', { 385 | skip: function (req, res) { return res.statusCode < 400 } 386 | })) 387 | 388 | // log all requests to access.log 389 | app.use(morgan('common', { 390 | stream: fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' }) 391 | })) 392 | 393 | app.get('/', function (req, res) { 394 | res.send('hello, world!') 395 | }) 396 | ``` 397 | 398 | ### use custom token formats 399 | 400 | Sample app that will use custom token formats. This adds an ID to all requests and displays it using the `:id` token. 401 | 402 | ```js 403 | var express = require('express') 404 | var morgan = require('morgan') 405 | var uuid = require('node-uuid') 406 | 407 | morgan.token('id', function getId (req) { 408 | return req.id 409 | }) 410 | 411 | var app = express() 412 | 413 | app.use(assignId) 414 | app.use(morgan(':id :method :url :response-time')) 415 | 416 | app.get('/', function (req, res) { 417 | res.send('hello, world!') 418 | }) 419 | 420 | function assignId (req, res, next) { 421 | req.id = uuid.v4() 422 | next() 423 | } 424 | ``` 425 | 426 | ## License 427 | 428 | [MIT](LICENSE) 429 | 430 | [ci-image]: https://badgen.net/github/checks/expressjs/morgan/master?label=ci 431 | [ci-url]: https://github.com/expressjs/morgan/actions/workflows/ci.yml 432 | [coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/morgan/master 433 | [coveralls-url]: https://coveralls.io/r/expressjs/morgan?branch=master 434 | [npm-downloads-image]: https://badgen.net/npm/dm/morgan 435 | [npm-url]: https://npmjs.org/package/morgan 436 | [npm-version-image]: https://badgen.net/npm/v/morgan 437 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * morgan 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * Copyright(c) 2014 Jonathan Ong 6 | * Copyright(c) 2014-2017 Douglas Christopher Wilson 7 | * MIT Licensed 8 | */ 9 | 10 | 'use strict' 11 | 12 | /** 13 | * Module exports. 14 | * @public 15 | */ 16 | 17 | module.exports = morgan 18 | module.exports.compile = compile 19 | module.exports.format = format 20 | module.exports.token = token 21 | 22 | /** 23 | * Module dependencies. 24 | * @private 25 | */ 26 | 27 | var auth = require('basic-auth') 28 | var debug = require('debug')('morgan') 29 | var deprecate = require('depd')('morgan') 30 | var onFinished = require('on-finished') 31 | var onHeaders = require('on-headers') 32 | 33 | /** 34 | * Array of CLF month names. 35 | * @private 36 | */ 37 | 38 | var CLF_MONTH = [ 39 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 40 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' 41 | ] 42 | 43 | /** 44 | * Default log buffer duration. 45 | * @private 46 | */ 47 | 48 | var DEFAULT_BUFFER_DURATION = 1000 49 | 50 | /** 51 | * Create a logger middleware. 52 | * 53 | * @public 54 | * @param {String|Function} format 55 | * @param {Object} [options] 56 | * @return {Function} middleware 57 | */ 58 | 59 | function morgan (format, options) { 60 | var fmt = format 61 | var opts = options || {} 62 | 63 | if (format && typeof format === 'object') { 64 | opts = format 65 | fmt = opts.format || 'default' 66 | 67 | // smart deprecation message 68 | deprecate('morgan(options): use morgan(' + (typeof fmt === 'string' ? JSON.stringify(fmt) : 'format') + ', options) instead') 69 | } 70 | 71 | if (fmt === undefined) { 72 | deprecate('undefined format: specify a format') 73 | } 74 | 75 | // output on request instead of response 76 | var immediate = opts.immediate 77 | 78 | // check if log entry should be skipped 79 | var skip = opts.skip || false 80 | 81 | // format function 82 | var formatLine = typeof fmt !== 'function' 83 | ? getFormatFunction(fmt) 84 | : fmt 85 | 86 | // stream 87 | var buffer = opts.buffer 88 | var stream = opts.stream || process.stdout 89 | 90 | // buffering support 91 | if (buffer) { 92 | deprecate('buffer option') 93 | 94 | // flush interval 95 | var interval = typeof buffer !== 'number' 96 | ? DEFAULT_BUFFER_DURATION 97 | : buffer 98 | 99 | // swap the stream 100 | stream = createBufferStream(stream, interval) 101 | } 102 | 103 | return function logger (req, res, next) { 104 | // request data 105 | req._startAt = undefined 106 | req._startTime = undefined 107 | req._remoteAddress = getip(req) 108 | 109 | // response data 110 | res._startAt = undefined 111 | res._startTime = undefined 112 | 113 | // record request start 114 | recordStartTime.call(req) 115 | 116 | function logRequest () { 117 | if (skip !== false && skip(req, res)) { 118 | debug('skip request') 119 | return 120 | } 121 | 122 | var line = formatLine(morgan, req, res) 123 | 124 | if (line == null) { 125 | debug('skip line') 126 | return 127 | } 128 | 129 | debug('log request') 130 | stream.write(line + '\n') 131 | }; 132 | 133 | if (immediate) { 134 | // immediate log 135 | logRequest() 136 | } else { 137 | // record response start 138 | onHeaders(res, recordStartTime) 139 | 140 | // log when response finished 141 | onFinished(res, logRequest) 142 | } 143 | 144 | next() 145 | } 146 | } 147 | 148 | /** 149 | * Apache combined log format. 150 | */ 151 | 152 | morgan.format('combined', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"') 153 | 154 | /** 155 | * Apache common log format. 156 | */ 157 | 158 | morgan.format('common', ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]') 159 | 160 | /** 161 | * Default format. 162 | */ 163 | 164 | morgan.format('default', ':remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"') 165 | deprecate.property(morgan, 'default', 'default format: use combined format') 166 | 167 | /** 168 | * Short format. 169 | */ 170 | 171 | morgan.format('short', ':remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms') 172 | 173 | /** 174 | * Tiny format. 175 | */ 176 | 177 | morgan.format('tiny', ':method :url :status :res[content-length] - :response-time ms') 178 | 179 | /** 180 | * dev (colored) 181 | */ 182 | 183 | morgan.format('dev', function developmentFormatLine (tokens, req, res) { 184 | // get the status code if response written 185 | var status = headersSent(res) 186 | ? res.statusCode 187 | : undefined 188 | 189 | // get status color 190 | var color = status >= 500 ? 31 // red 191 | : status >= 400 ? 33 // yellow 192 | : status >= 300 ? 36 // cyan 193 | : status >= 200 ? 32 // green 194 | : 0 // no color 195 | 196 | // get colored function 197 | var fn = developmentFormatLine[color] 198 | 199 | if (!fn) { 200 | // compile 201 | fn = developmentFormatLine[color] = compile('\x1b[0m:method :url \x1b[' + 202 | color + 'm:status\x1b[0m :response-time ms - :res[content-length]\x1b[0m') 203 | } 204 | 205 | return fn(tokens, req, res) 206 | }) 207 | 208 | /** 209 | * request url 210 | */ 211 | 212 | morgan.token('url', function getUrlToken (req) { 213 | return req.originalUrl || req.url 214 | }) 215 | 216 | /** 217 | * request method 218 | */ 219 | 220 | morgan.token('method', function getMethodToken (req) { 221 | return req.method 222 | }) 223 | 224 | /** 225 | * response time in milliseconds 226 | */ 227 | 228 | morgan.token('response-time', function getResponseTimeToken (req, res, digits) { 229 | if (!req._startAt || !res._startAt) { 230 | // missing request and/or response start time 231 | return 232 | } 233 | 234 | // calculate diff 235 | var ms = (res._startAt[0] - req._startAt[0]) * 1e3 + 236 | (res._startAt[1] - req._startAt[1]) * 1e-6 237 | 238 | // return truncated value 239 | return ms.toFixed(digits === undefined ? 3 : digits) 240 | }) 241 | 242 | /** 243 | * total time in milliseconds 244 | */ 245 | 246 | morgan.token('total-time', function getTotalTimeToken (req, res, digits) { 247 | if (!req._startAt || !res._startAt) { 248 | // missing request and/or response start time 249 | return 250 | } 251 | 252 | // time elapsed from request start 253 | var elapsed = process.hrtime(req._startAt) 254 | 255 | // cover to milliseconds 256 | var ms = (elapsed[0] * 1e3) + (elapsed[1] * 1e-6) 257 | 258 | // return truncated value 259 | return ms.toFixed(digits === undefined ? 3 : digits) 260 | }) 261 | 262 | /** 263 | * current date 264 | */ 265 | 266 | morgan.token('date', function getDateToken (req, res, format) { 267 | var date = new Date() 268 | 269 | switch (format || 'web') { 270 | case 'clf': 271 | return clfdate(date) 272 | case 'iso': 273 | return date.toISOString() 274 | case 'web': 275 | return date.toUTCString() 276 | } 277 | }) 278 | 279 | /** 280 | * response status code 281 | */ 282 | 283 | morgan.token('status', function getStatusToken (req, res) { 284 | return headersSent(res) 285 | ? String(res.statusCode) 286 | : undefined 287 | }) 288 | 289 | /** 290 | * normalized referrer 291 | */ 292 | 293 | morgan.token('referrer', function getReferrerToken (req) { 294 | return req.headers.referer || req.headers.referrer 295 | }) 296 | 297 | /** 298 | * remote address 299 | */ 300 | 301 | morgan.token('remote-addr', getip) 302 | 303 | /** 304 | * remote user 305 | */ 306 | 307 | morgan.token('remote-user', function getRemoteUserToken (req) { 308 | // parse basic credentials 309 | var credentials = auth(req) 310 | 311 | // return username 312 | return credentials 313 | ? credentials.name 314 | : undefined 315 | }) 316 | 317 | /** 318 | * HTTP version 319 | */ 320 | 321 | morgan.token('http-version', function getHttpVersionToken (req) { 322 | return req.httpVersionMajor + '.' + req.httpVersionMinor 323 | }) 324 | 325 | /** 326 | * UA string 327 | */ 328 | 329 | morgan.token('user-agent', function getUserAgentToken (req) { 330 | return req.headers['user-agent'] 331 | }) 332 | 333 | /** 334 | * request header 335 | */ 336 | 337 | morgan.token('req', function getRequestToken (req, res, field) { 338 | // get header 339 | var header = req.headers[field.toLowerCase()] 340 | 341 | return Array.isArray(header) 342 | ? header.join(', ') 343 | : header 344 | }) 345 | 346 | /** 347 | * response header 348 | */ 349 | 350 | morgan.token('res', function getResponseHeader (req, res, field) { 351 | if (!headersSent(res)) { 352 | return undefined 353 | } 354 | 355 | // get header 356 | var header = res.getHeader(field) 357 | 358 | return Array.isArray(header) 359 | ? header.join(', ') 360 | : header 361 | }) 362 | 363 | /** 364 | * Format a Date in the common log format. 365 | * 366 | * @private 367 | * @param {Date} dateTime 368 | * @return {string} 369 | */ 370 | 371 | function clfdate (dateTime) { 372 | var date = dateTime.getUTCDate() 373 | var hour = dateTime.getUTCHours() 374 | var mins = dateTime.getUTCMinutes() 375 | var secs = dateTime.getUTCSeconds() 376 | var year = dateTime.getUTCFullYear() 377 | 378 | var month = CLF_MONTH[dateTime.getUTCMonth()] 379 | 380 | return pad2(date) + '/' + month + '/' + year + 381 | ':' + pad2(hour) + ':' + pad2(mins) + ':' + pad2(secs) + 382 | ' +0000' 383 | } 384 | 385 | /** 386 | * Compile a format string into a function. 387 | * 388 | * @param {string} format 389 | * @return {function} 390 | * @public 391 | */ 392 | 393 | function compile (format) { 394 | if (typeof format !== 'string') { 395 | throw new TypeError('argument format must be a string') 396 | } 397 | 398 | var fmt = String(JSON.stringify(format)) 399 | var js = ' "use strict"\n return ' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function (_, name, arg) { 400 | var tokenArguments = 'req, res' 401 | var tokenFunction = 'tokens[' + String(JSON.stringify(name)) + ']' 402 | 403 | if (arg !== undefined) { 404 | tokenArguments += ', ' + String(JSON.stringify(arg)) 405 | } 406 | 407 | return '" +\n (' + tokenFunction + '(' + tokenArguments + ') || "-") + "' 408 | }) 409 | 410 | // eslint-disable-next-line no-new-func 411 | return new Function('tokens, req, res', js) 412 | } 413 | 414 | /** 415 | * Create a basic buffering stream. 416 | * 417 | * @param {object} stream 418 | * @param {number} interval 419 | * @public 420 | */ 421 | 422 | function createBufferStream (stream, interval) { 423 | var buf = [] 424 | var timer = null 425 | 426 | // flush function 427 | function flush () { 428 | timer = null 429 | stream.write(buf.join('')) 430 | buf.length = 0 431 | } 432 | 433 | // write function 434 | function write (str) { 435 | if (timer === null) { 436 | timer = setTimeout(flush, interval) 437 | } 438 | 439 | buf.push(str) 440 | } 441 | 442 | // return a minimal "stream" 443 | return { write: write } 444 | } 445 | 446 | /** 447 | * Define a format with the given name. 448 | * 449 | * @param {string} name 450 | * @param {string|function} fmt 451 | * @public 452 | */ 453 | 454 | function format (name, fmt) { 455 | morgan[name] = fmt 456 | return this 457 | } 458 | 459 | /** 460 | * Lookup and compile a named format function. 461 | * 462 | * @param {string} name 463 | * @return {function} 464 | * @public 465 | */ 466 | 467 | function getFormatFunction (name) { 468 | // lookup format 469 | var fmt = morgan[name] || name || morgan.default 470 | 471 | // return compiled format 472 | return typeof fmt !== 'function' 473 | ? compile(fmt) 474 | : fmt 475 | } 476 | 477 | /** 478 | * Get request IP address. 479 | * 480 | * @private 481 | * @param {IncomingMessage} req 482 | * @return {string} 483 | */ 484 | 485 | function getip (req) { 486 | return req.ip || 487 | req._remoteAddress || 488 | (req.connection && req.connection.remoteAddress) || 489 | undefined 490 | } 491 | 492 | /** 493 | * Determine if the response headers have been sent. 494 | * 495 | * @param {object} res 496 | * @returns {boolean} 497 | * @private 498 | */ 499 | 500 | function headersSent (res) { 501 | // istanbul ignore next: node.js 0.8 support 502 | return typeof res.headersSent !== 'boolean' 503 | ? Boolean(res._header) 504 | : res.headersSent 505 | } 506 | 507 | /** 508 | * Pad number to two digits. 509 | * 510 | * @private 511 | * @param {number} num 512 | * @return {string} 513 | */ 514 | 515 | function pad2 (num) { 516 | var str = String(num) 517 | 518 | // istanbul ignore next: num is current datetime 519 | return (str.length === 1 ? '0' : '') + str 520 | } 521 | 522 | /** 523 | * Record the start time. 524 | * @private 525 | */ 526 | 527 | function recordStartTime () { 528 | this._startAt = process.hrtime() 529 | this._startTime = new Date() 530 | } 531 | 532 | /** 533 | * Define a token function with the given name, 534 | * and callback fn(req, res). 535 | * 536 | * @param {string} name 537 | * @param {function} fn 538 | * @public 539 | */ 540 | 541 | function token (name, fn) { 542 | morgan[name] = fn 543 | return this 544 | } 545 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "morgan", 3 | "description": "HTTP request logger middleware for node.js", 4 | "version": "1.10.0", 5 | "contributors": [ 6 | "Douglas Christopher Wilson ", 7 | "Jonathan Ong (http://jongleberry.com)" 8 | ], 9 | "license": "MIT", 10 | "keywords": [ 11 | "express", 12 | "http", 13 | "logger", 14 | "middleware" 15 | ], 16 | "repository": "expressjs/morgan", 17 | "dependencies": { 18 | "basic-auth": "~2.0.1", 19 | "debug": "2.6.9", 20 | "depd": "~2.0.0", 21 | "on-finished": "~2.3.0", 22 | "on-headers": "~1.0.2" 23 | }, 24 | "devDependencies": { 25 | "eslint": "6.8.0", 26 | "eslint-config-standard": "14.1.1", 27 | "eslint-plugin-import": "2.20.2", 28 | "eslint-plugin-markdown": "1.0.2", 29 | "eslint-plugin-node": "11.1.0", 30 | "eslint-plugin-promise": "4.2.1", 31 | "eslint-plugin-standard": "4.0.1", 32 | "mocha": "10.4.0", 33 | "nyc": "15.1.0", 34 | "split": "1.0.1", 35 | "supertest": "4.0.2" 36 | }, 37 | "files": [ 38 | "LICENSE", 39 | "HISTORY.md", 40 | "README.md", 41 | "index.js" 42 | ], 43 | "engines": { 44 | "node": ">= 0.8.0" 45 | }, 46 | "scripts": { 47 | "lint": "eslint --plugin markdown --ext js,md .", 48 | "test": "mocha --check-leaks --reporter spec --bail", 49 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 50 | "test-cov": "nyc --reporter=html --reporter=text npm test" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICwDCCAaigAwIBAgIJAJH0k+imlBVmMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMTCWxvY2FsaG9zdDAgFw0xODExMDUwMTM5NThaGA8yMDY4MTAyMzAxMzk1OFow 4 | FDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEA3nXOaU7907qgMBLKd4ZuffDPZGN11BmdLCZ68myDVde4N8kzWH7d3ggF 6 | puqcpGb0sSko7rpf1KHw1p02YEFLRyQigbMfUOSu72BR5aUpxVbG7C87qQ8YWE/8 7 | 3eiImDaDn5tMPnUTwfh0ITCWaW3/4u3PmQB2oJQo7tF5kOMqbY8tDPeO3lX9cx84 8 | O8BvCchgIPSFthasIRZXnrJicCH0boesA6XNKJbSfoD2hNmUVK8aFeySJ60SdAns 9 | DNuRJEINy1eEuyHobXleaSZrlQUSPdmqYXkPkrtzHReX63LUnlmxdzn4IZVN79Tu 10 | YlahulqoIEsaIaAxoGethhNZenytUQIDAQABoxMwETAPBgNVHREECDAGhwR/AAAB 11 | MA0GCSqGSIb3DQEBCwUAA4IBAQAYLS/7SV+4v3s8KLfOEtCdGw4w7IkBFi3LKKns 12 | RH74CbDZANJlI/Cc7YuZx2zP/0ux1t2VtVvvMexL4fkQsAsZtLHK5TZq6EzsAIYF 13 | jRXXhkSCfFvxxgj52PxsMD/3tRYZQ8OWyE1f3XL7rwXmeQxOvOuSvafGTXazrSS3 14 | fmwE53QTLv8Sepf8pTacOY1jQ7pfI4ND3/dOwuO4cc+ulELeCsQq17TEKpL1aXqN 15 | 5gGKe02u9Otq2E39+2FaI11IKUg3EUWKezFlHrxEr32pLPHMq/arVFUOApTgLekB 16 | UPGac0c4lowHqCXXQBQusihQtnY0JKbzwWnW4UBk5apGSHWR 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDedc5pTv3TuqAw 3 | Esp3hm598M9kY3XUGZ0sJnrybINV17g3yTNYft3eCAWm6pykZvSxKSjuul/UofDW 4 | nTZgQUtHJCKBsx9Q5K7vYFHlpSnFVsbsLzupDxhYT/zd6IiYNoOfm0w+dRPB+HQh 5 | MJZpbf/i7c+ZAHaglCju0XmQ4yptjy0M947eVf1zHzg7wG8JyGAg9IW2FqwhFlee 6 | smJwIfRuh6wDpc0oltJ+gPaE2ZRUrxoV7JInrRJ0CewM25EkQg3LV4S7IehteV5p 7 | JmuVBRI92apheQ+Su3MdF5frctSeWbF3OfghlU3v1O5iVqG6WqggSxohoDGgZ62G 8 | E1l6fK1RAgMBAAECggEAHKHG/lDXZI/pnCZe/sFDqVv8JWyTtsfRLeSKAHes87h/ 9 | ElcID8TMY45ew9wAazyBE+g7R3afbOum5sh3Pi5JNQ/WjSDzz+KPDWo1QDxgwvBn 10 | S/DMWfcCaCNrZVhPdF/X0wwW5RcGgvmqYLczNMCepaN8C7I+km5fUlWNsvM5+72r 11 | UwagsYzb4H6FeR5UnXDVoHXhFw/rIgiTqicv6Dg4y177Jev9RPlT9F+RKBVZE1+u 12 | vB+v66yoN1w2if+wBQICWnqKu0YK+gL31hiFjlbd2SlFcHDl3U41FDdhEk7im13B 13 | 1e400/Fu4MYEmWgGJj82Mm+FBs6jh+CbcjTj4G7trQKBgQD3jswnm9RU21YaNW8N 14 | mfEXFS8j/cuzJe8Va8swrPh/8L/4L6pdfjrfpA0wXq/SuIiz+xZgszfVlSD1Rf+t 15 | IcbW71OhahIRglvFRbqk8NYX6uzniWE4IpFBgt9o8HaI8dvfIJNUy3Pc2c6ZgzCc 16 | ZBQbwic65K7VHYVlZ+NANKgN7wKBgQDmC+d9OkPSvOvt7miD5AlUepfHmxDYz9Kf 17 | UezOqHsI4i8O0cr2PL+HPgOOEsjgB2IIZAcGSV/IqT0Ys4y1KLDU0KMgao//tPFp 18 | 4cN/SxgeaK6WLZP+OAvFi2UXpqUaeCCM/7KaaLoi/WoMqCLJcuJpO8MxsDikg/jp 19 | rNoOANI4vwKBgQCiBOdAlRAmaVa02HvSHwpW3Rp5J8WFfjI3htD5Dnuk4GADgs6x 20 | WcgWTjwDiDTyaKuvf4lpyGGme2+Slzl6ijyktwW5Ar7IjtSZC8XX5Xd5N9vMvXDP 21 | WHBQu+KTv60Ue5Y3Ng621GEEDdjVR7Ms56Lxd+RM+xYhjKydbZyhjNTgKQKBgA9K 22 | U0Sbjs5/CB90bTX2/jfDPjtiLyh6B8HXLCpAQI1Cm2Ycw6TCPOi8Ungq/3cEhpuQ 23 | KndcgSVROmJd7MhNwBMlGvKYoqGYYUNsYhYf46aBxrjspp2LFB05OqrrxKWRvngg 24 | trpUo6qXtWjJ9CX3oNzlv/+ZeupUa0L83jF4FID5AoGAE3Y42ixUqv86R8ZtK02z 25 | FA86hT01M2WW7YJyePWQ0/x64Ym3seTzutDFCtEpkAbt+lu/YaCRTz8/NjF0KYzN 26 | xJ4jjeiccbqslLuVMSfpY7LAm+udn+PsOpL5ezCzwgnFkZAVSKV+VDdENkQCtMaL 27 | zGVrPC/Lt2tJiLSllgFace0= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/morgan.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NO_DEPRECATION = 'morgan' 3 | 4 | var assert = require('assert') 5 | var fs = require('fs') 6 | var http = require('http') 7 | var https = require('https') 8 | var join = require('path').join 9 | var morgan = require('..') 10 | var request = require('supertest') 11 | var split = require('split') 12 | 13 | describe('morgan()', function () { 14 | describe('arguments', function () { 15 | it('should use default format', function (done) { 16 | var cb = after(2, function (err, res, line) { 17 | if (err) return done(err) 18 | assert(res.text.length > 0) 19 | assert.strictEqual(line.substr(0, res.text.length), res.text) 20 | done() 21 | }) 22 | 23 | var stream = createLineStream(function (line) { 24 | cb(null, null, line) 25 | }) 26 | 27 | request(createServer(undefined, { stream: stream })) 28 | .get('/') 29 | .expect(200, cb) 30 | }) 31 | 32 | describe('format', function () { 33 | it('should accept format as format name', function (done) { 34 | var cb = after(2, function (err, res, line) { 35 | if (err) return done(err) 36 | assert(/^GET \/ 200 - - \d+\.\d{3} ms$/.test(line)) 37 | done() 38 | }) 39 | 40 | var stream = createLineStream(function (line) { 41 | cb(null, null, line) 42 | }) 43 | 44 | request(createServer('tiny', { stream: stream })) 45 | .get('/') 46 | .expect(200, cb) 47 | }) 48 | 49 | it('should accept format as format string', function (done) { 50 | var cb = after(2, function (err, res, line) { 51 | if (err) return done(err) 52 | assert.strictEqual(line, 'GET /') 53 | done() 54 | }) 55 | 56 | var stream = createLineStream(function (line) { 57 | cb(null, null, line) 58 | }) 59 | 60 | request(createServer(':method :url', { stream: stream })) 61 | .get('/') 62 | .expect(200, cb) 63 | }) 64 | 65 | it('should accept format as function', function (done) { 66 | var cb = after(2, function (err, res, line) { 67 | if (err) return done(err) 68 | assert.strictEqual(line, 'GET / 200') 69 | done() 70 | }) 71 | 72 | var stream = createLineStream(function (line) { 73 | cb(null, null, line) 74 | }) 75 | 76 | function format (tokens, req, res) { 77 | return [req.method, req.url, res.statusCode].join(' ') 78 | } 79 | 80 | request(createServer(format, { stream: stream })) 81 | .get('/') 82 | .expect(200, cb) 83 | }) 84 | 85 | it('should reject format as bool', function () { 86 | assert.throws(createServer.bind(null, true), /argument format/) 87 | }) 88 | 89 | describe('back-compat', function () { 90 | it('should accept options object', function (done) { 91 | var cb = after(2, function (err, res, line) { 92 | if (err) return done(err) 93 | assert(res.text.length > 0) 94 | assert.strictEqual(line.substr(0, res.text.length), res.text) 95 | done() 96 | }) 97 | 98 | var stream = createLineStream(function (line) { 99 | cb(null, null, line) 100 | }) 101 | 102 | request(createServer({ stream: stream })) 103 | .get('/') 104 | .expect(200, cb) 105 | }) 106 | 107 | it('should accept format in options for back-compat', function (done) { 108 | var cb = after(2, function (err, res, line) { 109 | if (err) return done(err) 110 | assert.strictEqual(line, 'GET /') 111 | done() 112 | }) 113 | 114 | var stream = createLineStream(function (line) { 115 | cb(null, null, line) 116 | }) 117 | 118 | request(createServer({ format: ':method :url', stream: stream })) 119 | .get('/') 120 | .expect(200, cb) 121 | }) 122 | 123 | it('should accept format function in options for back-compat', function (done) { 124 | var cb = after(2, function (err, res, line) { 125 | if (err) return done(err) 126 | assert.strictEqual(line, 'apple') 127 | done() 128 | }) 129 | 130 | var stream = createLineStream(function (line) { 131 | cb(null, null, line) 132 | }) 133 | 134 | function format () { 135 | return 'apple' 136 | } 137 | 138 | request(createServer({ format: format, stream: stream })) 139 | .get('/') 140 | .expect(200, cb) 141 | }) 142 | }) 143 | }) 144 | 145 | describe('stream', function () { 146 | beforeEach(function () { 147 | this.stdout = process.stdout 148 | }) 149 | 150 | afterEach(function () { 151 | Object.defineProperty(process, 'stdout', { 152 | value: this.stdout 153 | }) 154 | }) 155 | 156 | it('should default to process.stdout', function (done) { 157 | var cb = after(2, function (err, res, line) { 158 | if (err) return done(err) 159 | assert(res.text.length > 0) 160 | assert.strictEqual(line.substr(0, res.text.length), res.text) 161 | done() 162 | }) 163 | 164 | var stream = createLineStream(function (line) { 165 | cb(null, null, line) 166 | }) 167 | 168 | Object.defineProperty(process, 'stdout', { 169 | value: stream 170 | }) 171 | 172 | request(createServer(undefined, { stream: undefined })) 173 | .get('/') 174 | .expect(200, cb) 175 | }) 176 | 177 | it('should set stream to write logs to', function (done) { 178 | var cb = after(2, function (err, res, line) { 179 | if (err) return done(err) 180 | assert(res.text.length > 0) 181 | assert.strictEqual(line.substr(0, res.text.length), res.text) 182 | done() 183 | }) 184 | 185 | var stream = createLineStream(function (line) { 186 | cb(null, null, line) 187 | }) 188 | 189 | request(createServer(undefined, { stream: stream })) 190 | .get('/') 191 | .expect(200, cb) 192 | }) 193 | }) 194 | }) 195 | 196 | describe('tokens', function () { 197 | describe(':date', function () { 198 | it('should get current date in "web" format by default', function (done) { 199 | var cb = after(2, function (err, res, line) { 200 | if (err) return done(err) 201 | assert.ok(/^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(line)) 202 | done() 203 | }) 204 | 205 | var stream = createLineStream(function (line) { 206 | cb(null, null, line) 207 | }) 208 | 209 | request(createServer(':date', { stream: stream })) 210 | .get('/') 211 | .expect(200, cb) 212 | }) 213 | 214 | it('should get current date in "clf" format', function (done) { 215 | var cb = after(2, function (err, res, line) { 216 | if (err) return done(err) 217 | assert.ok(/^\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} \+0000$/.test(line)) 218 | done() 219 | }) 220 | 221 | var stream = createLineStream(function (line) { 222 | cb(null, null, line) 223 | }) 224 | 225 | request(createServer(':date[clf]', { stream: stream })) 226 | .get('/') 227 | .expect(200, cb) 228 | }) 229 | 230 | it('should get current date in "iso" format', function (done) { 231 | var cb = after(2, function (err, res, line) { 232 | if (err) return done(err) 233 | assert.ok(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(line)) 234 | done() 235 | }) 236 | 237 | var stream = createLineStream(function (line) { 238 | cb(null, null, line) 239 | }) 240 | 241 | request(createServer(':date[iso]', { stream: stream })) 242 | .get('/') 243 | .expect(200, cb) 244 | }) 245 | 246 | it('should get current date in "web" format', function (done) { 247 | var cb = after(2, function (err, res, line) { 248 | if (err) return done(err) 249 | assert.ok(/^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(line)) 250 | done() 251 | }) 252 | 253 | var stream = createLineStream(function (line) { 254 | cb(null, null, line) 255 | }) 256 | 257 | request(createServer(':date[web]', { stream: stream })) 258 | .get('/') 259 | .expect(200, cb) 260 | }) 261 | 262 | it('should be blank for unknown format', function (done) { 263 | var cb = after(2, function (err, res, line) { 264 | if (err) return done(err) 265 | assert.strictEqual(line, '-') 266 | done() 267 | }) 268 | 269 | var stream = createLineStream(function (line) { 270 | cb(null, null, line) 271 | }) 272 | 273 | request(createServer(':date[bogus]', { stream: stream })) 274 | .get('/') 275 | .expect(200, cb) 276 | }) 277 | }) 278 | 279 | describe(':http-version', function () { 280 | it('should be 1.0 or 1.1', function (done) { 281 | var cb = after(2, function (err, res, line) { 282 | if (err) return done(err) 283 | assert.ok(/^1\.[01]$/.test(line)) 284 | done() 285 | }) 286 | 287 | var stream = createLineStream(function (line) { 288 | cb(null, null, line) 289 | }) 290 | 291 | request(createServer(':http-version', { stream: stream })) 292 | .get('/') 293 | .expect(200, cb) 294 | }) 295 | }) 296 | 297 | describe(':req', function () { 298 | it('should get request properties', function (done) { 299 | var cb = after(2, function (err, res, line) { 300 | if (err) return done(err) 301 | assert.strictEqual(line, 'me') 302 | done() 303 | }) 304 | 305 | var stream = createLineStream(function (line) { 306 | cb(null, null, line) 307 | }) 308 | 309 | request(createServer(':req[x-from-string]', { stream: stream })) 310 | .get('/') 311 | .set('x-from-string', 'me') 312 | .expect(200, cb) 313 | }) 314 | 315 | it('should display all values of array headers', function (done) { 316 | var cb = after(2, function (err, res, line) { 317 | if (err) return done(err) 318 | assert.strictEqual(line, 'foo=bar, fizz=buzz') 319 | done() 320 | }) 321 | 322 | var stream = createLineStream(function (line) { 323 | cb(null, null, line) 324 | }) 325 | 326 | request(createServer(':req[set-cookie]', { stream: stream })) 327 | .get('/') 328 | .set('Set-Cookie', ['foo=bar', 'fizz=buzz']) 329 | .expect(200, cb) 330 | }) 331 | }) 332 | 333 | describe(':res', function () { 334 | it('should get response properties', function (done) { 335 | var cb = after(2, function (err, res, line) { 336 | if (err) return done(err) 337 | assert.strictEqual(line, 'true') 338 | done() 339 | }) 340 | 341 | var stream = createLineStream(function (line) { 342 | cb(null, null, line) 343 | }) 344 | 345 | request(createServer(':res[x-sent]', { stream: stream })) 346 | .get('/') 347 | .expect(200, cb) 348 | }) 349 | 350 | it('should display all values of array headers', function (done) { 351 | var cb = after(2, function (err, res, line) { 352 | if (err) return done(err) 353 | assert.strictEqual(line, 'foo, bar') 354 | done() 355 | }) 356 | 357 | var stream = createLineStream(function (line) { 358 | cb(null, null, line) 359 | }) 360 | 361 | var server = createServer(':res[x-keys]', { stream: stream }, function (req, res, next) { 362 | res.setHeader('X-Keys', ['foo', 'bar']) 363 | next() 364 | }) 365 | 366 | request(server) 367 | .get('/') 368 | .expect('X-Keys', 'foo, bar') 369 | .expect(200, cb) 370 | }) 371 | }) 372 | 373 | describe(':remote-addr', function () { 374 | it('should get remote address', function (done) { 375 | var cb = after(2, function (err, res, line) { 376 | if (err) return done(err) 377 | assert.ok(res.text.length > 0) 378 | assert.strictEqual(line, res.text) 379 | done() 380 | }) 381 | 382 | var stream = createLineStream(function (line) { 383 | cb(null, null, line) 384 | }) 385 | 386 | request(createServer(':remote-addr', { stream: stream })) 387 | .get('/') 388 | .expect(200, cb) 389 | }) 390 | 391 | it('should use req.ip if there', function (done) { 392 | var cb = after(2, function (err, res, line) { 393 | if (err) return done(err) 394 | assert.strictEqual(line, '10.0.0.1') 395 | done() 396 | }) 397 | 398 | var stream = createLineStream(function (line) { 399 | cb(null, null, line) 400 | }) 401 | 402 | var server = createServer(':remote-addr', { stream: stream }, null, function (req) { 403 | req.ip = '10.0.0.1' 404 | }) 405 | 406 | request(server) 407 | .get('/') 408 | .expect(200, cb) 409 | }) 410 | 411 | it('should work on https server', function (done) { 412 | var cb = after(2, function (err, res, line) { 413 | if (err) return done(err) 414 | assert.ok(res.text.length > 0) 415 | assert.strictEqual(line, res.text) 416 | done() 417 | }) 418 | 419 | var stream = createLineStream(function (line) { 420 | cb(null, null, line) 421 | }) 422 | 423 | var server = createSecureServer(':remote-addr', { stream: stream }) 424 | 425 | request(server) 426 | .get('/') 427 | .ca(server.cert) 428 | .expect(200, cb) 429 | }) 430 | 431 | it('should work when connection: close', function (done) { 432 | var cb = after(2, function (err, res, line) { 433 | if (err) return done(err) 434 | assert.ok(res.text.length > 0) 435 | assert.strictEqual(line, res.text) 436 | done() 437 | }) 438 | 439 | var stream = createLineStream(function (line) { 440 | cb(null, null, line) 441 | }) 442 | 443 | request(createServer(':remote-addr', { stream: stream })) 444 | .get('/') 445 | .set('Connection', 'close') 446 | .expect(200, cb) 447 | }) 448 | 449 | it('should work when connection: keep-alive', function (done) { 450 | var cb = after(2, function (err, res, line) { 451 | if (err) return done(err) 452 | assert.ok(res.text.length > 0) 453 | assert.strictEqual(line, res.text) 454 | 455 | res.req.connection.destroy() 456 | server.close(done) 457 | }) 458 | 459 | var stream = createLineStream(function (line) { 460 | cb(null, null, line) 461 | }) 462 | 463 | var server = createServer(':remote-addr', { stream: stream }, function (req, res, next) { 464 | delete req._remoteAddress 465 | next() 466 | }) 467 | 468 | request(server.listen()) 469 | .get('/') 470 | .set('Connection', 'keep-alive') 471 | .expect(200, cb) 472 | }) 473 | 474 | it('should work when req.ip is a getter', function (done) { 475 | var cb = after(2, function (err, res, line) { 476 | if (err) return done(err) 477 | assert.strictEqual(line, '10.0.0.1') 478 | done() 479 | }) 480 | 481 | var stream = createLineStream(function (line) { 482 | cb(null, null, line) 483 | }) 484 | 485 | var server = createServer(':remote-addr', { stream: stream }, null, function (req) { 486 | Object.defineProperty(req, 'ip', { 487 | get: function () { return req.connection.remoteAddress ? '10.0.0.1' : undefined } 488 | }) 489 | }) 490 | 491 | request(server) 492 | .get('/') 493 | .set('Connection', 'close') 494 | .expect(200, cb) 495 | }) 496 | 497 | it('should not fail if req.connection missing', function (done) { 498 | var cb = after(2, function (err, res, line) { 499 | if (err) return done(err) 500 | assert.ok(res.text.length > 0) 501 | assert.strictEqual(line, res.text) 502 | 503 | res.req.connection.destroy() 504 | server.close(done) 505 | }) 506 | 507 | var stream = createLineStream(function (line) { 508 | cb(null, null, line) 509 | }) 510 | 511 | var server = createServer(':remote-addr', { stream: stream }, null, function (req) { 512 | delete req.connection 513 | }) 514 | 515 | request(server.listen()) 516 | .get('/') 517 | .set('Connection', 'keep-alive') 518 | .expect(200, cb) 519 | }) 520 | }) 521 | 522 | describe(':remote-user', function () { 523 | it('should be empty if none present', function (done) { 524 | var cb = after(2, function (err, res, line) { 525 | if (err) return done(err) 526 | assert.strictEqual(line, '-') 527 | done() 528 | }) 529 | 530 | var stream = createLineStream(function (line) { 531 | cb(null, null, line) 532 | }) 533 | 534 | request(createServer(':remote-user', { stream: stream })) 535 | .get('/') 536 | .expect(200, cb) 537 | }) 538 | 539 | it('should support Basic authorization', function (done) { 540 | var cb = after(2, function (err, res, line) { 541 | if (err) return done(err) 542 | assert.strictEqual(line, 'tj') 543 | done() 544 | }) 545 | 546 | var stream = createLineStream(function (line) { 547 | cb(null, null, line) 548 | }) 549 | 550 | request(createServer(':remote-user', { stream: stream })) 551 | .get('/') 552 | .set('Authorization', 'Basic dGo6') 553 | .expect(200, cb) 554 | }) 555 | 556 | it('should be empty for empty Basic authorization user', function (done) { 557 | var cb = after(2, function (err, res, line) { 558 | if (err) return done(err) 559 | assert.strictEqual(line, '-') 560 | done() 561 | }) 562 | 563 | var stream = createLineStream(function (line) { 564 | cb(null, null, line) 565 | }) 566 | 567 | request(createServer(':remote-user', { stream: stream })) 568 | .get('/') 569 | .set('Authorization', 'Basic Og==') 570 | .expect(200, cb) 571 | }) 572 | }) 573 | 574 | describe(':response-time', function () { 575 | it('should be in milliseconds', function (done) { 576 | var cb = after(2, function (err, res, line) { 577 | if (err) return done(err) 578 | var end = Date.now() 579 | var ms = parseFloat(line) 580 | assert(ms > 0) 581 | assert(ms < end - start + 1) 582 | done() 583 | }) 584 | 585 | var stream = createLineStream(function (line) { 586 | cb(null, null, line) 587 | }) 588 | 589 | var start = Date.now() 590 | 591 | request(createServer(':response-time', { stream: stream })) 592 | .get('/') 593 | .expect(200, cb) 594 | }) 595 | 596 | it('should have three digits by default', function (done) { 597 | var cb = after(2, function (err, res, line) { 598 | if (err) return done(err) 599 | assert.ok(/^[0-9]+\.[0-9]{3}$/.test(line)) 600 | done() 601 | }) 602 | 603 | var stream = createLineStream(function (line) { 604 | cb(null, null, line) 605 | }) 606 | 607 | request(createServer(':response-time', { stream: stream })) 608 | .get('/') 609 | .expect(200, cb) 610 | }) 611 | 612 | it('should have five digits with argument "5"', function (done) { 613 | var cb = after(2, function (err, res, line) { 614 | if (err) return done(err) 615 | assert.ok(/^[0-9]+\.[0-9]{5}$/.test(line)) 616 | done() 617 | }) 618 | 619 | var stream = createLineStream(function (line) { 620 | cb(null, null, line) 621 | }) 622 | 623 | request(createServer(':response-time[5]', { stream: stream })) 624 | .get('/') 625 | .expect(200, cb) 626 | }) 627 | 628 | it('should have no digits with argument "0"', function (done) { 629 | var cb = after(2, function (err, res, line) { 630 | if (err) return done(err) 631 | assert.ok(/^[0-9]+$/.test(line)) 632 | done() 633 | }) 634 | 635 | var stream = createLineStream(function (line) { 636 | cb(null, null, line) 637 | }) 638 | 639 | request(createServer(':response-time[0]', { stream: stream })) 640 | .get('/') 641 | .expect(200, cb) 642 | }) 643 | 644 | it('should not include response write time', function (done) { 645 | var cb = after(2, function (err, res, line) { 646 | if (err) return done(err) 647 | var end = Date.now() 648 | var ms = parseFloat(line) 649 | assert(ms > 0) 650 | assert(ms < end - start + 1) 651 | assert(ms < write - start + 1) 652 | done() 653 | }) 654 | 655 | var stream = createLineStream(function (line) { 656 | cb(null, null, line) 657 | }) 658 | 659 | var server = createServer(':response-time', { stream: stream }, function (req, res) { 660 | res.write('hello, ') 661 | write = Date.now() 662 | 663 | setTimeout(function () { 664 | res.end('world!') 665 | }, 50) 666 | }) 667 | 668 | var start = Date.now() 669 | var write = null 670 | 671 | request(server) 672 | .get('/') 673 | .expect(200, cb) 674 | }) 675 | 676 | it('should be empty without hidden property', function (done) { 677 | var cb = after(2, function (err, res, line) { 678 | if (err) return done(err) 679 | assert.strictEqual(line, '-') 680 | done() 681 | }) 682 | 683 | var stream = createLineStream(function (line) { 684 | cb(null, null, line) 685 | }) 686 | 687 | var server = createServer(':response-time', { stream: stream }, function (req, res, next) { 688 | delete req._startAt 689 | next() 690 | }) 691 | 692 | request(server) 693 | .get('/') 694 | .expect(200, cb) 695 | }) 696 | 697 | it('should be empty before response', function (done) { 698 | var cb = after(2, function (err, res, line) { 699 | if (err) return done(err) 700 | assert.strictEqual(line, '-') 701 | done() 702 | }) 703 | 704 | var stream = createLineStream(function (line) { 705 | cb(null, null, line) 706 | }) 707 | 708 | var server = createServer(':response-time', { 709 | immediate: true, 710 | stream: stream 711 | }) 712 | 713 | request(server) 714 | .get('/') 715 | .expect(200, cb) 716 | }) 717 | 718 | it('should be empty if morgan invoked after response sent', function (done) { 719 | var cb = after(3, function (err, res, line) { 720 | if (err) return done(err) 721 | assert.strictEqual(line, '-') 722 | done() 723 | }) 724 | 725 | var stream = createLineStream(function (line) { 726 | cb(null, null, line) 727 | }) 728 | 729 | var logger = morgan(':response-time', { 730 | immediate: true, 731 | stream: stream 732 | }) 733 | 734 | var server = http.createServer(function (req, res) { 735 | setTimeout(function () { 736 | logger(req, res, cb) 737 | }, 10) 738 | 739 | res.end() 740 | }) 741 | 742 | request(server) 743 | .get('/') 744 | .expect(200, cb) 745 | }) 746 | }) 747 | 748 | describe(':status', function () { 749 | it('should get response status', function (done) { 750 | var cb = after(2, function (err, res, line) { 751 | if (err) return done(err) 752 | assert.strictEqual(line, String(res.statusCode)) 753 | done() 754 | }) 755 | 756 | var stream = createLineStream(function (line) { 757 | cb(null, null, line) 758 | }) 759 | 760 | request(createServer(':status', { stream: stream })) 761 | .get('/') 762 | .expect(200, cb) 763 | }) 764 | 765 | it('should not exist before response sent', function (done) { 766 | var cb = after(2, function (err, res, line) { 767 | if (err) return done(err) 768 | assert.strictEqual(line, '-') 769 | done() 770 | }) 771 | 772 | var stream = createLineStream(function (line) { 773 | cb(null, null, line) 774 | }) 775 | 776 | var server = createServer(':status', { 777 | immediate: true, 778 | stream: stream 779 | }) 780 | 781 | request(server) 782 | .get('/') 783 | .expect(200, cb) 784 | }) 785 | 786 | it('should not exist for aborted request', function (done) { 787 | var stream = createLineStream(function (line) { 788 | assert.strictEqual(line, '-') 789 | server.close(done) 790 | }) 791 | 792 | var server = createServer(':status', { stream: stream }, function () { 793 | test.abort() 794 | }) 795 | 796 | var test = request(server).post('/') 797 | test.write('0') 798 | }) 799 | }) 800 | 801 | describe(':total-time', function () { 802 | it('should be in milliseconds', function (done) { 803 | var cb = after(2, function (err, res, line) { 804 | if (err) return done(err) 805 | var end = Date.now() 806 | var ms = parseFloat(line) 807 | assert(ms > 0) 808 | assert(ms < end - start + 1) 809 | done() 810 | }) 811 | 812 | var stream = createLineStream(function (line) { 813 | cb(null, null, line) 814 | }) 815 | 816 | var start = Date.now() 817 | 818 | request(createServer(':total-time', { stream: stream })) 819 | .get('/') 820 | .expect(200, cb) 821 | }) 822 | 823 | it('should have three digits by default', function (done) { 824 | var cb = after(2, function (err, res, line) { 825 | if (err) return done(err) 826 | assert.ok(/^[0-9]+\.[0-9]{3}$/.test(line)) 827 | done() 828 | }) 829 | 830 | var stream = createLineStream(function (line) { 831 | cb(null, null, line) 832 | }) 833 | 834 | request(createServer(':total-time', { stream: stream })) 835 | .get('/') 836 | .expect(200, cb) 837 | }) 838 | 839 | it('should have five digits with argument "5"', function (done) { 840 | var cb = after(2, function (err, res, line) { 841 | if (err) return done(err) 842 | assert.ok(/^[0-9]+\.[0-9]{5}$/.test(line)) 843 | done() 844 | }) 845 | 846 | var stream = createLineStream(function (line) { 847 | cb(null, null, line) 848 | }) 849 | 850 | request(createServer(':total-time[5]', { stream: stream })) 851 | .get('/') 852 | .expect(200, cb) 853 | }) 854 | 855 | it('should have no digits with argument "0"', function (done) { 856 | var cb = after(2, function (err, res, line) { 857 | if (err) return done(err) 858 | assert.ok(/^[0-9]+$/.test(line)) 859 | done() 860 | }) 861 | 862 | var stream = createLineStream(function (line) { 863 | cb(null, null, line) 864 | }) 865 | 866 | request(createServer(':total-time[0]', { stream: stream })) 867 | .get('/') 868 | .expect(200, cb) 869 | }) 870 | 871 | it('should include response write time', function (done) { 872 | var cb = after(2, function (err, res, line) { 873 | if (err) return done(err) 874 | var end = Date.now() 875 | var ms = parseFloat(line) 876 | assert(ms > 0) 877 | assert(ms > write - start - 1) 878 | assert(ms < end - start + 1) 879 | done() 880 | }) 881 | 882 | var stream = createLineStream(function (line) { 883 | cb(null, null, line) 884 | }) 885 | 886 | var server = createServer(':total-time', { stream: stream }, function (req, res) { 887 | res.write('hello, ') 888 | write = Date.now() 889 | 890 | setTimeout(function () { 891 | res.end('world!') 892 | }, 50) 893 | }) 894 | 895 | var start = Date.now() 896 | var write = null 897 | 898 | request(server) 899 | .get('/') 900 | .expect(200, cb) 901 | }) 902 | 903 | it('should be empty without hidden property', function (done) { 904 | var cb = after(2, function (err, res, line) { 905 | if (err) return done(err) 906 | assert.strictEqual(line, '-') 907 | done() 908 | }) 909 | 910 | var stream = createLineStream(function (line) { 911 | cb(null, null, line) 912 | }) 913 | 914 | var server = createServer(':total-time', { stream: stream }, function (req, res, next) { 915 | delete req._startAt 916 | next() 917 | }) 918 | 919 | request(server) 920 | .get('/') 921 | .expect(200, cb) 922 | }) 923 | 924 | it('should be empty before response', function (done) { 925 | var cb = after(2, function (err, res, line) { 926 | if (err) return done(err) 927 | assert.strictEqual(line, '-') 928 | done() 929 | }) 930 | 931 | var stream = createLineStream(function (line) { 932 | cb(null, null, line) 933 | }) 934 | 935 | var server = createServer(':total-time', { 936 | immediate: true, 937 | stream: stream 938 | }) 939 | 940 | request(server) 941 | .get('/') 942 | .expect(200, cb) 943 | }) 944 | 945 | it('should be empty if morgan invoked after response sent', function (done) { 946 | var cb = after(3, function (err, res, line) { 947 | if (err) return done(err) 948 | assert.strictEqual(line, '-') 949 | done() 950 | }) 951 | 952 | var stream = createLineStream(function (line) { 953 | cb(null, null, line) 954 | }) 955 | 956 | var logger = morgan(':total-time', { 957 | immediate: true, 958 | stream: stream 959 | }) 960 | 961 | var server = http.createServer(function (req, res) { 962 | setTimeout(function () { 963 | logger(req, res, cb) 964 | }, 10) 965 | 966 | res.end() 967 | }) 968 | 969 | request(server) 970 | .get('/') 971 | .expect(200, cb) 972 | }) 973 | }) 974 | 975 | describe(':url', function () { 976 | it('should get request URL', function (done) { 977 | var cb = after(2, function (err, res, line) { 978 | if (err) return done(err) 979 | assert.strictEqual(line, '/foo') 980 | done() 981 | }) 982 | 983 | var stream = createLineStream(function (line) { 984 | cb(null, null, line) 985 | }) 986 | 987 | request(createServer(':url', { stream: stream })) 988 | .get('/foo') 989 | .expect(200, cb) 990 | }) 991 | 992 | it('should use req.originalUrl if exists', function (done) { 993 | var cb = after(2, function (err, res, line) { 994 | if (err) return done(err) 995 | assert.strictEqual(line, '/bar') 996 | done() 997 | }) 998 | 999 | var stream = createLineStream(function (line) { 1000 | cb(null, null, line) 1001 | }) 1002 | 1003 | var server = createServer(':url', { stream: stream }, function (req, res, next) { 1004 | req.originalUrl = '/bar' 1005 | next() 1006 | }) 1007 | 1008 | request(server) 1009 | .get('/') 1010 | .expect(200, cb) 1011 | }) 1012 | 1013 | it('should not exist for aborted request', function (done) { 1014 | var stream = createLineStream(function (line) { 1015 | assert.strictEqual(line, '-') 1016 | server.close(done) 1017 | }) 1018 | 1019 | var server = createServer(':status', { stream: stream }, function () { 1020 | test.abort() 1021 | }) 1022 | 1023 | var test = request(server).post('/') 1024 | test.write('0') 1025 | }) 1026 | }) 1027 | }) 1028 | 1029 | describe('formats', function () { 1030 | describe('a function', function () { 1031 | it('should log result of function', function (done) { 1032 | var cb = after(2, function (err, res, line) { 1033 | if (err) return done(err) 1034 | assert.strictEqual(line, 'GET / 200') 1035 | done() 1036 | }) 1037 | 1038 | var stream = createLineStream(function (line) { 1039 | cb(null, null, line) 1040 | }) 1041 | 1042 | function format (tokens, req, res) { 1043 | return [req.method, req.url, res.statusCode].join(' ') 1044 | } 1045 | 1046 | request(createServer(format, { stream: stream })) 1047 | .get('/') 1048 | .expect(200, cb) 1049 | }) 1050 | 1051 | it('should not log for undefined return', function (done) { 1052 | var stream = createLineStream(function () { 1053 | throw new Error('should not log line') 1054 | }) 1055 | 1056 | function format (tokens, req, res) { 1057 | return undefined 1058 | } 1059 | 1060 | request(createServer(format, { stream: stream })) 1061 | .get('/') 1062 | .expect(200, done) 1063 | }) 1064 | 1065 | it('should not log for null return', function (done) { 1066 | var stream = createLineStream(function () { 1067 | throw new Error('should not log line') 1068 | }) 1069 | 1070 | function format (tokens, req, res) { 1071 | return null 1072 | } 1073 | 1074 | request(createServer(format, { stream: stream })) 1075 | .get('/') 1076 | .expect(200, done) 1077 | }) 1078 | }) 1079 | 1080 | describe('a string', function () { 1081 | it('should accept format as format string of tokens', function (done) { 1082 | var cb = after(2, function (err, res, line) { 1083 | if (err) return done(err) 1084 | assert.strictEqual(line, 'GET /') 1085 | done() 1086 | }) 1087 | 1088 | var stream = createLineStream(function (line) { 1089 | cb(null, null, line) 1090 | }) 1091 | 1092 | request(createServer(':method :url', { stream: stream })) 1093 | .get('/') 1094 | .expect(200, cb) 1095 | }) 1096 | 1097 | it('should accept text mixed with tokens', function (done) { 1098 | var cb = after(2, function (err, res, line) { 1099 | if (err) return done(err) 1100 | assert.strictEqual(line, 'method=GET url=/') 1101 | done() 1102 | }) 1103 | 1104 | var stream = createLineStream(function (line) { 1105 | cb(null, null, line) 1106 | }) 1107 | 1108 | request(createServer('method=:method url=:url', { stream: stream })) 1109 | .get('/') 1110 | .expect(200, cb) 1111 | }) 1112 | 1113 | it('should accept special characters', function (done) { 1114 | var cb = after(2, function (err, res, line) { 1115 | if (err) return done(err) 1116 | assert.strictEqual(line, 'LOCAL\\tobi "GET /" 200') 1117 | done() 1118 | }) 1119 | 1120 | var stream = createLineStream(function (line) { 1121 | cb(null, null, line) 1122 | }) 1123 | 1124 | request(createServer('LOCAL\\:remote-user ":method :url" :status', { stream: stream })) 1125 | .get('/') 1126 | .set('Authorization', 'Basic dG9iaTpsb2tp') 1127 | .expect(200, cb) 1128 | }) 1129 | }) 1130 | 1131 | describe('combined', function () { 1132 | it('should match expectations', function (done) { 1133 | var cb = after(2, function (err, res, line) { 1134 | if (err) return done(err) 1135 | var masked = line.replace(/\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} \+0000/, '_timestamp_') 1136 | assert.strictEqual(masked, res.text + ' - tj [_timestamp_] "GET / HTTP/1.1" 200 - "http://localhost/" "my-ua"') 1137 | done() 1138 | }) 1139 | 1140 | var stream = createLineStream(function (line) { 1141 | cb(null, null, line) 1142 | }) 1143 | 1144 | request(createServer('combined', { stream: stream })) 1145 | .get('/') 1146 | .set('Authorization', 'Basic dGo6') 1147 | .set('Referer', 'http://localhost/') 1148 | .set('User-Agent', 'my-ua') 1149 | .expect(200, cb) 1150 | }) 1151 | }) 1152 | 1153 | describe('common', function () { 1154 | it('should match expectations', function (done) { 1155 | var cb = after(2, function (err, res, line) { 1156 | if (err) return done(err) 1157 | var masked = line.replace(/\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2} \+0000/, '_timestamp_') 1158 | assert.strictEqual(masked, res.text + ' - tj [_timestamp_] "GET / HTTP/1.1" 200 -') 1159 | done() 1160 | }) 1161 | 1162 | var stream = createLineStream(function (line) { 1163 | cb(null, null, line) 1164 | }) 1165 | 1166 | request(createServer('common', { stream: stream })) 1167 | .get('/') 1168 | .set('Authorization', 'Basic dGo6') 1169 | .expect(200, cb) 1170 | }) 1171 | }) 1172 | 1173 | describe('default', function () { 1174 | it('should match expectations', function (done) { 1175 | var cb = after(2, function (err, res, line) { 1176 | if (err) return done(err) 1177 | var masked = line.replace(/\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+/, '_timestamp_') 1178 | assert.strictEqual(masked, res.text + ' - tj [_timestamp_] "GET / HTTP/1.1" 200 - "http://localhost/" "my-ua"') 1179 | done() 1180 | }) 1181 | 1182 | var stream = createLineStream(function (line) { 1183 | cb(null, null, line) 1184 | }) 1185 | 1186 | request(createServer('default', { stream: stream })) 1187 | .get('/') 1188 | .set('Authorization', 'Basic dGo6') 1189 | .set('Referer', 'http://localhost/') 1190 | .set('User-Agent', 'my-ua') 1191 | .expect(200, cb) 1192 | }) 1193 | }) 1194 | 1195 | describe('dev', function () { 1196 | it('should not color 1xx', function (done) { 1197 | var cb = after(2, function (err, res, line) { 1198 | if (err) return done(err) 1199 | assert.strictEqual(line.substr(0, 36), '_color_0_GET / _color_0_102_color_0_') 1200 | assert.strictEqual(line.substr(-9), '_color_0_') 1201 | done() 1202 | }) 1203 | 1204 | var stream = createColorLineStream(function onLine (line) { 1205 | cb(null, null, line) 1206 | }) 1207 | 1208 | var server = createServer('dev', { stream: stream }, function (req, res, next) { 1209 | res.statusCode = 102 1210 | next() 1211 | }) 1212 | 1213 | request(server) 1214 | .get('/') 1215 | .expect(102, function (err, res) { 1216 | if (err && err.code === 'ECONNRESET') { 1217 | // finishing response with 1xx is invalid http 1218 | // but node.js server lets the server do this, so 1219 | // morgan needs to test in this condition even if 1220 | // the http client doesn't like it 1221 | err = null 1222 | } 1223 | cb(err, res) 1224 | }) 1225 | }) 1226 | 1227 | it('should color 2xx green', function (done) { 1228 | var cb = after(2, function (err, res, line) { 1229 | if (err) return done(err) 1230 | assert.strictEqual(line.substr(0, 37), '_color_0_GET / _color_32_200_color_0_') 1231 | assert.strictEqual(line.substr(-9), '_color_0_') 1232 | done() 1233 | }) 1234 | 1235 | var stream = createColorLineStream(function onLine (line) { 1236 | cb(null, null, line) 1237 | }) 1238 | 1239 | var server = createServer('dev', { stream: stream }, function (req, res, next) { 1240 | res.statusCode = 200 1241 | next() 1242 | }) 1243 | 1244 | request(server) 1245 | .get('/') 1246 | .expect(200, cb) 1247 | }) 1248 | 1249 | it('should color 3xx cyan', function (done) { 1250 | var cb = after(2, function (err, res, line) { 1251 | if (err) return done(err) 1252 | assert.strictEqual(line.substr(0, 37), '_color_0_GET / _color_36_300_color_0_') 1253 | assert.strictEqual(line.substr(-9), '_color_0_') 1254 | done() 1255 | }) 1256 | 1257 | var stream = createColorLineStream(function onLine (line) { 1258 | cb(null, null, line) 1259 | }) 1260 | 1261 | var server = createServer('dev', { stream: stream }, function (req, res, next) { 1262 | res.statusCode = 300 1263 | next() 1264 | }) 1265 | 1266 | request(server) 1267 | .get('/') 1268 | .expect(300, cb) 1269 | }) 1270 | 1271 | it('should color 4xx yelow', function (done) { 1272 | var cb = after(2, function (err, res, line) { 1273 | if (err) return done(err) 1274 | assert.strictEqual(line.substr(0, 37), '_color_0_GET / _color_33_400_color_0_') 1275 | assert.strictEqual(line.substr(-9), '_color_0_') 1276 | done() 1277 | }) 1278 | 1279 | var stream = createColorLineStream(function onLine (line) { 1280 | cb(null, null, line) 1281 | }) 1282 | 1283 | var server = createServer('dev', { stream: stream }, function (req, res, next) { 1284 | res.statusCode = 400 1285 | next() 1286 | }) 1287 | 1288 | request(server) 1289 | .get('/') 1290 | .expect(400, cb) 1291 | }) 1292 | 1293 | it('should color 5xx red', function (done) { 1294 | var cb = after(2, function (err, res, line) { 1295 | if (err) return done(err) 1296 | assert.strictEqual(line.substr(0, 37), '_color_0_GET / _color_31_500_color_0_') 1297 | assert.strictEqual(line.substr(-9), '_color_0_') 1298 | done() 1299 | }) 1300 | 1301 | var stream = createColorLineStream(function onLine (line) { 1302 | cb(null, null, line) 1303 | }) 1304 | 1305 | var server = createServer('dev', { stream: stream }, function (req, res, next) { 1306 | res.statusCode = 500 1307 | next() 1308 | }) 1309 | 1310 | request(server) 1311 | .get('/') 1312 | .expect(500, cb) 1313 | }) 1314 | 1315 | describe('with "immediate: true" option', function () { 1316 | it('should not have color or response values', function (done) { 1317 | var cb = after(2, function (err, res, line) { 1318 | if (err) return done(err) 1319 | assert.strictEqual(line, '_color_0_GET / _color_0_-_color_0_ - ms - -_color_0_') 1320 | done() 1321 | }) 1322 | 1323 | var stream = createColorLineStream(function onLine (line) { 1324 | cb(null, null, line) 1325 | }) 1326 | 1327 | var server = createServer('dev', { 1328 | immediate: true, 1329 | stream: stream 1330 | }) 1331 | 1332 | request(server) 1333 | .get('/') 1334 | .expect(200, cb) 1335 | }) 1336 | }) 1337 | }) 1338 | 1339 | describe('short', function () { 1340 | it('should match expectations', function (done) { 1341 | var cb = after(2, function (err, res, line) { 1342 | if (err) return done(err) 1343 | var masked = line.replace(/\d+\.\d{3} ms/, '_timer_') 1344 | assert.strictEqual(masked, res.text + ' tj GET / HTTP/1.1 200 - - _timer_') 1345 | done() 1346 | }) 1347 | 1348 | var stream = createLineStream(function (line) { 1349 | cb(null, null, line) 1350 | }) 1351 | 1352 | request(createServer('short', { stream: stream })) 1353 | .get('/') 1354 | .set('Authorization', 'Basic dGo6') 1355 | .expect(200, cb) 1356 | }) 1357 | }) 1358 | 1359 | describe('tiny', function () { 1360 | it('should match expectations', function (done) { 1361 | var cb = after(2, function (err, res, line) { 1362 | if (err) return done(err) 1363 | var masked = line.replace(/\d+\.\d{3} ms/, '_timer_') 1364 | assert.strictEqual(masked, 'GET / 200 - - _timer_') 1365 | done() 1366 | }) 1367 | 1368 | var stream = createLineStream(function (line) { 1369 | cb(null, null, line) 1370 | }) 1371 | 1372 | request(createServer('tiny', { stream: stream })) 1373 | .get('/') 1374 | .expect(200, cb) 1375 | }) 1376 | }) 1377 | }) 1378 | 1379 | describe('with buffer option', function () { 1380 | it('should flush log periodically', function (done) { 1381 | var cb = after(2, function (err, res, log) { 1382 | if (err) return done(err) 1383 | assert.strictEqual(log, 'GET /first\nGET /second\n') 1384 | assert.ok(Date.now() - time >= 1000) 1385 | assert.ok(Date.now() - time <= 1100) 1386 | done() 1387 | }) 1388 | var server = createServer(':method :url', { 1389 | buffer: true, 1390 | stream: { write: writeLog } 1391 | }) 1392 | var time = Date.now() 1393 | 1394 | function writeLog (log) { 1395 | cb(null, null, log) 1396 | } 1397 | 1398 | request(server) 1399 | .get('/first') 1400 | .expect(200, function (err) { 1401 | if (err) return cb(err) 1402 | request(server) 1403 | .get('/second') 1404 | .expect(200, cb) 1405 | }) 1406 | }) 1407 | 1408 | it('should accept custom interval', function (done) { 1409 | var cb = after(2, function (err, res, log) { 1410 | if (err) return done(err) 1411 | assert.strictEqual(log, 'GET /first\nGET /second\n') 1412 | assert.ok(Date.now() - time >= 200) 1413 | assert.ok(Date.now() - time <= 300) 1414 | done() 1415 | }) 1416 | var server = createServer(':method :url', { 1417 | buffer: 200, 1418 | stream: { write: writeLog } 1419 | }) 1420 | var time = Date.now() 1421 | 1422 | function writeLog (log) { 1423 | cb(null, null, log) 1424 | } 1425 | 1426 | request(server) 1427 | .get('/first') 1428 | .expect(200, function (err) { 1429 | if (err) return cb(err) 1430 | request(server) 1431 | .get('/second') 1432 | .expect(200, cb) 1433 | }) 1434 | }) 1435 | }) 1436 | 1437 | describe('with immediate option', function () { 1438 | it('should not have value for :res', function (done) { 1439 | var cb = after(2, function (err, res, line) { 1440 | if (err) return done(err) 1441 | assert.strictEqual(line, 'GET / -') 1442 | done() 1443 | }) 1444 | 1445 | var stream = createLineStream(function (line) { 1446 | cb(null, null, line) 1447 | }) 1448 | 1449 | var server = createServer(':method :url :res[x-sent]', { 1450 | immediate: true, 1451 | stream: stream 1452 | }) 1453 | 1454 | request(server) 1455 | .get('/') 1456 | .expect(200, cb) 1457 | }) 1458 | 1459 | it('should not have value for :response-time', function (done) { 1460 | var cb = after(2, function (err, res, line) { 1461 | if (err) return done(err) 1462 | assert.strictEqual(line, 'GET / -') 1463 | done() 1464 | }) 1465 | 1466 | var stream = createLineStream(function (line) { 1467 | cb(null, null, line) 1468 | }) 1469 | 1470 | var server = createServer(':method :url :response-time', { 1471 | immediate: true, 1472 | stream: stream 1473 | }) 1474 | 1475 | request(server) 1476 | .get('/') 1477 | .expect(200, cb) 1478 | }) 1479 | 1480 | it('should not have value for :status', function (done) { 1481 | var cb = after(2, function (err, res, line) { 1482 | if (err) return done(err) 1483 | assert.strictEqual(line, 'GET / -') 1484 | done() 1485 | }) 1486 | 1487 | var stream = createLineStream(function (line) { 1488 | cb(null, null, line) 1489 | }) 1490 | 1491 | var server = createServer(':method :url :status', { 1492 | immediate: true, 1493 | stream: stream 1494 | }) 1495 | 1496 | request(server) 1497 | .get('/') 1498 | .expect(200, cb) 1499 | }) 1500 | 1501 | it('should log before response', function (done) { 1502 | var lineLogged = false 1503 | var cb = after(2, function (err, res, line) { 1504 | if (err) return done(err) 1505 | assert.strictEqual(line, 'GET / -') 1506 | done() 1507 | }) 1508 | 1509 | var stream = createLineStream(function (line) { 1510 | lineLogged = true 1511 | cb(null, null, line) 1512 | }) 1513 | 1514 | var server = createServer(':method :url :res[x-sent]', { immediate: true, stream: stream }, function (req, res, next) { 1515 | assert.ok(lineLogged) 1516 | next() 1517 | }) 1518 | 1519 | request(server) 1520 | .get('/') 1521 | .expect(200, cb) 1522 | }) 1523 | }) 1524 | 1525 | describe('with skip option', function () { 1526 | it('should be able to skip based on request', function (done) { 1527 | var stream = createLineStream(function () { 1528 | throw new Error('should not log line') 1529 | }) 1530 | 1531 | function skip (req) { 1532 | return req.url.indexOf('skip=true') !== -1 1533 | } 1534 | 1535 | request(createServer({ format: 'default', skip: skip, stream: stream })) 1536 | .get('/?skip=true') 1537 | .set('Connection', 'close') 1538 | .expect(200, done) 1539 | }) 1540 | 1541 | it('should be able to skip based on response', function (done) { 1542 | var stream = createLineStream(function () { 1543 | throw new Error('should not log line') 1544 | }) 1545 | 1546 | function skip (req, res) { 1547 | return res.statusCode === 200 1548 | } 1549 | 1550 | request(createServer({ format: 'default', skip: skip, stream: stream })) 1551 | .get('/') 1552 | .expect(200, done) 1553 | }) 1554 | }) 1555 | }) 1556 | 1557 | describe('morgan.compile(format)', function () { 1558 | describe('arguments', function () { 1559 | describe('format', function () { 1560 | it('should be required', function () { 1561 | assert.throws(morgan.compile.bind(morgan), /argument format/) 1562 | }) 1563 | 1564 | it('should reject functions', function () { 1565 | assert.throws(morgan.compile.bind(morgan, function () {}), /argument format/) 1566 | }) 1567 | 1568 | it('should reject numbers', function () { 1569 | assert.throws(morgan.compile.bind(morgan, 42), /argument format/) 1570 | }) 1571 | 1572 | it('should compile a string into a function', function () { 1573 | var fn = morgan.compile(':method') 1574 | assert.ok(typeof fn === 'function') 1575 | assert.ok(fn.length === 3) 1576 | }) 1577 | }) 1578 | }) 1579 | }) 1580 | 1581 | function after (count, callback) { 1582 | var args = new Array(3) 1583 | var i = 0 1584 | 1585 | return function (err, arg1, arg2) { 1586 | assert.ok(i++ < count, 'callback called ' + count + ' times') 1587 | 1588 | args[0] = args[0] || err 1589 | args[1] = args[1] || arg1 1590 | args[2] = args[2] || arg2 1591 | 1592 | if (count === i) { 1593 | callback.apply(null, args) 1594 | } 1595 | } 1596 | } 1597 | 1598 | function createColorLineStream (callback) { 1599 | return createLineStream(function onLine (line) { 1600 | callback(expandColorCharacters(line)) 1601 | }) 1602 | } 1603 | 1604 | function createLineStream (callback) { 1605 | return split().on('data', callback) 1606 | } 1607 | 1608 | function createRequestListener (format, opts, fn, fn1) { 1609 | var logger = morgan(format, opts) 1610 | var middle = fn || noopMiddleware 1611 | 1612 | return function onRequest (req, res) { 1613 | // prior alterations 1614 | if (fn1) { 1615 | fn1(req, res) 1616 | } 1617 | 1618 | logger(req, res, function onNext (err) { 1619 | // allow req, res alterations 1620 | middle(req, res, function onDone () { 1621 | if (err) { 1622 | res.statusCode = 500 1623 | res.end(err.message) 1624 | } 1625 | 1626 | res.setHeader('X-Sent', 'true') 1627 | res.end((req.connection && req.connection.remoteAddress) || '-') 1628 | }) 1629 | }) 1630 | } 1631 | } 1632 | 1633 | function createSecureServer (format, opts, fn, fn1) { 1634 | var cert = fs.readFileSync(join(__dirname, 'fixtures', 'server.crt'), 'ascii') 1635 | var key = fs.readFileSync(join(__dirname, 'fixtures', 'server.key'), 'ascii') 1636 | 1637 | return https.createServer({ cert: cert, key: key }) 1638 | .on('request', createRequestListener(format, opts, fn, fn1)) 1639 | } 1640 | 1641 | function createServer (format, opts, fn, fn1) { 1642 | return http.createServer() 1643 | .on('request', createRequestListener(format, opts, fn, fn1)) 1644 | } 1645 | 1646 | function expandColorCharacters (str) { 1647 | // eslint-disable-next-line no-control-regex 1648 | return str.replace(/\x1b\[(\d+)m/g, '_color_$1_') 1649 | } 1650 | 1651 | function noopMiddleware (req, res, next) { 1652 | next() 1653 | } 1654 | --------------------------------------------------------------------------------