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
+
+
+
+## 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"]
+}