diff --git a/.travis.yml b/.travis.yml index 89a57ef0ccdd..9a5bac3a5aff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -93,6 +93,16 @@ matrix: # - npm run build -- --scope "@loopback/test-repository-cloudant" --include-dependencies # - cd acceptance/repository-cloudant && npm run docker:setup # - source cloudant-config.sh && npm run mocha + - node_js: "12" + os: linux + env: + - TASK=test-extension-logging-fluentd + services: + - docker + script: + - npm run postinstall -- --scope "@loopback/test-extension-logging-fluentd" --include-dependencies + - npm run build -- --scope "@loopback/test-extension-logging-fluentd" --include-dependencies + - cd acceptance/extension-logging-fluentd && npm run mocha branches: only: diff --git a/CODEOWNERS b/CODEOWNERS index 6809ff0b51aa..4d515ab8c932 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,8 @@ packages/testlab/* @bajtos packages/tsdocs/* @raymondfeng extensions/health/* @raymondfeng +extensions/logging/* @raymondfeng +extensions/metrics/* @raymondfeng examples/todo/* @bajtos @hacksparrow examples/context/* @raymondfeng diff --git a/acceptance/extension-logging-fluentd/.npmrc b/acceptance/extension-logging-fluentd/.npmrc new file mode 100644 index 000000000000..cafe685a112d --- /dev/null +++ b/acceptance/extension-logging-fluentd/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/acceptance/extension-logging-fluentd/LICENSE b/acceptance/extension-logging-fluentd/LICENSE new file mode 100644 index 000000000000..2002764c6998 --- /dev/null +++ b/acceptance/extension-logging-fluentd/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017,2019. All Rights Reserved. +Node module: @loopback/test-extension-logging-fluentd +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/acceptance/extension-logging-fluentd/README.md b/acceptance/extension-logging-fluentd/README.md new file mode 100644 index 000000000000..84c58b223679 --- /dev/null +++ b/acceptance/extension-logging-fluentd/README.md @@ -0,0 +1,30 @@ +# @loopback/test-extension-logging-fluentd + +Acceptance tests for `@loopback/extension-logging` against a fluentd server. + +## Running the test suite + +### Prerequisite + +- [Docker](https://docs.docker.com/engine/installation/) is installed. + +### Run the test: + +For mocha tests, we use +[testcontainers](https://github.com/testcontainers/testcontainers-node) to +start/stop the fluentd docker container automatically. + +The fluentd configuration is read from `fixtures/etc/fluentd.conf`. + +```bash +npm test +``` + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/acceptance/extension-logging-fluentd/fixtures/etc/fluentd.conf b/acceptance/extension-logging-fluentd/fixtures/etc/fluentd.conf new file mode 100644 index 000000000000..5c0449d411bb --- /dev/null +++ b/acceptance/extension-logging-fluentd/fixtures/etc/fluentd.conf @@ -0,0 +1,18 @@ + + @type http + port 9880 + bind 0.0.0.0 + + + @type forward + port 24224 + + + @type file + path /fluentd/log/ + + timekey 1d + timekey_use_utc true + timekey_wait 10m + + diff --git a/acceptance/extension-logging-fluentd/fixtures/start-fluentd.sh b/acceptance/extension-logging-fluentd/fixtures/start-fluentd.sh new file mode 100755 index 000000000000..5ed433018f17 --- /dev/null +++ b/acceptance/extension-logging-fluentd/fixtures/start-fluentd.sh @@ -0,0 +1,18 @@ +#!/bin/bash +SCRIPT_NAME=${BASH_SOURCE[0]} +BASE_DIR=`dirname "${SCRIPT_NAME}"` +$BASE_DIR/stop-fluentd.sh + +FLUENTD__CONTAINER_NAME="fluentd_lb4" +pushd $BASE_DIR >/dev/null +ROOT_DIR=$(pwd) +rm -rf $ROOT_DIR/logs/* +docker pull fluent/fluentd:v1.7.4-debian-1.0 +docker run --name $FLUENTD__CONTAINER_NAME -d \ + -p 24224:24224 -p 9880:9880 -v $ROOT_DIR/etc:/fluentd/etc \ + -e FLUENTD_CONF=fluentd.conf fluent/fluentd +popd >/dev/null + +export FLUENTD_SERVICE_HOST=127.0.0.1 +export FLUENTD_SERVICE_PORT_TCP=24224 +export FLUENTD_SERVICE_PORT_HTTP=9880 diff --git a/acceptance/extension-logging-fluentd/fixtures/stop-fluentd.sh b/acceptance/extension-logging-fluentd/fixtures/stop-fluentd.sh new file mode 100755 index 000000000000..ec3f28117eaf --- /dev/null +++ b/acceptance/extension-logging-fluentd/fixtures/stop-fluentd.sh @@ -0,0 +1,3 @@ +#!/bin/bash +FLUENTD__CONTAINER_NAME="fluentd_lb4" +docker rm -f $FLUENTD__CONTAINER_NAME >/dev/null 2>&1 diff --git a/acceptance/extension-logging-fluentd/index.d.ts b/acceptance/extension-logging-fluentd/index.d.ts new file mode 100644 index 000000000000..cd72d0ceb8c6 --- /dev/null +++ b/acceptance/extension-logging-fluentd/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/test-extension-logging-fluentd +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/acceptance/extension-logging-fluentd/index.js b/acceptance/extension-logging-fluentd/index.js new file mode 100644 index 000000000000..922391a5d639 --- /dev/null +++ b/acceptance/extension-logging-fluentd/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/test-extension-logging-fluentd +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/acceptance/extension-logging-fluentd/package-lock.json b/acceptance/extension-logging-fluentd/package-lock.json new file mode 100644 index 000000000000..a0bd497f7d8d --- /dev/null +++ b/acceptance/extension-logging-fluentd/package-lock.json @@ -0,0 +1,582 @@ +{ + "name": "@loopback/test-extension-logging-fluentd", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "dev": true + }, + "JSONStream": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", + "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "dev": true + }, + "chownr": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "default-gateway": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-5.0.5.tgz", + "integrity": "sha512-z2RnruVmj8hVMmAnEJMTIJNijhKCDiGjbLP+BHJFOT7ld3Bo5qcIBpVYDniqhbMIIf+jZDlkP2MkPXiQy/DBLA==", + "dev": true, + "requires": { + "execa": "^3.3.0" + } + }, + "docker-modem": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.9.tgz", + "integrity": "sha512-lVjqCSCIAUDZPAZIeyM125HXfNvOmYYInciphNrLrylUtKyW66meAjSPXWchKVzoIYZx69TPnAepVSSkeawoIw==", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "debug": "^3.2.6", + "readable-stream": "~1.0.26-4", + "split-ca": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "dockerode": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.8.tgz", + "integrity": "sha512-+7iOUYBeDTScmOmQqpUYQaE7F4vvIt6+gIZNHWhqAQEI887tiPFB9OvXI/HzQYqfUNvukMK+9myLW63oTJPZpw==", + "dev": true, + "requires": { + "concat-stream": "~1.6.2", + "docker-modem": "^1.0.8", + "tar-fs": "~1.16.3" + }, + "dependencies": { + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "tar-fs": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "dev": true, + "requires": { + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "dev": true + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node-duration": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-duration/-/node-duration-1.0.4.tgz", + "integrity": "sha512-eUXYNSY7DL53vqfTosggWkvyIW3bhAcqBDIlolgNYlZhianXTrCL50rlUJWD1eRqkIxMppXTfiFbp+9SjpPrgA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=", + "dev": true + }, + "stream-to-array": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", + "integrity": "sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M=", + "dev": true, + "requires": { + "any-promise": "^1.1.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "tar-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz", + "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp": "^0.5.1", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + }, + "dependencies": { + "bl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.0.tgz", + "integrity": "sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==", + "dev": true, + "requires": { + "readable-stream": "^3.0.1" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-stream": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.0.tgz", + "integrity": "sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==", + "dev": true, + "requires": { + "bl": "^3.0.0", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "testcontainers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-2.2.0.tgz", + "integrity": "sha512-SFj2LVj+MWA7YNQ2YS+istdW2P+NSZLHeVFH6Lsp4h8ZQy+fH+lEk7JXcNLt8j54M6zUDXzmQLTSCRpuCZ9v7Q==", + "dev": true, + "requires": { + "byline": "^5.0.0", + "debug": "^4.1.1", + "default-gateway": "^5.0.2", + "dockerode": "^2.5.8", + "get-port": "^4.2.0", + "node-duration": "^1.0.4", + "stream-to-array": "^2.3.0", + "tar-fs": "^2.0.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + } + } +} diff --git a/acceptance/extension-logging-fluentd/package.json b/acceptance/extension-logging-fluentd/package.json new file mode 100644 index 000000000000..97e51e135dd7 --- /dev/null +++ b/acceptance/extension-logging-fluentd/package.json @@ -0,0 +1,42 @@ +{ + "name": "@loopback/test-extension-logging-fluentd", + "version": "0.0.1", + "description": "Acceptance test for extension-logging with fluentd", + "private": true, + "engines": { + "node": ">=8.9" + }, + "scripts": { + "build": "lb-tsc", + "clean": "lb-clean loopback-test-extension-logging-fluentd*.tgz dist tsconfig.build.tsbuildinfo package", + "pretest": "npm run build", + "test": "npm run mocha", + "mocha": "lb-mocha \"dist/__tests__/**/*.js\"", + "verify": "npm pack && tar xf loopback-test-extension-logging-fluentd*.tgz && tree package && npm run clean" + }, + "author": "IBM Corp.", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "devDependencies": { + "@loopback/core": "^1.10.5", + "@loopback/testlab": "^1.9.2", + "@loopback/build": "^2.0.16", + "@loopback/eslint-config": "^4.1.4", + "@loopback/extension-logging": "^0.0.1", + "testcontainers": "^2.1.0", + "@types/node": "^10.17.5" + }, + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist", + "src", + "!*/__tests__" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git", + "directory": "acceptance/extension-logging-fluentd" + } +} diff --git a/acceptance/extension-logging-fluentd/src/__tests__/accpetance/fluent.acceptance.ts b/acceptance/extension-logging-fluentd/src/__tests__/accpetance/fluent.acceptance.ts new file mode 100644 index 000000000000..15e22ae97cb7 --- /dev/null +++ b/acceptance/extension-logging-fluentd/src/__tests__/accpetance/fluent.acceptance.ts @@ -0,0 +1,71 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application, BindingKey} from '@loopback/core'; +import {LoggingBindings, LoggingComponent} from '@loopback/extension-logging'; +import {expect} from '@loopback/testlab'; +import {promisify} from 'util'; +import {readLog} from '../fixtures/fluentd.docker'; + +const sleep = promisify(setTimeout); + +describe('LoggingComponent', () => { + let app: Application; + + beforeEach(givenAppWithCustomConfig); + + /* eslint-disable no-invalid-this */ + it('binds a fluent sender', async function() { + if (process.env.FLUENTD_SERVICE_PORT_TCP == null) return this.skip(); + const sender = await app.get(LoggingBindings.FLUENT_SENDER); + sender.emit({greeting: 'Hello, LoopBack!'}); + await sleep(100); + await expectLogsToMatch(/LoopBack\s+\{"greeting"\:"Hello, LoopBack!"\}/); + }); + + it('binds a winston transport for fluent', async function() { + if (process.env.FLUENTD_SERVICE_PORT_TCP == null) return this.skip(); + const logger = await app.get(LoggingBindings.WINSTON_LOGGER); + logger.log('info', 'Hello, LoopBack!'); + await sleep(100); + await expectLogsToMatch( + /LoopBack\s+\{"level"\:"info","message":"Hello, LoopBack!"\}/, + ); + }); + + it('throws error if fluent is not configured', async function() { + if (process.env.FLUENTD_SERVICE_PORT_TCP == null) return this.skip(); + + // Remove the configuration for fluent sender + app.unbind(BindingKey.buildKeyForConfig(LoggingBindings.FLUENT_SENDER)); + return expect(app.get(LoggingBindings.FLUENT_SENDER)).to.be.rejectedWith( + /Fluent is not configured\. Please configure logging\.fluent\.sender\./, + ); + }); + + /** + * Read fluentd log files to match against the given regular exp + * @param regex A regular expression for assertion + */ + async function expectLogsToMatch(regex: RegExp) { + const content = await readLog(); + expect(content).match(regex); + } + + async function givenAppWithCustomConfig() { + app = givenApplication(); + app.configure(LoggingBindings.FLUENT_SENDER).to({ + host: process.env.FLUENTD_SERVICE_HOST ?? '127.0.0.1', + port: +(process.env.FLUENTD_SERVICE_PORT_TCP ?? 24224), + timeout: 3.0, + reconnectInterval: 600000, // 10 minutes + }); + app.component(LoggingComponent); + } + + function givenApplication() { + return new Application(); + } +}); diff --git a/acceptance/extension-logging-fluentd/src/__tests__/fixtures/fluentd.docker.ts b/acceptance/extension-logging-fluentd/src/__tests__/fixtures/fluentd.docker.ts new file mode 100644 index 000000000000..0e8f5446cd2e --- /dev/null +++ b/acceptance/extension-logging-fluentd/src/__tests__/fixtures/fluentd.docker.ts @@ -0,0 +1,65 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import * as path from 'path'; +import {GenericContainer, StartedTestContainer} from 'testcontainers'; + +export const ROOT_DIR = path.join(__dirname, '../../../fixtures'); +export const ETC_DIR = path.join(ROOT_DIR, 'etc'); + +async function startFluentd() { + if (process.env.FLUENTD_SERVICE_HOST != null) return; + const container = await new GenericContainer( + 'fluent/fluentd', + 'v1.7.4-debian-1.0', + ) + .withName('fluentd_lb4') + .withExposedPorts(24224, 9880) + .withEnv('FLUENTD_CONF', 'fluentd.conf') + .withBindMount(ETC_DIR, '/fluentd/etc', 'ro') + .start(); + process.env.FLUENTD_SERVICE_HOST = container.getContainerIpAddress(); + process.env.FLUENTD_SERVICE_PORT_TCP = container + .getMappedPort(24224) + .toString(); + process.env.FLUENTD_SERVICE_PORT_HTTP = container + .getMappedPort(9880) + .toString(); + return container; +} + +let fluentd: StartedTestContainer | undefined; + +/* eslint-disable no-invalid-this */ +/** + * Root-level before hook to start Fluentd container + */ +before(async function() { + // Do not run with non-linux CI + if (process.env.CI && process.platform !== 'linux') return; + this.timeout(30 * 1000); + fluentd = await startFluentd(); + expect(process.env.FLUENTD_SERVICE_PORT_HTTP).to.be.String(); + expect(process.env.FLUENTD_SERVICE_PORT_TCP).to.be.String(); +}); + +/** + * Root-level before hook to stop Fluentd container + */ +after(async function() { + this.timeout(30 * 1000); + if (fluentd) await fluentd.stop(); +}); + +export async function readLog() { + if (fluentd == null) return ''; + const result = await fluentd.exec([ + '/bin/sh', + '-c', + 'cat /fluentd/log/*.log', + ]); + return result.output; +} diff --git a/acceptance/extension-logging-fluentd/tsconfig.build.json b/acceptance/extension-logging-fluentd/tsconfig.build.json new file mode 100644 index 000000000000..c7b8e49eaca5 --- /dev/null +++ b/acceptance/extension-logging-fluentd/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/docs/site/MONOREPO.md b/docs/site/MONOREPO.md index f4ae301cca50..ddc0ab7270a9 100644 --- a/docs/site/MONOREPO.md +++ b/docs/site/MONOREPO.md @@ -30,6 +30,9 @@ The [loopback-next](https://github.com/strongloop/loopback-next) repository uses | [example-soap-calculator](https://github.com/strongloop/loopback-next/tree/master/examples/soap-calculator) | @loopback/example-soap-calculator | A tutorial demonstrating integration with a SOAP webservice | | [example-todo-list](https://github.com/strongloop/loopback-next/tree/master/examples/todo-list) | @loopback/example-todo-list | Continuation of the todo example using relations in LoopBack 4 | | [example-todo](https://github.com/strongloop/loopback-next/tree/master/examples/todo) | @loopback/example-todo | A basic tutorial for getting started with Loopback 4 | +| [extension-health](https://github.com/strongloop/loopback-next/tree/master/extensions/health) | @loopback/extension-health | Expose health check related endpoints | +| [extension-logging](https://github.com/strongloop/loopback-next/tree/master/extensions/logging) | @loopback/extension-logging | Add Winston Logger and Fluentd integration | +| [extension-metrics](https://github.com/strongloop/loopback-next/tree/master/extensions/metrics) | @loopback/extension-metrics | Report metrics to Prometheus | | [http-caching-proxy](https://github.com/strongloop/loopback-next/tree/master/packages/http-caching-proxy) | @loopback/http-caching-proxy | A caching HTTP proxy for integration tests. NOT SUITABLE FOR PRODUCTION USE! | | [http-server](https://github.com/strongloop/loopback-next/tree/master/packages/http-server) | @loopback/http-server | A wrapper for creating HTTP/HTTPS servers | | [metadata](https://github.com/strongloop/loopback-next/tree/master/packages/metadata) | @loopback/metadata | Utilities to help developers implement TypeScript decorators, define/merge metadata, and inspect metadata | @@ -44,7 +47,6 @@ The [loopback-next](https://github.com/strongloop/loopback-next) repository uses | [service-proxy](https://github.com/strongloop/loopback-next/tree/master/packages/service-proxy) | @loopback/service-proxy | A common set of interfaces for interacting with service oriented backends such as REST APIs, SOAP Web Services, and gRPC microservices | | [testlab](https://github.com/strongloop/loopback-next/tree/master/packages/testlab) | @loopback/testlab | A collection of test utilities we use to write LoopBack tests | | [tsdocs](https://github.com/strongloop/loopback-next/tree/master/packages/tsdocs) | @loopback/tsdocs | An internal package to generate api docs using Microsoft api-extractor and api-documenter | -| [extensions/health](https://github.com/strongloop/loopback-next/tree/master/extensions/health) | @loopback/extension-health | Expose health check related endpoints | We use npm scripts declared in [package.json](https://github.com/strongloop/loopback-next/blob/master/package.json) diff --git a/extensions/logging/.npmrc b/extensions/logging/.npmrc new file mode 100644 index 000000000000..cafe685a112d --- /dev/null +++ b/extensions/logging/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/extensions/logging/LICENSE b/extensions/logging/LICENSE new file mode 100644 index 000000000000..f23590de26d9 --- /dev/null +++ b/extensions/logging/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2019. All Rights Reserved. +Node module: @loopback/extension-logging +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/extensions/logging/README.md b/extensions/logging/README.md new file mode 100644 index 000000000000..9ac391d34949 --- /dev/null +++ b/extensions/logging/README.md @@ -0,0 +1,196 @@ +# @loopback/extension-logging + +This module contains a component provides logging facilities based on +[Winston](https://github.com/winstonjs/winston) and +[Fluentd](https://github.com/fluent/fluent-logger-node). + +## Stability: :warning:Experimental:warning: + +> Experimental packages provide early access to advanced or experimental +> functionality to get community feedback. Such modules are published to npm +> using `0.x.y` versions. Their APIs and functionality may be subject to +> breaking changes in future releases. + +## Architecture overview + +![logging-component](logging-component.png) + +## Installation + +```sh +npm install --save @loopback/extension-logging +``` + +## Basic use + +The component should be loaded in the constructor of your custom Application +class. + +Start by importing the component class: + +```ts +import {LoggingComponent} from '@loopback/extension-logging'; +``` + +In the constructor, add the component to your application: + +```ts +this.component(LoggingComponent); +``` + +Now your application can add a controller as follows to leverage the logging +facilities: + +```ts +import {inject} from '@loopback/context'; +import {Logger, logInvocation} from '@loopback/extension-logging'; +import {get, param} from '@loopback/rest'; + +class MyController { + // Inject a winston logger + @inject(LoggingBindings.WINSTON_LOGGER) + private logger: Logger; + + // http access is logged by a global interceptor + @get('/greet/{name}') + // log the `greet` method invocations + @logInvocation() + greet(@param.path.string('name') name: string) { + return `Hello, ${name}`; + } + + @get('/hello/{name}') + hello(@param.path.string('name') name: string) { + // Use the winston logger explicitly + this.logger.log('info', `greeting ${name}`); + return `Hello, ${name}`; + } +} +``` + +## Configure the logging component + +The logging component can be configured as follows: + +```ts +app.configure(LoggingBindings.COMPONENT).to({ + enableFluent: false, // default to true + enableHttpAccessLog: true, // default to true +}); +``` + +- `enableFluent`: Enable logs to be sent to Fluentd +- `enableHttpAccessLog`: Enable all http requests to be logged via a global + interceptor + +The component contributes bindings with keys declared under `LoggingBindings` +namespace as follows: + +- FLUENT_SENDER - A fluent sender +- WINSTON_LOGGER - A winston logger +- WINSTON_TRANSPORT_FLUENT - A fluent transport for winston +- WINSTON_INTERCEPTOR - A local interceptor set by `@logInvocation` to log + method invocations +- WINSTON_HTTP_ACCESS_LOGGER - A global interceptor that logs http access with + [Morgan](https://github.com/expressjs/morgan) format + +The fluent sender and transport for winston can be configured against +`FLUENT_SENDER`: + +```ts +import {LoggingBindings} from '@loopback/extension-logging'; + +app.configure(LoggingBindings.FLUENT_SENDER).to({ + host: process.env.FLUENTD_SERVICE_HOST ?? 'localhost', + port: +(process.env.FLUENTD_SERVICE_PORT_TCP ?? 24224), + timeout: 3.0, + reconnectInterval: 600000, // 10 minutes +}); +``` + +The winston logger can be configured against `LoggingBindings.WINSTON_LOGGER`: + +```ts +import {LoggingBindings} from '@loopback/extension-logging'; + +ctx.configure(LoggingBindings.WINSTON_LOGGER).to({ + level: 'info', + format: format.json(), + defaultMeta: {framework: 'LoopBack'}, +}); +``` + +The winston logger accepts two types of extensions to the following extension +points: + +- WINSTON_TRANSPORT = 'logging.winston.transport' +- WINSTON_FORMAT = 'logging.winston.format' + +```ts +import {extensionFor} from '@loopback/core'; +import {format} from 'winston'; +import { + WINSTON_FORMAT, + WINSTON_TRANSPORT, + WinstonFormat, + WinstonTransports, +} from '@loopback/extension-logging'; + +const myFormat: WinstonFormat = format((info, opts) => { + console.log(info); + return false; +})(); + +ctx + .bind('logging.winston.formats.myFormat') + .to(myFormat) + .apply(extensionFor(WINSTON_FORMAT)); +ctx + .bind('logging.winston.formats.colorize') + .to(format.colorize()) + .apply(extensionFor(WINSTON_FORMAT)); + +const consoleTransport = new WinstonTransports.Console({ + level: 'info', + format: format.combine(format.colorize(), format.simple()), +}); +ctx + .bind('logging.winston.transports.console') + .to(consoleTransport) + .apply(extensionFor(WINSTON_TRANSPORT)); +``` + +If no transport is contributed, the winston logger uses the +[console transport](https://github.com/winstonjs/winston/blob/master/docs/transports.md#console-transport). + +No default format is configured for the winston logger. + +The access log interceptor can also be configured to customize +[Morgan format and options](https://github.com/expressjs/morgan#morganformat-options): + +```ts +ctx + .configure(LoggingBindings.WINSTON_HTTP_ACCESS_LOGGER) + .to({format: 'combined'}); +``` + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +The acceptance test against fluentd is available as a separate package at +`acceptance/extension-logging-fluentd`. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/extensions/logging/index.d.ts b/extensions/logging/index.d.ts new file mode 100644 index 000000000000..d399a659f439 --- /dev/null +++ b/extensions/logging/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/extensions/logging/index.js b/extensions/logging/index.js new file mode 100644 index 000000000000..5b57f4878e57 --- /dev/null +++ b/extensions/logging/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/extensions/logging/index.ts b/extensions/logging/index.ts new file mode 100644 index 000000000000..cf8ec779d223 --- /dev/null +++ b/extensions/logging/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/extensions/logging/logging-component.png b/extensions/logging/logging-component.png new file mode 100644 index 000000000000..6f856e3c7aba Binary files /dev/null and b/extensions/logging/logging-component.png differ diff --git a/extensions/logging/package-lock.json b/extensions/logging/package-lock.json new file mode 100644 index 000000000000..db616d71b74b --- /dev/null +++ b/extensions/logging/package-lock.json @@ -0,0 +1,464 @@ +{ + "name": "@loopback/extension-logging", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", + "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz", + "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz", + "integrity": "sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, + "@types/morgan": { + "version": "1.7.37", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.7.37.tgz", + "integrity": "sha512-tIdEA10BcHcOumMmUiiYdw8lhiVVq62r0ghih5Xpp4WETkfsMiTUZL4w9jCI502BBOrKhFrAOGml9IeELvVaBA==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/node": { + "version": "10.17.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.14.tgz", + "integrity": "sha512-G0UmX5uKEmW+ZAhmZ6PLTQ5eu/VPaT+d/tdLd5IFsKRPcbe6lPxocBtcYBFSaLaCW8O60AX90e91Nsp8lVHCNw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "requires": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.x" + } + }, + "env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==" + }, + "event-lite": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.2.tgz", + "integrity": "sha512-HnSYx1BsJ87/p6swwzv+2v6B4X+uxUteoDfRxsAb1S1BePzQqOLevVmkdA15GHJVd9A9Ok6wygUR18Hu0YeV9g==" + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" + }, + "fluent-logger": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/fluent-logger/-/fluent-logger-3.4.1.tgz", + "integrity": "sha512-lERIhXAvhtCYeQq8K7sBDg/HY9GkiVRq5xY3oN+hcSINVKwqwBzG6LQOJK73EnV50qO59U7XEmRnn2hBzLWaHw==", + "requires": { + "msgpack-lite": "*" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "requires": { + "colornames": "^1.1.1" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=", + "requires": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, + "p-event": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", + "integrity": "sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA==", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz", + "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "requires": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + } + }, + "winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "requires": { + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + } + } +} diff --git a/extensions/logging/package.json b/extensions/logging/package.json new file mode 100644 index 000000000000..4dd2368b6fde --- /dev/null +++ b/extensions/logging/package.json @@ -0,0 +1,58 @@ +{ + "name": "@loopback/extension-logging", + "version": "0.0.1", + "description": "LoopBack Logging for Fluentd", + "engines": { + "node": ">=8.9" + }, + "scripts": { + "build": "lb-tsc", + "clean": "lb-clean loopback-extension-logging*.tgz dist tsconfig.build.tsbuildinfo package", + "pretest": "npm run build", + "test": "lb-mocha \"dist/__tests__/**/*.js\"", + "verify": "npm pack && tar xf loopback-extension-logging*.tgz && tree package && npm run clean" + }, + "author": "IBM Corp.", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "@loopback/context": "^2.1.0", + "@loopback/core": "^1.12.3", + "@loopback/rest": "^1.26.1", + "fluent-logger": "^3.4.1", + "morgan": "^1.9.1", + "winston": "^3.2.1", + "winston-transport": "^4.3.0" + }, + "devDependencies": { + "@loopback/build": "^3.1.1", + "@loopback/eslint-config": "^5.0.3", + "@loopback/testlab": "^1.10.3", + "@types/morgan": "^1.7.37", + "@types/node": "^10.17.14", + "p-event": "^4.1.0" + }, + "keywords": [ + "LoopBack", + "Cloud Native", + "Fluentd", + "Logging" + ], + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist", + "src", + "!*/__tests__", + "templates" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git", + "directory": "extensions/logging" + } +} diff --git a/extensions/logging/src/__tests__/accpetance/logging.component.acceptance.ts b/extensions/logging/src/__tests__/accpetance/logging.component.acceptance.ts new file mode 100644 index 000000000000..3bcfa356faf3 --- /dev/null +++ b/extensions/logging/src/__tests__/accpetance/logging.component.acceptance.ts @@ -0,0 +1,86 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {expect} from '@loopback/testlab'; +import {LoggingBindings, LoggingComponent} from '../..'; + +describe('LoggingComponent', () => { + let app: Application; + + before(givenAppWithCustomConfig); + + it('binds a fluent sender', async () => { + expect(app.isBound(LoggingBindings.FLUENT_SENDER)).to.be.true(); + const sender = await app.get(LoggingBindings.FLUENT_SENDER); + expect(sender.emit).to.be.Function(); + }); + + it('binds a winston logger', async () => { + expect(app.isBound(LoggingBindings.WINSTON_LOGGER)).to.be.true(); + const logger = await app.get(LoggingBindings.WINSTON_LOGGER); + expect(logger.log).to.be.Function(); + }); + + it('binds a winston transport for fluent', async () => { + expect(app.isBound(LoggingBindings.WINSTON_TRANSPORT_FLUENT)).to.be.true(); + const logger = await app.get(LoggingBindings.WINSTON_LOGGER); + expect(logger.transports.length).to.eql(1); + }); + + async function givenAppWithCustomConfig() { + app = givenApplication(); + app.configure(LoggingBindings.FLUENT_SENDER).to({ + host: 'localhost', + port: 24224, + timeout: 3.0, + reconnectInterval: 600000, // 10 minutes + }); + app.component(LoggingComponent); + } + + function givenApplication() { + return new Application(); + } +}); + +describe('LoggingComponent without fluent', () => { + let app: Application; + + before(givenAppAndLoggingComponent); + + it('does not bind a fluent sender', async () => { + expect(app.isBound(LoggingBindings.FLUENT_SENDER)).to.be.false(); + }); + + it('binds a winston logger', async () => { + expect(app.isBound(LoggingBindings.WINSTON_LOGGER)).to.be.true(); + const logger = await app.get(LoggingBindings.WINSTON_LOGGER); + expect(logger.log).to.be.Function(); + }); + + it('does not bind a winston transport for fluent', async () => { + expect(app.isBound(LoggingBindings.WINSTON_TRANSPORT_FLUENT)).to.be.false(); + }); + + it('does not bind a winston access log', async () => { + expect( + app.isBound(LoggingBindings.WINSTON_HTTP_ACCESS_LOGGER), + ).to.be.false(); + }); + + async function givenAppAndLoggingComponent() { + app = givenApplication(); + app.configure(LoggingBindings.COMPONENT).to({ + enableFluent: false, + enableHttpAccessLog: false, + }); + app.component(LoggingComponent); + } + + function givenApplication() { + return new Application(); + } +}); diff --git a/extensions/logging/src/__tests__/accpetance/logging.interceptor.acceptance.ts b/extensions/logging/src/__tests__/accpetance/logging.interceptor.acceptance.ts new file mode 100644 index 000000000000..1ffc88f033ab --- /dev/null +++ b/extensions/logging/src/__tests__/accpetance/logging.interceptor.acceptance.ts @@ -0,0 +1,146 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-health +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {extensionFor} from '@loopback/core'; +import { + get, + HttpErrors, + param, + RestApplication, + RestServerConfig, +} from '@loopback/rest'; +import { + Client, + createRestAppClient, + expect, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {format, transports} from 'winston'; +import { + LoggingComponent, + logInvocation, + WinstonFormat, + WinstonLogRecord, + WINSTON_FORMAT, + WINSTON_TRANSPORT, +} from '../..'; +import {LoggingBindings} from '../../keys'; + +describe('Logging interceptor', () => { + let app: RestApplication; + let request: Client; + let logs: WinstonLogRecord[] = []; + + class MyController { + @get('/greet/{name}') + @logInvocation() + greet(@param.path.string('name') name: string) { + if (name === 'invalid-name') { + throw new HttpErrors.BadRequest('Invalid name'); + } + return `Hello, ${name}`; + } + + @get('/hello/{name}') + hello(@param.path.string('name') name: string) { + if (name === 'invalid-name') { + throw new HttpErrors.BadRequest('Invalid name'); + } + return `Hello, ${name}`; + } + } + + before(async () => { + app = givenRestApplication(); + app.controller(MyController); + logs = []; + const myFormat: WinstonFormat = format((info, opts) => { + logs.push(info); + return false; + })(); + app + .bind('logging.winston.formats.myFormat') + .to(myFormat) + .apply(extensionFor(WINSTON_FORMAT)); + const consoleTransport = new transports.Console({ + level: 'verbose', + format: myFormat, + }); + app + .bind('logging.winston.transports.console') + .to(consoleTransport) + .apply(extensionFor(WINSTON_TRANSPORT)); + app.configure(LoggingBindings.COMPONENT).to({ + enableFluent: false, + }); + app.component(LoggingComponent); + await app.start(); + request = createRestAppClient(app); + }); + + after(async () => { + if (app) await app.stop(); + (app as unknown) = undefined; + }); + + beforeEach(() => { + logs = []; + }); + + it('logs http req/res for /hello', async () => { + await request.get('/hello/John').expect(200); + expect(logs.length).to.equal(1); + expect(logs[0].level).to.equal('info'); + expect(logs[0].message).to.match(/"GET \/hello\/John HTTP\/1.1" 200/); + }); + + it('logs error for http req/res for /hello', async () => { + await request.get('/hello/invalid-name').expect(400); + expect(logs.length).to.equal(1); + expect(logs[0].level).to.equal('info'); + expect(logs[0].message).to.match( + /"GET \/hello\/invalid-name HTTP\/1.1" 400/, + ); + }); + + it('logs http req/res for /greet', async () => { + await request.get('/greet/John').expect(200); + expect(logs.length).to.equal(3); + expect(logs[0].level).to.equal('verbose'); + expect(logs[1].level).to.equal('verbose'); + expect(logs[2].level).to.equal('info'); + expect(logs[2].message).to.match(/"GET \/greet\/John HTTP\/1.1" 200/); + }); + + it('logs method invocation of greet()', async () => { + await request.get('/greet/Jane').expect(200); + expect(logs).to.containEql({ + level: 'verbose', + message: "invoking MyController.prototype.greet with: [ 'Jane' ]", + }); + expect(logs).to.containEql({ + level: 'verbose', + message: 'returned from MyController.prototype.greet: Hello, Jane', + }); + }); + + it('logs method invocation error of greet()', async () => { + await request.get('/greet/invalid-name').expect(400); + expect(logs).to.containEql({ + level: 'verbose', + message: "invoking MyController.prototype.greet with: [ 'invalid-name' ]", + }); + expect(logs[1].level).to.eql('error'); + expect(logs[1].message).to.match( + /error from MyController\.prototype\.greet/, + ); + expect(logs[1].message).to.match(/BadRequestError\: Invalid name/); + }); + + function givenRestApplication(config?: RestServerConfig) { + const rest = Object.assign({}, givenHttpServerConfig(), config); + return new RestApplication({rest}); + } +}); diff --git a/extensions/logging/src/__tests__/accpetance/winston.acceptance.ts b/extensions/logging/src/__tests__/accpetance/winston.acceptance.ts new file mode 100644 index 000000000000..fb3e3b67729c --- /dev/null +++ b/extensions/logging/src/__tests__/accpetance/winston.acceptance.ts @@ -0,0 +1,83 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Context} from '@loopback/context'; +import {extensionFor} from '@loopback/core'; +import {expect} from '@loopback/testlab'; +import { + format, + LoggingBindings, + WinstonFormat, + WinstonLoggerOptions, + WinstonLoggerProvider, + WinstonLogRecord, + WinstonTransports, + WINSTON_FORMAT, + WINSTON_TRANSPORT, +} from '../..'; + +describe('Winston Logger', () => { + let ctx: Context; + + beforeEach(givenContext); + + it('creates a winston logger', async () => { + ctx.bind(LoggingBindings.WINSTON_LOGGER).toProvider(WinstonLoggerProvider); + ctx.configure(LoggingBindings.WINSTON_LOGGER).to({ + level: 'info', + format: format.json(), + defaultMeta: {framework: 'LoopBack'}, + }); + const logger = await ctx.get(LoggingBindings.WINSTON_LOGGER); + expect(logger.level).to.eql('info'); + }); + + it('creates a winston logger with transports', async () => { + ctx.bind(LoggingBindings.WINSTON_LOGGER).toProvider(WinstonLoggerProvider); + ctx.configure(LoggingBindings.WINSTON_LOGGER).to({ + level: 'info', + format: format.json(), + defaultMeta: {framework: 'LoopBack'}, + }); + const consoleTransport = new WinstonTransports.Console({ + level: 'info', + format: format.combine(format.colorize(), format.simple()), + }); + ctx + .bind('logging.winston.transports.console') + .to(consoleTransport) + .apply(extensionFor(WINSTON_TRANSPORT)); + const logger = await ctx.get(LoggingBindings.WINSTON_LOGGER); + expect(logger.transports).to.eql([consoleTransport]); + }); + + it('creates a winston logger with formats', async () => { + ctx.bind(LoggingBindings.WINSTON_LOGGER).toProvider(WinstonLoggerProvider); + ctx.configure(LoggingBindings.WINSTON_LOGGER).to({ + level: 'info', + defaultMeta: {framework: 'LoopBack'}, + }); + const logs: WinstonLogRecord[] = []; + const myFormat: WinstonFormat = format((info, opts) => { + logs.push(info); + return false; + })(); + ctx + .bind('logging.winston.formats.myFormat') + .to(myFormat) + .apply(extensionFor(WINSTON_FORMAT)); + ctx + .bind('logging.winston.formats.colorize') + .to(format.colorize()) + .apply(extensionFor(WINSTON_FORMAT)); + const logger = await ctx.get(LoggingBindings.WINSTON_LOGGER); + logger.log('info', 'Hello, LoopBack!'); + expect(logs).to.eql([{level: 'info', message: 'Hello, LoopBack!'}]); + }); + + function givenContext() { + ctx = new Context(); + } +}); diff --git a/extensions/logging/src/decorators/index.ts b/extensions/logging/src/decorators/index.ts new file mode 100644 index 000000000000..61992178bdf2 --- /dev/null +++ b/extensions/logging/src/decorators/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './logging.decorator'; diff --git a/extensions/logging/src/decorators/logging.decorator.ts b/extensions/logging/src/decorators/logging.decorator.ts new file mode 100644 index 000000000000..3d2d0a65b720 --- /dev/null +++ b/extensions/logging/src/decorators/logging.decorator.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {intercept} from '@loopback/context'; +import {LoggingBindings} from '../keys'; + +/** + * @logInvocation decorator for method invocations. + * + * @example + * ```ts + * import {logInvocation} from '@loopback/extension-logging'; + * + * export class HelloController { + * @logInvocation() + * hello(name: string) { + * return `Hello, ${name}`; + * } + * } + * ``` + */ +export function logInvocation() { + // A shortcut to `@intercept` that invokes the winston interceptor that logs + // method invocations + return intercept(LoggingBindings.WINSTON_INVOCATION_LOGGER); +} diff --git a/extensions/logging/src/fluent.ts b/extensions/logging/src/fluent.ts new file mode 100644 index 000000000000..8af73668f4b0 --- /dev/null +++ b/extensions/logging/src/fluent.ts @@ -0,0 +1,59 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Binding, config, Getter, inject, Provider} from '@loopback/core'; +import {createFluentSender, FluentSender, Options} from 'fluent-logger'; +import {LoggingBindings} from './keys'; +const fluent = require('fluent-logger'); +import TransportStream = require('winston-transport'); + +export {FluentSender} from 'fluent-logger'; + +/** + * Provider for FluentSender + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class FluentSenderProvider implements Provider> { + constructor( + @config.getter() private getFluentConfig: Getter, + ) {} + + @inject.binding() + private binding: Binding; + + async value() { + const options = await this.getFluentConfig(); + if (options == null) { + throw new Error( + `Fluent is not configured. Please configure ${this.binding.key}.`, + ); + } + return createFluentSender('LoopBack', options); + } +} + +/** + * Provider to create FluentTransport for Winston + */ +export class FluentTransportProvider implements Provider { + constructor( + @config.getter({fromBinding: LoggingBindings.FLUENT_SENDER}) + private getFluentConfig: Getter, + ) {} + + @inject.binding() + private binding: Binding; + + async value() { + const options = await this.getFluentConfig(); + if (options == null) { + throw new Error( + `Fluent is not configured. Please configure ${this.binding.key}.`, + ); + } + const winstonTransportClass = fluent.support.winstonTransport(); + return new winstonTransportClass('LoopBack', options); + } +} diff --git a/extensions/logging/src/index.ts b/extensions/logging/src/index.ts new file mode 100644 index 000000000000..f619551c2239 --- /dev/null +++ b/extensions/logging/src/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './decorators'; +export * from './keys'; +export * from './logging.component'; +export * from './winston'; diff --git a/extensions/logging/src/interceptors/index.ts b/extensions/logging/src/interceptors/index.ts new file mode 100644 index 000000000000..ea923a19ef1c --- /dev/null +++ b/extensions/logging/src/interceptors/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './logging.interceptor'; diff --git a/extensions/logging/src/interceptors/logging.interceptor.ts b/extensions/logging/src/interceptors/logging.interceptor.ts new file mode 100644 index 000000000000..63096f3e28d7 --- /dev/null +++ b/extensions/logging/src/interceptors/logging.interceptor.ts @@ -0,0 +1,127 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + asGlobalInterceptor, + bind, + BindingScope, + config, + ContextTags, + inject, + Interceptor, + InvocationContext, + Provider, + ValueOrPromise, +} from '@loopback/context'; +import {RequestContext, RestBindings} from '@loopback/rest'; +import morgan from 'morgan'; +import {format} from 'util'; +import {Logger} from 'winston'; +import {LoggingBindings} from '../keys'; + +/** + * A local interceptor that provides logging for method invocations. + */ +@bind({ + tags: {[ContextTags.KEY]: LoggingBindings.WINSTON_INVOCATION_LOGGER}, + scope: BindingScope.SINGLETON, +}) +export class InvocationLoggingInterceptor implements Provider { + constructor( + @inject(LoggingBindings.WINSTON_LOGGER) + private logger: Logger, + ) {} + + value() { + return this.intercept.bind(this); + } + + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + try { + this.logger.log( + 'verbose', + format( + 'invoking %s with:', + invocationCtx.targetName, + invocationCtx.args, + ), + ); + const result = await next(); + this.logger.log( + 'verbose', + format('returned from %s:', invocationCtx.targetName, result), + ); + return result; + } catch (err) { + this.logger.log( + 'error', + format('error from %s', invocationCtx.targetName, err), + ); + throw err; + } + } +} + +export interface AccessLogOptions extends morgan.Options { + format?: string | morgan.FormatFn; +} + +/** + * A global interceptor that provides logging for http requests/responses. + */ +@bind(asGlobalInterceptor('logging'), { + tags: { + [ContextTags.KEY]: LoggingBindings.WINSTON_HTTP_ACCESS_LOGGER, + // Only apply to invocations from REST routes + [ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: 'route', + }, + scope: BindingScope.SINGLETON, +}) +export class HttpAccessLogInterceptor implements Provider { + constructor( + @inject(LoggingBindings.WINSTON_LOGGER) + private logger: Logger, + @config() + private morganOptions: AccessLogOptions = {format: 'combined'}, + ) {} + + value() { + return this.intercept.bind(this); + } + + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + const reqCtx = await invocationCtx.get( + RestBindings.Http.CONTEXT, + ); + const options: AccessLogOptions = { + ...this.morganOptions, + stream: { + write: (message: string) => { + this.logger.info(message); + }, + }, + }; + if (typeof options.format === 'function') { + morgan(options.format, options)( + reqCtx.request, + reqCtx.response, + () => {}, + ); + } else { + morgan(options.format ?? 'combined', options)( + reqCtx.request, + reqCtx.response, + () => {}, + ); + } + return next(); + } +} diff --git a/extensions/logging/src/keys.ts b/extensions/logging/src/keys.ts new file mode 100644 index 000000000000..cb9f0cd15f0b --- /dev/null +++ b/extensions/logging/src/keys.ts @@ -0,0 +1,52 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingKey, GenericInterceptor} from '@loopback/core'; +import {FluentSender} from 'fluent-logger'; +import * as WinstonTransport from 'winston-transport'; +import {LoggingComponent} from './logging.component'; +import {WinstonLogger} from './winston'; + +/** + * Binding keys used by this component. + */ +export namespace LoggingBindings { + export const COMPONENT = BindingKey.create( + 'components.LoggingComponent', + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export const FLUENT_SENDER = BindingKey.create>( + 'logging.fluent.sender', + ); + + /** + * Binding key for winston logger + */ + export const WINSTON_LOGGER = BindingKey.create( + 'logging.winston.logger', + ); + + /** + * Binding key for winston transport backed by fluent + */ + export const WINSTON_TRANSPORT_FLUENT = BindingKey.create( + 'logging.winston.transports.fluent', + ); + + /** + * Binding key for method invocation logger with winston + */ + export const WINSTON_INVOCATION_LOGGER = BindingKey.create< + GenericInterceptor + >('logging.winston.invocationLogger'); + + /** + * Binding key for http access logger with winston + */ + export const WINSTON_HTTP_ACCESS_LOGGER = BindingKey.create< + GenericInterceptor + >('logging.winston.httpAccessLogger'); +} diff --git a/extensions/logging/src/logging.component.ts b/extensions/logging/src/logging.component.ts new file mode 100644 index 000000000000..6d10a44eaf81 --- /dev/null +++ b/extensions/logging/src/logging.component.ts @@ -0,0 +1,76 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + bind, + Binding, + Component, + config, + ContextTags, + extensionFor, + ProviderMap, +} from '@loopback/core'; +import {FluentSenderProvider, FluentTransportProvider} from './fluent'; +import { + HttpAccessLogInterceptor, + InvocationLoggingInterceptor, +} from './interceptors'; +import {LoggingBindings} from './keys'; +import {WinstonLoggerProvider, WINSTON_TRANSPORT} from './winston'; + +/** + * Configuration for LoggingComponent + */ +export type LoggingComponentConfig = { + /** + * A flag to enable fluent, default to `true` + */ + enableFluent?: boolean; + /** + * A flag to enable Winston-based http access log, default to `true` + */ + enableHttpAccessLog?: boolean; +}; + +/** + * A component providing logging facilities + */ +@bind({tags: {[ContextTags.KEY]: LoggingBindings.COMPONENT}}) +export class LoggingComponent implements Component { + providers: ProviderMap; + bindings: Binding[]; + + constructor( + @config() + loggingConfig: LoggingComponentConfig | undefined, + ) { + loggingConfig = { + enableFluent: true, + enableHttpAccessLog: true, + ...loggingConfig, + }; + this.providers = { + [LoggingBindings.WINSTON_LOGGER.key]: WinstonLoggerProvider, + [LoggingBindings.WINSTON_INVOCATION_LOGGER + .key]: InvocationLoggingInterceptor, + }; + + if (loggingConfig.enableHttpAccessLog) { + this.providers[ + LoggingBindings.WINSTON_HTTP_ACCESS_LOGGER.key + ] = HttpAccessLogInterceptor; + } + + if (loggingConfig.enableFluent) { + this.providers[LoggingBindings.FLUENT_SENDER.key] = FluentSenderProvider; + // Only create fluent transport if it's configured + this.bindings = [ + Binding.bind(LoggingBindings.WINSTON_TRANSPORT_FLUENT) + .toProvider(FluentTransportProvider) + .apply(extensionFor(WINSTON_TRANSPORT)), + ]; + } + } +} diff --git a/extensions/logging/src/winston.ts b/extensions/logging/src/winston.ts new file mode 100644 index 000000000000..5bcaf1a8a7fd --- /dev/null +++ b/extensions/logging/src/winston.ts @@ -0,0 +1,74 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/extension-logging +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {config, extensions, Getter, Provider} from '@loopback/core'; +import {Format} from 'logform'; +import { + createLogger, + format, + Logger, + LoggerOptions, + transports as commonTransports, +} from 'winston'; +import * as Transport from 'winston-transport'; + +/** + * Re-export logform/winston types + */ +export { + Format as WinstonFormat, + TransformableInfo as WinstonLogRecord, +} from 'logform'; +export { + format, + Logger as WinstonLogger, + LoggerOptions as WinstonLoggerOptions, + transports as WinstonTransports, +} from 'winston'; + +/** + * An extension point for winston transports + */ +export const WINSTON_TRANSPORT = 'logging.winston.transport'; + +/** + * An extension point for winston formats + */ +export const WINSTON_FORMAT = 'logging.winston.format'; + +/** + * A provider class that creates WinstonLogger instances + */ +export class WinstonLoggerProvider implements Provider { + constructor( + /** + * Getter for transports + */ + @extensions(WINSTON_TRANSPORT) + private transports: Getter, + /** + * Getter for formats + */ + @extensions(WINSTON_FORMAT) + private formats: Getter, + /** + * Configuration for the logger + */ + @config() private options: LoggerOptions = {}, + ) {} + + async value() { + let transports = await this.transports(); + if (transports.length === 0) { + transports = [new commonTransports.Console({})]; + } + const formats = await this.formats(); + return createLogger({ + transports, + format: format.combine(...formats), + ...this.options, + }); + } +} diff --git a/extensions/logging/tsconfig.build.json b/extensions/logging/tsconfig.build.json new file mode 100644 index 000000000000..c7b8e49eaca5 --- /dev/null +++ b/extensions/logging/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +}