├── src ├── lib │ ├── config │ │ ├── default │ │ │ ├── config-kyma.json │ │ │ ├── config-core.json │ │ │ ├── config-sap-passport.json │ │ │ ├── config-cf.json │ │ │ └── config-schema.json │ │ ├── configValidator.ts │ │ └── interfaces.ts │ ├── logger │ │ ├── level.ts │ │ ├── record.ts │ │ ├── recordWriter.ts │ │ ├── context.ts │ │ ├── cache.ts │ │ ├── rootLogger.ts │ │ ├── logger.ts │ │ ├── recordFactory.ts │ │ └── sourceUtils.ts │ ├── helper │ │ ├── jsonHelper.ts │ │ ├── envService.ts │ │ ├── levelUtils.ts │ │ ├── envVarHelper.ts │ │ ├── jwtService.ts │ │ └── stacktraceUtils.ts │ ├── middleware │ │ ├── interfaces.ts │ │ ├── requestAccessor.ts │ │ ├── responseAccessor.ts │ │ ├── utils.ts │ │ ├── framework-services │ │ │ ├── connect.ts │ │ │ ├── plainhttp.ts │ │ │ ├── restify.ts │ │ │ ├── express.ts │ │ │ └── fastify.ts │ │ └── middleware.ts │ └── winston │ │ └── winstonTransport.ts ├── performance-test │ ├── test-app │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ └── app.js │ └── config-cf-with-defaults.json ├── index.ts └── test │ ├── acceptance-test │ ├── nodejs-http │ │ └── app.js │ ├── connect │ │ └── app.js │ ├── restify │ │ └── app.js │ ├── fastify │ │ └── app.js │ ├── express │ │ └── app.js │ ├── config.test.js │ ├── global-context.test.js │ └── child-logger.test.js │ ├── config-test.json │ └── unit-test │ └── winston-transport.test.js ├── docs ├── .bundle │ └── config ├── .gitignore ├── _sass │ └── custom │ │ └── custom.scss ├── general-usage │ ├── index.md │ ├── 01-request-logs.md │ ├── 05-stacktraces.md │ ├── 03-logging-contexts.md │ ├── 02-message-logs.md │ └── 04-custom-fields.md ├── advanced-usage │ ├── index.md │ ├── 07-custom-sink-function.md │ ├── 05-override-request-log-fields.md │ ├── 08-winston-transport.md │ ├── 06-sap-passort.md │ ├── 02-correlation-and-tenant.md │ ├── 03-sensitive-data-reduction.md │ ├── 01-child-loggers.md │ └── 04-dynamic-logging-level-threshold.md ├── getting-started │ ├── index.md │ ├── 01-installation.md │ └── 02-framework-samples.md ├── configuration │ ├── index.md │ ├── default-req-log-level.md │ ├── framework.md │ ├── custom-fields-format.md │ └── introduction.md ├── 404.html ├── sample-apps │ └── index.md ├── Gemfile ├── migration.md ├── index.markdown └── _config.yml ├── sample ├── winston-sample │ ├── .gitignore │ ├── README.md │ ├── manifest.yml │ ├── web.js │ └── package.json └── cf-nodejs-logging-support-sample │ ├── .gitignore │ ├── manifest.yml │ ├── package.json │ ├── README.md │ ├── public │ ├── scripts │ │ └── logic.js │ ├── index.html │ └── styles │ │ └── style.css │ └── app.js ├── .gitignore ├── .jshintrc ├── tools └── token-creator │ ├── package.json │ ├── README.md │ ├── package-lock.json │ └── token-creator.js ├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── README.md ├── REUSE.toml ├── tsconfig.json ├── package.json └── CONTRIBUTING.md /src/lib/config/default/config-kyma.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /docs/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: "vendor/bundle" 3 | -------------------------------------------------------------------------------- /sample/winston-sample/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | coverage/ 4 | .vscode/ 5 | .nyc_output/ 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /docs/_sass/custom/custom.scss: -------------------------------------------------------------------------------- 1 | 2 | .site-title { 3 | font-size: 1.2em !important; 4 | } 5 | -------------------------------------------------------------------------------- /src/performance-test/test-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .vscode/ 4 | .nyc_output/ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .vscode/ 4 | .nyc_output/ 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /docs/general-usage/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: General Usage 4 | nav_order: 3 5 | has_children: true 6 | --- 7 | 8 | # General Usage 9 | -------------------------------------------------------------------------------- /docs/advanced-usage/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Advanced Usage 4 | nav_order: 4 5 | has_children: true 6 | --- 7 | 8 | # Advanced Usage 9 | -------------------------------------------------------------------------------- /docs/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Getting Started 4 | nav_order: 2 5 | has_children: true 6 | --- 7 | # Getting Started 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion":6, 3 | "node": true, 4 | "jquery": true, 5 | "browser": true, 6 | "-W041": false, 7 | "mocha": true 8 | } 9 | -------------------------------------------------------------------------------- /docs/configuration/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Advanced Configuration 4 | nav_order: 5 5 | has_children: true 6 | --- 7 | 8 | # Advanced Configuration 9 | -------------------------------------------------------------------------------- /sample/winston-sample/README.md: -------------------------------------------------------------------------------- 1 | # Winston Sample for cf-nodejs-logging support 2 | 3 | This app demonstrates the abilities of the winston transport the library provides. -------------------------------------------------------------------------------- /src/lib/logger/level.ts: -------------------------------------------------------------------------------- 1 | export enum Level { 2 | Inherit = -2, 3 | Off = -1, 4 | Error = 0, 5 | Warn = 1, 6 | Info = 2, 7 | Verbose = 3, 8 | Debug = 4, 9 | Silly = 5 10 | } 11 | -------------------------------------------------------------------------------- /sample/winston-sample/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: winston-sample 4 | command: node web.js 5 | memory: 128m 6 | disk: 128m 7 | instances: 1 8 | path: . 9 | stackato: 10 | env: 11 | RELEASE: v5 -------------------------------------------------------------------------------- /src/lib/logger/record.ts: -------------------------------------------------------------------------------- 1 | export default class Record { 2 | payload: any 3 | metadata: any 4 | 5 | constructor(level: string) { 6 | this.payload = {} 7 | this.metadata = { 8 | level: level 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/lib/helper/jsonHelper.ts: -------------------------------------------------------------------------------- 1 | export default class JSONHelper { 2 | parseJSONSafe(value: string | undefined): any { 3 | let tmp = {}; 4 | if (value) { 5 | try { 6 | tmp = JSON.parse(value); 7 | } catch (e) { 8 | tmp = {}; 9 | } 10 | } 11 | return tmp; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import RootLogger from "./lib/logger/rootLogger"; 2 | const rootLogger = RootLogger.getInstance(); 3 | 4 | module.exports = rootLogger; // assign default export 5 | exports = module.exports; // re-assign exports 6 | 7 | export default rootLogger; 8 | export * from "./lib/config/interfaces"; 9 | export * from "./lib/logger/level"; 10 | export * from "./lib/logger/logger"; 11 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: cf-nodejs-logging-support-sample 4 | command: node app.js 5 | memory: 128m 6 | disk: 128m 7 | instances: 1 8 | path: . 9 | env: 10 | # Set LOG_*: true to activate logging of respective field 11 | LOG_SENSITIVE_CONNECTION_DATA: false 12 | LOG_REMOTE_USER: false 13 | LOG_REFERER: false 14 | -------------------------------------------------------------------------------- /src/lib/middleware/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface FrameworkService { 2 | getReqHeaderField(req: any, fieldName: string): string | undefined; 3 | getReqField(req: any, fieldName: string): string | number | boolean | undefined; 4 | getResHeaderField(req: any, fieldName: string): string | undefined; 5 | getResField(req: any, fieldName: string): string | number | boolean | undefined; 6 | onResFinish(res: any, handler: () => void): void; 7 | } 8 | -------------------------------------------------------------------------------- /src/performance-test/test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "1.0.0", 4 | "description": "Sample app to test performance of cf-nodejs-logging-support library", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node --prof app.js" 8 | }, 9 | "dependencies": { 10 | "cf-nodejs-logging-support": "7.0.0-beta.6", 11 | "ejs": "^3.1.10", 12 | "express": "^4.21.2" 13 | }, 14 | "engines": { 15 | "node": ">=8.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /src/performance-test/test-app/README.md: -------------------------------------------------------------------------------- 1 | # cf-nodejs-logging-support-sample 2 | This app serves to test the performance of the library cf-nodejs-logging-support. 3 | 4 | 5 | ## Test performance with built-in node profiler 6 | * Install dependencies by running ```npm install``` 7 | * Execute ```npm start``` to run the app in production mode with the built-in profiler. 8 | * Send http requests to trigger operations and then use the tick processor bundled with the Node.js binary to create a readable .txt file (See https://nodejs.org/en/docs/guides/simple-profiling/). 9 | -------------------------------------------------------------------------------- /src/lib/config/configValidator.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { ValidateFunction } from 'ajv'; 2 | 3 | import ConfigSchema from './default/config-schema.json'; 4 | 5 | export default class ConfigValidator { 6 | 7 | private validate: ValidateFunction 8 | 9 | constructor() { 10 | const ajv = new Ajv({ strict: false }); 11 | this.validate = ajv.compile(ConfigSchema); 12 | } 13 | 14 | isValid(config: any): true | [false, any] { 15 | const valid = this.validate(config); 16 | if (!valid) { 17 | return [false, this.validate.errors]; 18 | } 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/winston-sample/web.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var log = require("cf-nodejs-logging-support"); 3 | var winston = require('winston'); 4 | 5 | var app = express(); 6 | 7 | var logger = winston.createLogger({ 8 | // Bind transport to winston 9 | transports: [log.createWinstonTransport()] 10 | }); 11 | 12 | app.get('/', function (req, res) { 13 | res.send('Hello World'); 14 | logger.log("info", "Received request"); 15 | }); 16 | 17 | app.listen(3000); 18 | 19 | // Messages will now be logged exactly as demonstrated by the Custom Messages paragraph 20 | logger.log("info", "Server is listening on port %d", 3000); 21 | -------------------------------------------------------------------------------- /tools/token-creator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cf-nodejs-logging-support-token-creator", 3 | "version": "1.0.1", 4 | "description": "Creates JWTs, which can be used to set log-levels dynamically per request.", 5 | "main": "token-creator.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/SAP/cf-nodejs-logging-support" 12 | }, 13 | "license": "Apache-2.0", 14 | "author": { 15 | "name": "Christian Dinse" 16 | }, 17 | "dependencies": { 18 | "commander": "^2.15.0", 19 | "jsonwebtoken": "^9.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cf-nodejs-logging-support-sample-app", 3 | "version": "1.1.0", 4 | "description": "Sample app to learn and test features from the cf-nodejs-logging-support library", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "start-nodemon": "nodemon app.js" 9 | }, 10 | "author": "Federico Romagnoli", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "cf-nodejs-logging-support": "latest", 14 | "ejs": "^3.1.10", 15 | "express": "latest" 16 | }, 17 | "devDependencies": { 18 | "nodemon": "^3.0.1" 19 | }, 20 | "engines": { 21 | "node": ">=12.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/README.md: -------------------------------------------------------------------------------- 1 | # cf-nodejs-logging-support-sample 2 | This app demonstrates the logging functionality of the library cf-nodejs-logging-support by providing a simple UI with buttons that send HTTP requests. 3 | 4 | ## Installation on CloudFoundry 5 | * Make sure to choose a unique name by editing the ```name``` field in manifest.yml 6 | * Execute ```cf push ``` in cf-nodejs-logging-support/sample/cf-logging-support-sample folder 7 | * Visit the endpoint of the app provided by CloudFoundry 8 | 9 | ## Running locally 10 | * Install dependencies by running ```npm install``` 11 | * Execute ```node app.js``` to run the app 12 | * Visit the endpoint of the app (usually http://localhost:8080) -------------------------------------------------------------------------------- /src/lib/helper/envService.ts: -------------------------------------------------------------------------------- 1 | import JSONHelper from './jsonHelper'; 2 | 3 | export default class EnvService { 4 | private static instance: EnvService; 5 | private jsonHelper: JSONHelper; 6 | 7 | constructor() { 8 | this.jsonHelper = new JSONHelper() 9 | } 10 | 11 | static getInstance(): EnvService { 12 | if (!EnvService.instance) { 13 | EnvService.instance = new EnvService(); 14 | } 15 | 16 | return EnvService.instance; 17 | } 18 | 19 | getRuntimeName(): string { 20 | return process.env.VCAP_APPLICATION ? "CF" : "Kyma"; 21 | } 22 | 23 | getBoundServices() { 24 | const boundServices = this.jsonHelper.parseJSONSafe(process.env.VCAP_SERVICES); 25 | return boundServices; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/helper/levelUtils.ts: -------------------------------------------------------------------------------- 1 | import { Level } from '../logger/level'; 2 | 3 | export default class LevelUtils { 4 | 5 | private static readonly defaultLevel: Level = Level.Info 6 | 7 | static getLevel(name: string): Level { 8 | const key = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); 9 | const level: Level = Level[key as keyof typeof Level] 10 | if (level === undefined) { 11 | return LevelUtils.defaultLevel; 12 | } 13 | return level 14 | } 15 | 16 | static getName(level: Level): string { 17 | return Level[level].toLowerCase() 18 | } 19 | 20 | static isLevelEnabled(threshold: Level, level: Level) { 21 | if (level <= Level.Off) return false; 22 | return level <= threshold 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/configuration/default-req-log-level.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Default Request Level 4 | parent: Advanced Configuration 5 | nav_order: 3 6 | permalink: /configuration/default-request-level 7 | --- 8 | 9 | # Set the default request logging level 10 | 11 | Change the default request logging level by setting the property `reqLoggingLevel` in your configuration file. For example: 12 | 13 | ```js 14 | { 15 | "reqLoggingLevel": "info" 16 | } 17 | ``` 18 | 19 | Alternatively, you can set the default request logging level as mentioned in [Dynamic Logging Level Threshold](/cf-nodejs-logging-support/advanced-usage/dynamic-logging-level-threshold) 20 | 21 | Following common logging levels are supported: 22 | 23 | - `error` 24 | - `warn` 25 | - `info` 26 | - `verbose` 27 | - `debug` 28 | - `silly` 29 | -------------------------------------------------------------------------------- /sample/winston-sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "winston-sample", 3 | "version": "1.0.4", 4 | "description": "Sample Node.js app", 5 | "main": "web.js", 6 | "license": "Apache-2.0", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node web.js" 10 | }, 11 | "keywords": [ 12 | "example", 13 | "node.js", 14 | "cloudfoundry", 15 | "winston" 16 | ], 17 | "author": "Nicklas Dohrn, Christian Dinse", 18 | "dependencies": { 19 | "cf-nodejs-logging-support": "latest", 20 | "express": "latest", 21 | "winston": "latest" 22 | }, 23 | "repository": "https://github.com/SAP/cf-nodejs-logging-support/tree/master/sample/winston", 24 | "engines": { 25 | "node": ">=14.14" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/advanced-usage/07-custom-sink-function.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Custom Sink Function 4 | parent: Advanced Usage 5 | nav_order: 7 6 | permalink: /advanced-usage/custom-sink-function 7 | --- 8 | 9 | # Custom Sink Function 10 | Per default the library writes output messages to `stdout`. 11 | For debugging purposes it can be useful to redirect the output of the library to another sink (e.g. `console.log()`). 12 | You can set a custom sink method as follows: 13 | ```js 14 | log.setSinkFunction(function(level, output) { 15 | console.log(output); 16 | }); 17 | ``` 18 | A custom sink function should have two arguments: `level` und `output`. 19 | You can redirect or filter output messages based on their logging level. 20 | 21 | Note: If a custom sink function is set, the library will no longer output messages to the default sink (stdout). 22 | -------------------------------------------------------------------------------- /docs/advanced-usage/05-override-request-log-fields.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Override Request Log Fields 4 | parent: Advanced Usage 5 | nav_order: 5 6 | permalink: /advanced-usage/override-request-log-fields 7 | --- 8 | 9 | # Override Request Log Fields 10 | 11 | Possibility to tailor logs to your needs, you can for example change the msg field for request logs to find them in the Human readable format: 12 | 13 | ```js 14 | log.overrideNetworkField("msg", YOUR_CUSTOM_MSG); 15 | ``` 16 | 17 | This will replace the value of the previously not existing msg field for request logs with YOUR_CUSTOM_MSG. 18 | If the overridden field is already existing, it will be overridden by YOUR_CUSTOM_MSG for ALL subsequent request logs, until you remove the override with: 19 | 20 | ```js 21 | log.overrideNetworkField("msg", null); 22 | ``` 23 | 24 | If you use this override feature in conjunction with a log parser, make sure you will not violate any parsing rules. 25 | -------------------------------------------------------------------------------- /docs/advanced-usage/08-winston-transport.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Winston Transport 4 | parent: Advanced Usage 5 | nav_order: 8 6 | permalink: /advanced-usage/winston-transport 7 | --- 8 | 9 | This logging library can be used in conjunction with Winston. 10 | Logging via Winston transport is limited to message logs. 11 | Network activity can not be tracked automatically. 12 | Example: 13 | ```js 14 | var express = require('express'); 15 | var log = require('cf-nodejs-logging-support'); 16 | var winston = require('winston'); 17 | 18 | var app = express(); 19 | 20 | var logger = winston.createLogger({ 21 | // Bind transport to winston 22 | transports: [log.createWinstonTransport()] 23 | }); 24 | 25 | app.get('/', function (req, res) { 26 | res.send('Hello World'); 27 | }); 28 | 29 | app.listen(3000); 30 | 31 | // Messages will now be logged exactly as demonstrated by the message logs paragraph 32 | logger.log("info", "Server is listening on port %d", 3000); 33 | ``` 34 | -------------------------------------------------------------------------------- /src/lib/winston/winstonTransport.ts: -------------------------------------------------------------------------------- 1 | import TripleBeam from 'triple-beam'; 2 | import TransportStream from 'winston-transport'; 3 | import { Logger } from '../logger/logger'; 4 | 5 | class CfNodejsLoggingSupportLogger extends TransportStream { 6 | 7 | logger: Logger 8 | 9 | constructor(options: any) { 10 | super(options); 11 | this.level = options.level || "info"; 12 | this.logger = options.rootLogger; 13 | } 14 | 15 | log(info: any, callback: () => void) { 16 | setImmediate(() => { 17 | this.emit('logged', info); 18 | }); 19 | 20 | if (info[TripleBeam.SPLAT]) { 21 | this.logger.logMessage.apply(this.logger, [info.level, info.message, ...info[TripleBeam.SPLAT]]); 22 | } else { 23 | this.logger.logMessage(info.level, info.message); 24 | } 25 | 26 | callback(); 27 | } 28 | } 29 | 30 | export default function createTransport(options: any) { 31 | return new CfNodejsLoggingSupportLogger(options); 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": "./tsconfig.json" 6 | }, 7 | "env": { 8 | "es6": true 9 | }, 10 | "ignorePatterns": [ 11 | "node_modules", 12 | "build", 13 | "coverage" 14 | ], 15 | "plugins": [ 16 | "import", 17 | "eslint-comments" 18 | ], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:eslint-comments/recommended", 22 | "plugin:@typescript-eslint/recommended", 23 | "plugin:import/typescript" 24 | ], 25 | "globals": {}, 26 | "rules": { 27 | "@typescript-eslint/explicit-module-boundary-types": "off", 28 | "eslint-comments/disable-enable-pair": [ 29 | "error", 30 | { 31 | "allowWholeFile": true 32 | } 33 | ], 34 | "eslint-comments/no-unused-disable": "error", 35 | "sort-imports": [ 36 | "error", 37 | { 38 | "ignoreDeclarationSort": true, 39 | "ignoreCase": true 40 | } 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [14, 16, 18, 20] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | - run: npm ci 31 | - run: npm run test 32 | - run: npm run coverage 33 | -------------------------------------------------------------------------------- /tools/token-creator/README.md: -------------------------------------------------------------------------------- 1 | # TokenCreator 2 | 3 | This tool helps with the creation of valid JSON Web Tokens for [dynamic log level setting](https://github.com/SAP/cf-nodejs-logging-support#dynamic-log-levels) feature. 4 | 5 | ## Install 6 | Switch to the token-creator folder and execute 7 | ```sh 8 | npm install 9 | ``` 10 | 11 | ## Usage 12 | ### Basic usage 13 | ```node token-creator [options] ``` 14 | 15 | ### Options: 16 | Available command-line options: 17 | ```sh 18 | -k, --key a private key to sign token with 19 | -f, --keyfile a path to a file containing the private key 20 | -v, --validityPeriod number of days the token will expire after 21 | -i, --issuer valid issuer e-mail address 22 | -h, --help output usage information 23 | ``` 24 | 25 | ### Note: 26 | Currently it is neccessary to create a keypair (public and private key) yourself and provide the private key via the ```--key``` or ```--keyfile``` option. 27 | -------------------------------------------------------------------------------- /src/lib/logger/recordWriter.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | import Record from './record'; 4 | 5 | export default class RecordWriter { 6 | 7 | private static instance: RecordWriter; 8 | private customSinkFunction: ((level: string, payload: string) => any) | undefined; 9 | 10 | private constructor() {} 11 | 12 | static getInstance(): RecordWriter { 13 | if (!RecordWriter.instance) { 14 | RecordWriter.instance = new RecordWriter(); 15 | } 16 | 17 | return RecordWriter.instance; 18 | } 19 | 20 | writeLog(record: Record): void { 21 | let level = record.metadata.level; 22 | if (this.customSinkFunction) { 23 | this.customSinkFunction(level, JSON.stringify(record.payload)); 24 | } else { 25 | // default to stdout 26 | process.stdout.write(JSON.stringify(record.payload) + os.EOL); 27 | } 28 | } 29 | 30 | setSinkFunction(f: (level: string, payload: string) => any) { 31 | this.customSinkFunction = f; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/middleware/requestAccessor.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkService } from './interfaces'; 2 | import { assignFrameworkService } from './utils'; 3 | 4 | export default class RequestAccessor { 5 | private static instance: RequestAccessor; 6 | private frameworkService: FrameworkService; 7 | 8 | private constructor() { 9 | this.frameworkService = assignFrameworkService(); 10 | } 11 | 12 | static getInstance(): RequestAccessor { 13 | if (!RequestAccessor.instance) { 14 | RequestAccessor.instance = new RequestAccessor(); 15 | } 16 | return RequestAccessor.instance; 17 | } 18 | 19 | getHeaderField(req: any, fieldName: string): string | undefined { 20 | return this.frameworkService.getReqHeaderField(req, fieldName); 21 | } 22 | 23 | getField(req: any, fieldName: string): any { 24 | return this.frameworkService.getReqField(req, fieldName); 25 | } 26 | 27 | setFrameworkService(): void { 28 | this.frameworkService = assignFrameworkService(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/configuration/framework.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Server Framework 4 | parent: Advanced Configuration 5 | nav_order: 5 6 | permalink: /configuration/framework 7 | --- 8 | 9 | # Set server framework 10 | 11 | By default the library will be configured to run with express. If you are going to use another server framework, you have to change the configuration accordingly. There are 2 ways to do this: 12 | 13 | 1. Set the key-value pair `"framework": "$frameworkName"` in a configuration file: 14 | 15 | ```js 16 | { 17 | "framework": "restify" 18 | } 19 | ``` 20 | 21 | 2. Set the server framework from a logger instance by calling: 22 | 23 | ```js 24 | log.forceLogger("connect") 25 | ``` 26 | 27 | Our supported server frameworks are: 28 | 29 | * [Express](https://expressjs.com/): declare as `express` 30 | * [Restify](http://restify.com/): declare as `restify` 31 | * [Connect](https://www.npmjs.com/package/connect): declare as `connect` 32 | * [Fastify](https://fastify.dev/): declare as `fastify` 33 | * [Node.js HTTP](https://nodejs.org/api/http.html): declare as `plainhttp` 34 | -------------------------------------------------------------------------------- /src/lib/logger/context.ts: -------------------------------------------------------------------------------- 1 | import Config from '../config/config'; 2 | import { Output } from '../config/interfaces'; 3 | import SourceUtils from './sourceUtils'; 4 | 5 | export default class Context { 6 | private properties: any = {}; 7 | private config: Config; 8 | private sourceUtils: SourceUtils; 9 | 10 | constructor(req?: any) { 11 | this.config = Config.getInstance(); 12 | this.sourceUtils = SourceUtils.getInstance(); 13 | this.assignProperties(req); 14 | } 15 | 16 | getProperty(key: string): string | undefined { 17 | return this.properties[key]; 18 | } 19 | 20 | getProperties(): any { 21 | return this.properties; 22 | } 23 | 24 | setProperty(key: string, value: string) { 25 | this.properties[key] = value; 26 | } 27 | 28 | private assignProperties(req: any) { 29 | const contextFields = this.config.getContextFields(); 30 | contextFields.forEach(field => { 31 | this.properties[field.name] = this.sourceUtils.getValue(field, this.properties, Output.ReqLog, req, null); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/getting-started/01-installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Installation 4 | parent: Getting Started 5 | nav_order: 1 6 | permalink: /getting-started/installation/ 7 | --- 8 | 9 | # Installation 10 | 11 | The latest release version can be downloaded from [npm](https://www.npmjs.com/package/cf-nodejs-logging-support) and [github](https://github.com/SAP/cf-nodejs-logging-support/releases). 12 | 13 | ## Requirements 14 | 15 | To take full advantage of cf-nodejs-logging-support make sure to fulfill following requirements: 16 | 17 | * Node.js app to be deployed on Cloud Foundry 18 | * Use [node.js](https://nodejs.org/) version 14.14 or higher 19 | * Use one of the supported server frameworks: 20 | * [Express](https://expressjs.com/) 21 | * [Connect](https://www.npmjs.com/package/connect) 22 | * [Restify](http://restify.com/) 23 | * [Fastify](https://fastify.dev/) 24 | * [Node.js HTTP](https://nodejs.org/api/http.html) 25 | 26 | ## Install using npm 27 | 28 | Use following command to add cf-nodejs-logging-support and its dependencies to your app. 29 | 30 | ```bash 31 | npm install cf-nodejs-logging-support --save 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/sample-apps/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Sample Apps 4 | nav_order: 6 5 | has_children: true 6 | --- 7 | 8 | # Sample Apps 9 | 10 | ## cf-nodejs-sample-app 11 | 12 | To get a better understanding of how this library can be used to write request and message logs in actual Node.js applications take a look at our [cf-node-sample-app](https://github.com/SAP/cf-nodejs-logging-support/tree/master/sample/cf-nodejs-logging-support-sample). 13 | The application shows buttons to test different features and can be deployed on Cloud Foundry. 14 | 15 | ## cf-nodejs-shoutboard 16 | 17 | To get a better understanding of how this library can be used to write request and message logs in actual Node.js applications take a look at our [cf-node-shoutboard](https://github.com/SAP/cf-nodejs-logging-support/tree/master/sample/cf-nodejs-shoutboard). 18 | The application hosts a simple shoutboard and can be deployed on Cloud Foundry. 19 | 20 | ## winston-sample 21 | 22 | Have a look at our minimal [winston-sample](https://github.com/SAP/cf-nodejs-logging-support/tree/master/sample/winston-sample) application to learn how to use our Winston transport. 23 | -------------------------------------------------------------------------------- /src/lib/middleware/responseAccessor.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkService } from './interfaces'; 2 | import { assignFrameworkService } from './utils'; 3 | 4 | export default class ResponseAccessor { 5 | private static instance: ResponseAccessor; 6 | private frameworkService: FrameworkService; 7 | 8 | private constructor() { 9 | this.frameworkService = assignFrameworkService(); 10 | } 11 | 12 | static getInstance(): ResponseAccessor { 13 | if (!ResponseAccessor.instance) { 14 | ResponseAccessor.instance = new ResponseAccessor(); 15 | } 16 | return ResponseAccessor.instance; 17 | } 18 | 19 | getHeaderField(res: any, fieldName: string): string | undefined { 20 | return this.frameworkService.getResHeaderField(res, fieldName); 21 | } 22 | 23 | getField(res: any, fieldName: string): any { 24 | return this.frameworkService.getResField(res, fieldName); 25 | } 26 | 27 | onFinish(res: any, handler: () => void): void { 28 | this.frameworkService.onResFinish(res, handler); 29 | } 30 | 31 | setFrameworkService(): void { 32 | this.frameworkService = assignFrameworkService(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/acceptance-test/nodejs-http/app.js: -------------------------------------------------------------------------------- 1 | const importFresh = require('import-fresh'); 2 | const http = importFresh('http'); 3 | const log = importFresh('../../../../build/main/index'); 4 | 5 | // Force logger to run the http version. 6 | log.forceLogger("plainhttp"); 7 | 8 | const server = http.createServer((req, res) => { 9 | // Binds logging to the given request for request tracking 10 | log.logNetwork(req, res); 11 | 12 | var correlationId = "cbc2654f-1c35-45d0-96fc-f32efac20986"; 13 | var tenantId = "abc8714f-5t15-12h0-78gt-n73jeuc01847"; 14 | 15 | if (req.url == '/setcorrelationandtenantid') { 16 | req.logger.setCorrelationId(correlationId); 17 | req.logger.setTenantId(tenantId); 18 | } 19 | 20 | if (req.url == '/testget') { 21 | req.logger.setCorrelationId(correlationId); 22 | req.logger.setTenantId(tenantId); 23 | 24 | if (req.logger.getCorrelationId() == correlationId && req.logger.getTenantId() == tenantId) { 25 | req.logger.logMessage("info", "successful"); 26 | } 27 | res.end(); 28 | } 29 | // Context bound custom message 30 | req.logger.logMessage("info", "http-message"); 31 | res.end(); 32 | }); 33 | 34 | module.exports = server; 35 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/public/scripts/logic.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | 3 | const collection = Array.from(document.getElementsByClassName("btn")); 4 | const feedback = document.getElementById("feedback"); 5 | const binding = document.getElementById("binding_information"); 6 | 7 | // Getting currently working binding information 8 | var xhttp = new XMLHttpRequest(); 9 | xhttp.open("GET", "binding_information"); 10 | xhttp.send(); 11 | xhttp.onreadystatechange = function() { 12 | if (xhttp.readyState == XMLHttpRequest.DONE) { 13 | console.log("testing") 14 | console.log(xhttp.responseText) 15 | binding.innerHTML = xhttp.responseText 16 | } 17 | } 18 | 19 | collection.forEach( 20 | (element) => { 21 | element.addEventListener("click", () => { 22 | var xhttp = new XMLHttpRequest(); 23 | xhttp.open("GET", element.getAttribute('targetpath')); 24 | xhttp.send(); 25 | xhttp.onreadystatechange = function() { 26 | if (xhttp.readyState == XMLHttpRequest.DONE) { 27 | console.log(xhttp.responseText) 28 | feedback.innerHTML= xhttp.responseText; 29 | } 30 | } 31 | }); 32 | } 33 | ); 34 | }) 35 | -------------------------------------------------------------------------------- /src/lib/middleware/utils.ts: -------------------------------------------------------------------------------- 1 | import Config from '../config/config'; 2 | import { Framework } from '../config/interfaces'; 3 | import ConnectService from './framework-services/connect'; 4 | import ExpressService from './framework-services/express'; 5 | import FastifyService from './framework-services/fastify'; 6 | import HttpService from './framework-services/plainhttp'; 7 | import RestifyService from './framework-services/restify'; 8 | import { FrameworkService } from './interfaces'; 9 | 10 | export function assignFrameworkService(): FrameworkService { 11 | const framework = Config.getInstance().getFramework(); 12 | switch (framework) { 13 | case Framework.Restify: 14 | return new RestifyService(); 15 | case Framework.NodeJsHttp: 16 | return new HttpService(); 17 | case Framework.Connect: 18 | return new ConnectService(); 19 | case Framework.Fastify: 20 | return new FastifyService(); 21 | case Framework.Express: 22 | default: 23 | return new ExpressService(); 24 | } 25 | } 26 | 27 | export function isValidObject(obj: any, canBeEmpty?: boolean): boolean { 28 | if (!obj) { 29 | return false; 30 | } else if (typeof obj !== "object") { 31 | return false; 32 | } else if (!canBeEmpty && Object.keys(obj).length === 0) { 33 | return false; 34 | } 35 | return true; 36 | } 37 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | # gem "jekyll", "~> 4.2.0" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | #gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | gem "github-pages", "~> 228", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | # https://pmarsceill.github.io/just-the-docs/#getting-started 32 | gem "just-the-docs" 33 | 34 | 35 | gem "webrick", "~> 1.8" 36 | 37 | gem "jekyll", "~> 3.9" 38 | -------------------------------------------------------------------------------- /docs/configuration/custom-fields-format.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Custom Fields Format 4 | parent: Advanced Configuration 5 | nav_order: 4 6 | permalink: /configuration/custom-fields-format 7 | --- 8 | 9 | # Custom fields format and type conversion 10 | 11 | 12 | 13 | As described in [Custom Fields](/cf-nodejs-logging-support/general-usage/custom-fields), the library supports different formats for logging custom fields to ensure compatibility with SAP logging services. 14 | When using user-provided services or running in environments without service bindings, you might want to explicitly set the custom fields format. 15 | Besides programmatic configuration, this can also be achieved via by adding the following settings to your configuration file. 16 | 17 | Example: 18 | 19 | ```js 20 | { 21 | "customFieldsFormat": "application-logging" 22 | } 23 | ``` 24 | 25 | Supported format values: 26 | 27 | * `application-logging`: to be used with SAP Application Logging 28 | * `cloud-logging`: to be used with SAP Cloud Logging 29 | * `all`: use application-logging and cloud-logging format in parallel. 30 | * `disabled`: do not log any custom fields. 31 | * `default`: set default format cloud-logging. 32 | 33 | Additionally, the `customFieldsTypeConversion` setting can be set when logging in cloud-logging format: 34 | 35 | * `stringify`: convert all custom field values to strings 36 | * `retain`: keep the original custom field value types 37 | 38 | -------------------------------------------------------------------------------- /docs/general-usage/01-request-logs.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Request Logs 4 | parent: General Usage 5 | nav_order: 1 6 | permalink: /general-usage/request-logs 7 | --- 8 | 9 | # Request Logs 10 | {: .no_toc } 11 | Logging so called 'request logs' is the main feature of this logging library. 12 | Request logs get issued for each handled http/s request and contain information about the request itself, its response and also CF metadata. 13 | 14 |
15 | 16 | Table of contents 17 | 18 | {: .text-delta } 19 | 1. TOC 20 | {:toc} 21 |
22 | 23 | ## Attaching to server frameworks 24 | The library can be provided as middleware to log requests as follows: 25 | ```js 26 | app.use(log.logNetwork); 27 | ``` 28 | 29 | When using a plain Node.js http server it is necessary to call the network logging function directly: 30 | ```js 31 | http.createServer((req, res) => { 32 | log.logNetwork(req, res); 33 | ... 34 | }); 35 | ``` 36 | 37 | You can find code samples for supported server frameworks in the [Framework Samples](/cf-nodejs-logging-support/getting-started/framework-samples/) section. 38 | 39 | ## Setting the log level for requests 40 | 41 | You can set the [level](/cf-nodejs-logging-support/general-usage/message-logs#logging-levels) to be assigned to request logs as follows: 42 | ```js 43 | log.setRequestLogLevel("verbose"); 44 | ``` 45 | The default log level for request logs is ``info``. 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Logging Support for Cloud Foundry 2 | 3 | [![Version npm](https://img.shields.io/npm/v/cf-nodejs-logging-support.svg)](https://www.npmjs.com/package/cf-nodejs-logging-support) 4 | [![npm Downloads](https://img.shields.io/npm/dm/cf-nodejs-logging-support.svg)](https://www.npmjs.com/package/cf-nodejs-logging-support) 5 | [![Node.js CI](https://github.com/SAP/cf-nodejs-logging-support/actions/workflows/node.js.yml/badge.svg)](https://github.com/SAP/cf-nodejs-logging-support/actions/workflows/node.js.yml) 6 | [![REUSE status](https://api.reuse.software/badge/github.com/SAP/cf-nodejs-logging-support)](https://api.reuse.software/info/github.com/SAP/cf-nodejs-logging-support) 7 | 8 | ## Summary 9 | 10 | This library provides a bundle of targeted logging services for node.js applications running on Cloud Foundry which serves two main purposes: 11 | It provides means to emit *structured application log messages* and instrument parts of your application stack to *collect request metrics*. 12 | 13 | ## Documentation 14 | 15 | Head over to our [Documentation](https://sap.github.io/cf-nodejs-logging-support/) to learn more about features and how to use this library. 16 | 17 | ## Licensing 18 | 19 | Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the [REUSE](https://api.reuse.software/info/github.com/SAP/cf-nodejs-logging-support) tool. 20 | -------------------------------------------------------------------------------- /src/test/acceptance-test/connect/app.js: -------------------------------------------------------------------------------- 1 | const importFresh = require('import-fresh'); 2 | const connect = importFresh('connect'); 3 | const log = importFresh('../../../../build/main/index'); 4 | const http = importFresh('http'); 5 | const app = connect(); 6 | 7 | // Force logger to run the connect version. (default is express, forcing express is also legal) 8 | log.forceLogger("connect"); 9 | 10 | // Add the logger middleware, so each time a request is received, it is will get logged. 11 | app.use(log.logNetwork); 12 | 13 | app.use("/log", function (req, res) { 14 | req.logger.setLoggingLevel("info"); 15 | req.logger.logMessage("info", "connect-message"); 16 | res.end(); 17 | }); 18 | 19 | app.use("/setcorrelationandtenantid", function (req, res) { 20 | req.logger.setCorrelationId("cbc2654f-1c35-45d0-96fc-f32efac20986"); 21 | req.logger.setTenantId("abc8714f-5t15-12h0-78gt-n73jeuc01847"); 22 | req.logger.logMessage("info", "connect-message"); 23 | res.end(); 24 | }); 25 | 26 | app.use("/getcorrelationandtenantid", function (req, res) { 27 | var correlationId = "cbc2654f-1c35-45d0-96fc-f32efac20986"; 28 | var tenantId = "abc8714f-5t15-12h0-78gt-n73jeuc01847"; 29 | 30 | req.logger.setCorrelationId(correlationId); 31 | req.logger.setTenantId(tenantId); 32 | 33 | if (req.logger.getCorrelationId() == correlationId && req.logger.getTenantId() == tenantId) { 34 | req.logger.logMessage("info", "successful"); 35 | } 36 | res.end(); 37 | }); 38 | 39 | module.exports = http.createServer(app); 40 | -------------------------------------------------------------------------------- /src/test/acceptance-test/restify/app.js: -------------------------------------------------------------------------------- 1 | const restify = require('restify'); 2 | const importFresh = require('import-fresh'); 3 | const log = importFresh('../../../../build/main/index'); 4 | const app = restify.createServer(); 5 | 6 | // Force logger to run the restify version. (default is express, forcing express is also legal) 7 | log.forceLogger("restify"); 8 | 9 | // Add the logger middleware, so each time a request is received, it is will get logged. 10 | app.use(log.logNetwork); 11 | 12 | app.get('/log', (req, res, next) => { 13 | req.logger.logMessage("info", "restify-message"); 14 | res.send(); 15 | }); 16 | 17 | 18 | app.get('/setcorrelationandtenantid', (req, res, next) => { 19 | var correlationId = "cbc2654f-1c35-45d0-96fc-f32efac20986"; 20 | var tenantId = "abc8714f-5t15-12h0-78gt-n73jeuc01847"; 21 | 22 | req.logger.setCorrelationId(correlationId); 23 | req.logger.setTenantId(tenantId); 24 | 25 | req.logger.logMessage("info", "restify-message"); 26 | res.send(); 27 | }); 28 | 29 | app.get("/getcorrelationandtenantid", function (req, res, next) { 30 | var correlationId = "cbc2654f-1c35-45d0-96fc-f32efac20986"; 31 | var tenantId = "abc8714f-5t15-12h0-78gt-n73jeuc01847"; 32 | 33 | req.logger.setCorrelationId(correlationId); 34 | req.logger.setTenantId(tenantId); 35 | 36 | if (req.logger.getCorrelationId() == correlationId && req.logger.getTenantId() == tenantId) { 37 | req.logger.logMessage("info", "successful"); 38 | } 39 | res.send(); 40 | }); 41 | 42 | module.exports = app; 43 | -------------------------------------------------------------------------------- /src/test/acceptance-test/fastify/app.js: -------------------------------------------------------------------------------- 1 | 2 | const importFresh = require('import-fresh'); 3 | const fastify = importFresh('fastify'); 4 | const log = importFresh('../../../../build/main/index'); 5 | const app = fastify(); 6 | 7 | // Force logger to run the fastify version. 8 | log.forceLogger("fastify"); 9 | 10 | // Add the logger middleware, so each time a request is received, it is will get logged. 11 | app.addHook("onRequest", log.logNetwork); 12 | 13 | app.get('/log', function (request, reply) { 14 | request.logger.setLoggingLevel("info"); 15 | request.logger.logMessage("info", "fastify-message"); 16 | reply.send(); 17 | }) 18 | 19 | app.get("/setcorrelationandtenantid", function (request, reply) { 20 | request.logger.setCorrelationId("cbc2654f-1c35-45d0-96fc-f32efac20986"); 21 | request.logger.setTenantId("abc8714f-5t15-12h0-78gt-n73jeuc01847"); 22 | request.logger.logMessage("info", "fastify-message"); 23 | reply.send(); 24 | }); 25 | 26 | app.get("/getcorrelationandtenantid", function (request, reply) { 27 | var correlationId = "cbc2654f-1c35-45d0-96fc-f32efac20986"; 28 | var tenantId = "abc8714f-5t15-12h0-78gt-n73jeuc01847"; 29 | 30 | request.logger.setCorrelationId(correlationId); 31 | request.logger.setTenantId(tenantId); 32 | 33 | if (request.logger.getCorrelationId() == correlationId && request.logger.getTenantId() == tenantId) { 34 | request.logger.logMessage("info", "successful"); 35 | } 36 | reply.send(); 37 | }); 38 | 39 | module.exports = app; 40 | -------------------------------------------------------------------------------- /docs/general-usage/05-stacktraces.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Logging Stack Traces 4 | parent: General Usage 5 | nav_order: 5 6 | permalink: /general-usage/stacktraces 7 | --- 8 | 9 | # Stack Traces 10 | 11 | Stack traces can be written to the `stacktrace` field of the log and are represented as an array of strings. 12 | 13 | If a stack trace exceeds a total size of 55kB, it will not be logged entirely. 14 | The library removes as few lines as necessary from the middle of the stack trace since the relevant parts are usually at the start and the end. 15 | We use following strategy to do so: 16 | 17 | - Take one line from the top and two lines from the bottom of the stacktrace until the limit is reached. 18 | 19 | For writing stack traces as part of a message log you can append the `Error` object as follows: 20 | 21 | ```js 22 | try { 23 | // Code throwing an Error 24 | } catch (e) { 25 | logger.error("Error occurred", e) 26 | } 27 | // ... "msg":"Error occurred", "stacktrace": [...] ... 28 | ``` 29 | 30 | In case you want to a log stack traces along with custom fields you can attach it as follows: 31 | 32 | ```js 33 | try { 34 | // Code throwing an Error 35 | } catch (e) { 36 | logger.error("Error occurred", {"custom-field" :"value", "_error": e}) 37 | } 38 | // ... "msg":"Error occurred", "stacktrace": [...] ... 39 | ``` 40 | 41 | The `_error` field gets handled separately and its stack trace gets written to the `stacktrace` field. 42 | The given custom fields get handled as described in [Custom Fields](/cf-nodejs-logging-support/general-usage/custom-fields). 43 | -------------------------------------------------------------------------------- /src/lib/middleware/framework-services/connect.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkService } from "../interfaces"; 2 | 3 | export default class ConnectService implements FrameworkService { 4 | getReqHeaderField(req: any, fieldName: string): string | undefined { 5 | return req.headers[fieldName]; 6 | } 7 | 8 | getReqField(req: any, fieldName: string): any { 9 | let value: string | number | boolean | undefined = undefined; 10 | switch (fieldName) { 11 | case "protocol": 12 | value = "HTTP" + (req.httpVersion == null ? "" : "/" + req.httpVersion); 13 | break; 14 | case "remote_host": 15 | value = req.connection?.remoteAddress; 16 | break; 17 | case "remote_port": 18 | value = req.connection?.remotePort?.toString(); 19 | break; 20 | case "remote_user": 21 | if (req.user && req.user.id) { 22 | value = req.user.id; 23 | } 24 | break; 25 | default: 26 | value = req[fieldName] 27 | break; 28 | } 29 | return value 30 | } 31 | 32 | getResHeaderField(res: any, fieldName: string): string | undefined { 33 | return res.getHeader ? res.getHeader(fieldName) : undefined; 34 | } 35 | 36 | getResField(res: any, fieldName: string): any { 37 | return res[fieldName]; 38 | } 39 | 40 | onResFinish(res: any, handler: () => void): void { 41 | res.on("header", handler); 42 | res.on("finish", handler); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/middleware/framework-services/plainhttp.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkService } from "../interfaces"; 2 | 3 | export default class HttpService implements FrameworkService { 4 | 5 | getReqHeaderField(req: any, fieldName: string): string | undefined { 6 | return req.headers[fieldName]; 7 | } 8 | 9 | getReqField(req: any, fieldName: string): any { 10 | let value: string | number | boolean | undefined = undefined; 11 | switch (fieldName) { 12 | case "protocol": 13 | value = "HTTP" + (req.httpVersion == null ? "" : "/" + req.httpVersion); 14 | break; 15 | case "remote_host": 16 | value = req.connection?.remoteAddress; 17 | break; 18 | case "remote_port": 19 | value = req.connection?.remotePort?.toString(); 20 | break; 21 | case "remote_user": 22 | if (req.user && req.user.id) { 23 | value = req.user.id; 24 | } 25 | break; 26 | default: 27 | value = req[fieldName] 28 | break; 29 | } 30 | return value 31 | } 32 | 33 | getResHeaderField(res: any, fieldName: string): string | undefined { 34 | return res.getHeader ? res.getHeader(fieldName) : undefined; 35 | } 36 | 37 | getResField(res: any, fieldName: string): any { 38 | return res[fieldName]; 39 | } 40 | 41 | onResFinish(res: any, handler: () => void): void { 42 | res.on("header", handler); 43 | res.on("finish", handler); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/middleware/framework-services/restify.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkService } from "../interfaces"; 2 | 3 | export default class RestifyService implements FrameworkService { 4 | 5 | getReqHeaderField(req: any, fieldName: string): string | undefined { 6 | return req ? req.header(fieldName): undefined; 7 | } 8 | 9 | getReqField(req: any, fieldName: string): any { 10 | let value: string | number | boolean | undefined = undefined; 11 | switch (fieldName) { 12 | case "protocol": 13 | value = "HTTP" + (req.httpVersion == null ? "" : "/" + req.httpVersion); 14 | break; 15 | case "remote_host": 16 | value = req.connection?.remoteAddress; 17 | break; 18 | case "remote_port": 19 | value = req.connection?.remotePort?.toString(); 20 | break; 21 | case "remote_user": 22 | if (req.user && req.user.id) { 23 | value = req.user.id; 24 | } 25 | break; 26 | default: 27 | value = req[fieldName] 28 | break; 29 | } 30 | return value 31 | } 32 | 33 | getResHeaderField(res: any, fieldName: string): string | undefined { 34 | return res.get ? res.get(fieldName) : undefined; 35 | } 36 | 37 | getResField(res: any, fieldName: string): any { 38 | return res[fieldName]; 39 | } 40 | 41 | onResFinish(res: any, handler: () => void): void { 42 | res.on("header", handler); 43 | res.on("finish", handler); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/middleware/framework-services/express.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkService } from "../interfaces"; 2 | 3 | export default class ExpressService implements FrameworkService { 4 | 5 | getReqHeaderField(req: any, fieldName: string): string | undefined { 6 | return req.header ? req.header(fieldName) : undefined; 7 | } 8 | 9 | getReqField(req: any, fieldName: string): any { 10 | let value: string | number | boolean | undefined = undefined; 11 | switch (fieldName) { 12 | case "protocol": 13 | value = "HTTP" + (req.httpVersion == null ? "" : "/" + req.httpVersion); 14 | break; 15 | case "remote_host": 16 | value = req.connection?.remoteAddress; 17 | break; 18 | case "remote_port": 19 | value = req.connection?.remotePort?.toString(); 20 | break; 21 | case "remote_user": 22 | if (req.user && req.user.id) { 23 | value = req.user.id; 24 | } 25 | break; 26 | default: 27 | value = req[fieldName] 28 | break; 29 | } 30 | return value 31 | } 32 | 33 | getResHeaderField(res: any, fieldName: string): string | undefined { 34 | return res.get ? res.get(fieldName) : undefined; 35 | } 36 | 37 | getResField(res: any, fieldName: string): any { 38 | return res[fieldName]; 39 | } 40 | 41 | onResFinish(res: any, handler: () => void): void { 42 | res.on("header", handler); 43 | res.on("finish", handler); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/middleware/framework-services/fastify.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkService } from "../interfaces"; 2 | 3 | export default class FastifyService implements FrameworkService { 4 | 5 | getReqHeaderField(req: any, fieldName: string): string | undefined { 6 | return req.headers[fieldName] 7 | } 8 | 9 | getReqField(req: any, fieldName: string): any { 10 | let value: string | number | boolean | undefined = undefined; 11 | switch (fieldName) { 12 | case "protocol": 13 | value = "HTTP" + (req.raw.httpVersion == null ? "" : "/" + req.raw.httpVersion); 14 | break; 15 | case "remote_host": 16 | value = req.raw.connection?.remoteAddress; 17 | break; 18 | case "remote_port": 19 | value = req.raw.connection?.remotePort?.toString(); 20 | break; 21 | case "remote_user": 22 | if (req.raw.user && req.raw.user.id) { 23 | value = req.raw.user.id; 24 | } 25 | break; 26 | default: 27 | value = req.raw[fieldName] 28 | break; 29 | } 30 | return value 31 | } 32 | 33 | getResHeaderField(res: any, fieldName: string): string | undefined { 34 | return res.getHeader ? res.getHeader(fieldName) : undefined 35 | } 36 | 37 | getResField(res: any, fieldName: string): any { 38 | return res.raw[fieldName]; 39 | } 40 | 41 | onResFinish(res: any, handler: () => void): void { 42 | res.raw.on("header", handler); 43 | res.raw.on("finish", handler); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/helper/envVarHelper.ts: -------------------------------------------------------------------------------- 1 | export default class EnvVarHelper { 2 | 3 | private static instance: EnvVarHelper; 4 | 5 | static getInstance(): EnvVarHelper { 6 | if (!EnvVarHelper.instance) { 7 | EnvVarHelper.instance = new EnvVarHelper(); 8 | } 9 | 10 | return EnvVarHelper.instance; 11 | } 12 | 13 | isVarEnabled(name: string): boolean { 14 | const value = process.env[name]; 15 | return (value == "true" || value == "True" || value == "TRUE") 16 | } 17 | 18 | 19 | resolveNestedVar(path: string[]): string | undefined { 20 | const copiedPath = [...path]; 21 | return this.resolve(process.env, copiedPath) 22 | } 23 | 24 | private resolve(root: any, path: string[]): string | undefined { 25 | // return, if path is empty. 26 | if (path == null || path.length == 0) { 27 | return undefined; 28 | } 29 | 30 | let rootObj; 31 | 32 | // if root is a string => parse it to an object. Otherwise => use it directly as object. 33 | if (typeof root === "string") { 34 | rootObj = JSON.parse(root); 35 | } else if (typeof root === "object") { 36 | rootObj = root; 37 | } else { 38 | return undefined; 39 | } 40 | 41 | // get value from root object 42 | let value = rootObj[path[0]]; 43 | 44 | // cut first entry of the object path 45 | path.shift(); 46 | 47 | // if the path is not empty, recursively resolve the remaining waypoints. 48 | if (path.length >= 1) { 49 | return this.resolve(value, path); 50 | } 51 | 52 | // return the resolved value, if path is empty. 53 | return value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/advanced-usage/06-sap-passort.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: SAP Passport 4 | parent: Advanced Usage 5 | nav_order: 6 6 | permalink: /advanced-usage/sap-passport 7 | --- 8 | 9 | # SAP Passport 10 | 11 | SAP Passport is an end to end tracing technology used in many SAP products. 12 | It is a binary format encoded in hex notation. 13 | Example: 14 | 15 | ```text 16 | 2a54482a0300e60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a54482a 17 | ``` 18 | 19 | Applications can add the whole SAP Passport in this field or give its constituents in the respective fields. 20 | 21 | To read up on the possible fields, please look at [fields](https://github.com/SAP/cf-java-logging-support/blob/master/cf-java-logging-support-core/beats/app-logs/docs/fields.asciidoc). 22 | 23 | You can activate the SAP Passport as follows: 24 | 25 | ```js 26 | log.enableTracing("sap_passport") 27 | ``` 28 | 29 | After activating the SAP Passport the property `sap_passport` will be added automatically to the configuration. This will read the property `sap-passport` from your request header. 30 | 31 | To add its constituents related fields use the `setCustomFields` method. These fields will always be attached directly in the log object as normal fields regardless of the custom fields format. 32 | 33 | Example for adding SAP Passport related fields: 34 | 35 | ```js 36 | log.setCustomFields({"sap_passport_Action":"value", "sap_passport_ClientNumber":"1234"}) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Migrate to Version 7 4 | permalink: /migration/ 5 | nav_order: 2 6 | --- 7 | 8 | # Migrate from Version 6.x to Version 7.x 9 | 10 | Version 7.x introduces a redesigned architecture based on TypeScript. 11 | This enables the customization of generated fields and field contents and get rid of obsolete features. 12 | Below, you can find a description of discontinued, changed, and new features. 13 | Please let us know if you experience any unexpected behavior or problems that are not listed yet. 14 | 15 | ## New Configuration Concept 16 | 17 | Switching to v7 allows you to change the configuration without having to rebuild from source. 18 | Similar to the previous version, v7 loads default field configurations automatically based on the detected runtime environment and bound logging services without any further configuration. 19 | See [Advanced Configuration](/cf-nodejs-logging-support/configuration) to learn more about the new configuration concept. 20 | 21 | ## Typescript Typings 22 | 23 | Typescript typings are available for log levels and configuration types. 24 | Typings can be imported in addition to the default import: 25 | 26 | ```ts 27 | import log, { Level, Framework } from "cf-nodejs-logging-support"; 28 | 29 | log.setLoggingLevel(Level.Info) 30 | log.setFramework(Framework.Express) 31 | ``` 32 | 33 | 34 | ## Omit default values 35 | 36 | The new version no longer writes default values (e.g. `-`) for unresolved values but omits the fields instead. 37 | 38 | ## Discontinued log pattern feature 39 | 40 | The log pattern feature (`log.setLogPattern()`) has been removed. 41 | It was used to set a custom formatting pattern for printing logs in a human-readable format. 42 | A similar behavior can be achieved by configuring a custom sink function that outputs the logs in the desired format. 43 | -------------------------------------------------------------------------------- /src/test/config-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { 4 | "name": "logger", 5 | "source": { 6 | "type": "static", 7 | "value": "TEST" 8 | }, 9 | "output": [ 10 | "msg-log", 11 | "req-log" 12 | ] 13 | }, 14 | { 15 | "name": "disabled_field", 16 | "source": { 17 | "type": "config-field", 18 | "fieldName": "component_instance" 19 | }, 20 | "output": [ 21 | "msg-log" 22 | ], 23 | "disable": true 24 | }, 25 | { 26 | "name": "uuid_field", 27 | "source": { 28 | "type": "static", 29 | "value": "8888c6e8-f44e-4a33-a444-1eadd1234567", 30 | "regExp": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}" 31 | }, 32 | "output": [ 33 | "msg-log", 34 | "req-log" 35 | ] 36 | }, 37 | { 38 | "name": "will_never_log", 39 | "source": { 40 | "type": "static", 41 | "value": "1234c6e8-f23e-1dd4-a1-1eadd69c3665", 42 | "regExp": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}" 43 | }, 44 | "output": [ 45 | "msg-log", 46 | "req-log" 47 | ] 48 | }, 49 | { 50 | "name": "new_field", 51 | "source": { 52 | "type": "config-field", 53 | "fieldName": "component_instance" 54 | }, 55 | "output": [ 56 | "msg-log" 57 | ] 58 | } 59 | ], 60 | "customFieldsFormat": "cloud-logging", 61 | "outputStartupMsg": false, 62 | "reqLoggingLevel": "info" 63 | } 64 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "CF NodeJS Logging Support" 3 | SPDX-PackageSupplier = "" 4 | SPDX-PackageDownloadLocation = "https://github.com/SAP/cf-nodejs-logging-support" 5 | SPDX-PackageComment = "The code in this project may include calls to APIs (“API Calls”) of\n SAP or third-party products or services developed outside of this project\n (“External Products”).\n “APIs” means application programming interfaces, as well as their respective\n specifications and implementing code that allows software to communicate with\n other software.\n API Calls to External Products are not licensed under the open source license\n that governs this project. The use of such API Calls and related External\n Products are subject to applicable additional agreements with the relevant\n provider of the External Products. In no event shall the open source license\n that governs this project grant any rights in or to any External Products,or\n alter, expand or supersede any terms of the applicable additional agreements.\n If you have a valid license agreement with SAP for the use of a particular SAP\n External Product, then you may make use of any API Calls included in this\n project’s code for that SAP External Product, subject to the terms of such\n license agreement. If you do not have a valid license agreement for the use of\n a particular SAP External Product, then you may only make use of any API Calls\n in this project for that SAP External Product for your internal, non-productive\n and non-commercial test and evaluation of such API Calls. Nothing herein grants\n you any rights to use or access any SAP External Product, or provide any third\n parties the right to use of access any SAP External Product, through API Calls." 6 | 7 | [[annotations]] 8 | path = "**" 9 | precedence = "aggregate" 10 | SPDX-FileCopyrightText = "2020 SAP SE or an SAP affiliate company and CF NodeJS Logging Support contributors" 11 | SPDX-License-Identifier = "Apache-2.0" 12 | -------------------------------------------------------------------------------- /docs/advanced-usage/02-correlation-and-tenant.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Correlation and Tenant Data 4 | parent: Advanced Usage 5 | nav_order: 2 6 | permalink: /advanced-usage/correlation-and-tenant-data 7 | --- 8 | 9 | # Getting and setting correlation and tenant data 10 | {: .no_toc } 11 | To further propagate correlation and tenant information to subsequent requests or processes you can extract them from the request context. 12 | 13 |
14 | 15 | Table of contents 16 | 17 | {: .text-delta } 18 | 1. TOC 19 | {:toc} 20 |
21 | 22 | ## Correlation ID 23 | In order to get the `correlation_id` of a request you can use the following method: 24 | ```js 25 | var id = req.logger.getCorrelationId(); 26 | ``` 27 | 28 | It is also possible to change the `correlation_id` to a valid UUIDv4: 29 | ```js 30 | req.logger.setCorrelationId("cbc2654f-1c35-45d0-96fc-f32efac20986"); 31 | ``` 32 | 33 | ## Tenant ID 34 | In order to get the `tenant_id` of a request you can call the following method: 35 | ```js 36 | var tenantId = req.logger.getTenantId(); 37 | ``` 38 | 39 | It is also possible to change the `tenant_id` to any string value: 40 | ```js 41 | req.logger.setTenantId("cbc2654f-1c35-45d0-96fc-f32efac20986"); 42 | ``` 43 | 44 | Be aware that changing the `tenant_id` for a logger will also affect ancestor and descendant loggers within the same request context, especially the network log for this request will contain the new `tenant_id`. 45 | 46 | 47 | ## Tenant Subdomain 48 | The `tenant_subdomain` does **not** get determined automatically. 49 | However, you can set it per request as follows: 50 | 51 | ```js 52 | req.logger.setTenantSubdomain("my-subdomain"); 53 | ``` 54 | 55 | Be aware that changing the tenant_subdomain for a logger will also affect ancestor and descendant loggers within the same request context, especially the network log for this request will contain the new tenant_subdomain. 56 | -------------------------------------------------------------------------------- /src/lib/helper/jwtService.ts: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const ENV_DYN_LOG_HEADER = "DYN_LOG_HEADER"; 3 | const ENV_DYN_LOG_KEY = "DYN_LOG_LEVEL_KEY"; 4 | const DEFAULT_DYN_LOG_LEVEL_HEADER = "SAP-LOG-LEVEL"; 5 | 6 | export default class JWTService { 7 | private static instance: JWTService; 8 | private dynLogLevelHeader: string; 9 | 10 | private constructor() { 11 | // Read dynamic log level header name from environment var 12 | const headerName = process.env[ENV_DYN_LOG_HEADER]; 13 | this.dynLogLevelHeader = headerName ? headerName : DEFAULT_DYN_LOG_LEVEL_HEADER; 14 | } 15 | 16 | static getInstance(): JWTService { 17 | if (!JWTService.instance) { 18 | JWTService.instance = new JWTService(); 19 | } 20 | 21 | return JWTService.instance; 22 | } 23 | 24 | getDynLogLevelHeaderName(): string { 25 | return this.dynLogLevelHeader; 26 | } 27 | 28 | getDynLogLevel(token: string): string | null { 29 | // Read dynamic log level key from environment var. 30 | const dynLogLevelKey = process.env[ENV_DYN_LOG_KEY]; 31 | const payload = dynLogLevelKey ? this.verifyAndDecodeJWT(token, dynLogLevelKey) : null; 32 | if (payload) { 33 | return payload.level; 34 | } 35 | return null; 36 | }; 37 | 38 | private verifyAndDecodeJWT(token: string, pubKey: string) { 39 | if (!token || !pubKey || typeof pubKey !== "string") { 40 | return null; // no public key or jwt provided 41 | } 42 | 43 | try { 44 | if (pubKey.match(/BEGIN PUBLIC KEY/)) 45 | return jwt.verify(token, pubKey, { algorithms: ["RS256", "RS384", "RS512"] }); 46 | else 47 | return jwt.verify(token, "-----BEGIN PUBLIC KEY-----\n" + pubKey + "\n-----END PUBLIC KEY-----", { algorithms: ["RS256", "RS384", "RS512"] }); 48 | } catch (err) { 49 | return null; // token expired or invalid 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | # Feel free to add content and custom Front Matter to this file. 3 | # To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults 4 | 5 | layout: home 6 | title: About 7 | nav_order: 1 8 | --- 9 | 10 | # CF Node.js Logging Support 11 | 12 | This library provides a bundle of targeted logging services for Node.js applications running on Cloud Foundry which serves two main purposes: 13 | It provides means to emit *structured application log messages* and instrument parts of your application stack to *collect request metrics*. 14 | 15 | The library is optimized for seamless integration with SAP BTP Cloud Foundry environment and its logging services [SAP Application Logging Service](https://help.sap.com/docs/application-logging-service) and [SAP Cloud Logging](https://help.sap.com/docs/cloud-logging). 16 | 17 | To get started follow our [Getting Started](/cf-nodejs-logging-support/getting-started/) chapters to install the library and learn how to integrate it with supported server frameworks. 18 | 19 | Head over to the [General Usage](/cf-nodejs-logging-support/general-usage) articles to find information about the main logging functions. 20 | 21 | Advanced topics, e.g., dynamic logging level threshold, SAP Passport support get covered in the [Advanced Usage](/cf-nodejs-logging-support/advanced-usage) articles. 22 | 23 | As of version 7.0.0 the configuration of the logging library is customizable. Head over to [Advanced Configuration](/cf-nodejs-logging-support/configuration) for further information. 24 | 25 | For details on the concepts and log formats please look at the sibling project for [java logging support](https://github.com/SAP/cf-java-logging-support). 26 | 27 | [Get started](/cf-nodejs-logging-support/getting-started/installation){: .btn .btn-purple .mr-2 } 28 | [Migrate to v7](/cf-nodejs-logging-support/migration){: .btn .mr-2 } 29 | [View it on GitHub](https://github.com/SAP/cf-nodejs-logging-support){: .btn .mr-2 } 30 | [View it on npm](https://www.npmjs.com/package/cf-nodejs-logging-support){: .btn } 31 | -------------------------------------------------------------------------------- /docs/configuration/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Introduction 4 | parent: Advanced Configuration 5 | nav_order: 1 6 | has_children: false 7 | --- 8 | 9 | # Introduction 10 | 11 | You can extend or replace the default configuration by defining and loading JSON config files. 12 | Refer to the following sections to get a better understanding of configuration options: 13 | 14 | * [Configuration of logging fields](/cf-nodejs-logging-support/configuration/fields/) 15 | * [Default request logging level](/cf-nodejs-logging-support/configuration/default-request-level/) 16 | * [Custom fields format](/cf-nodejs-logging-support/configuration/custom-fields-format/) 17 | * [Server Framework](/cf-nodejs-logging-support/configuration/framework/) 18 | 19 | ## Add custom configuration 20 | 21 | Once you have a JSON file with your configuration, you can add it to the logger as follows: 22 | 23 | ```js 24 | const configFile = './config.json'; 25 | 26 | log.addConfig(configFile); 27 | ``` 28 | 29 | Alternatively, you can provide and load multiple configuration files: 30 | 31 | ```js 32 | const configFile1 = require('./config1.json'); 33 | const configFile2 = require('./config2.json'); 34 | const configFile3 = require('./config3.json'); 35 | 36 | log.addConfig(configFile1, configFile2, configFile3); 37 | ``` 38 | 39 | Configuration files can be added iteratively. 40 | This means that in case of collisions latter configuration files will override previous ones. 41 | 42 | ## Get configuration as JSON object 43 | 44 | For local testing purposes you can get the full library configuration as JSON object from any logger instance as follows: 45 | 46 | ```js 47 | log.getConfig(); 48 | ``` 49 | 50 | Alternatively, you can get the configuration for one or multiple desired fields as follows: 51 | 52 | ```js 53 | log.getConfigFields("organization_id","request_id") 54 | ``` 55 | 56 | ## Clear fields configuration 57 | 58 | You can clear the all default and configured fields as follows: 59 | 60 | ```js 61 | log.clearFieldsConfig(); 62 | ``` 63 | 64 | This might come in handy when aiming for a fully customized field configuration. 65 | -------------------------------------------------------------------------------- /src/lib/logger/cache.ts: -------------------------------------------------------------------------------- 1 | import { ConfigField, Output } from '../config/interfaces'; 2 | import SourceUtils from './sourceUtils'; 3 | 4 | export default class Cache { 5 | 6 | private static instance: Cache; 7 | private sourceUtils: SourceUtils; 8 | private cacheMsgRecord: any; 9 | private cacheReqRecord: any; 10 | private shouldUpdateMsg: boolean; 11 | private shouldUpdateReq: boolean; 12 | 13 | private constructor() { 14 | this.sourceUtils = SourceUtils.getInstance(); 15 | this.shouldUpdateMsg = true; 16 | this.shouldUpdateReq = true; 17 | } 18 | 19 | static getInstance(): Cache { 20 | if (!Cache.instance) { 21 | Cache.instance = new Cache(); 22 | } 23 | 24 | return Cache.instance; 25 | } 26 | 27 | getCacheMsgRecord(cacheFields: ConfigField[]): any { 28 | if (this.shouldUpdateMsg) { 29 | this.updateCache(Output.MsgLog, cacheFields); 30 | this.shouldUpdateMsg = false; 31 | } 32 | return this.cacheMsgRecord; 33 | } 34 | 35 | getCacheReqRecord(cacheFields: ConfigField[], req: any, res: any): any { 36 | if (this.shouldUpdateReq) { 37 | this.updateCache(Output.ReqLog, cacheFields, req, res); 38 | this.shouldUpdateReq = false; 39 | } 40 | return this.cacheReqRecord; 41 | } 42 | 43 | markDirty() { 44 | this.shouldUpdateMsg = true; 45 | this.shouldUpdateReq = true; 46 | } 47 | 48 | private updateCache(output: Output, cacheFields: ConfigField[], req?: any, res?: any) { 49 | let cache: any = {}; 50 | 51 | if (output == Output.MsgLog) { 52 | this.cacheMsgRecord = {}; 53 | cache = this.cacheMsgRecord; 54 | } else { 55 | this.cacheReqRecord = {}; 56 | cache = this.cacheReqRecord; 57 | } 58 | 59 | // build cache 60 | cacheFields.forEach( 61 | field => { 62 | cache[field.name] = this.sourceUtils.getValue(field, cache, output, req, res); 63 | } 64 | ); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
cf-nodejs-logging-support | sample app
15 |
16 |
17 |
Use buttons to send test logs:
18 | 20 | 22 | 24 | 26 | 27 | 28 | 30 |
31 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/public/styles/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Segoe UI", "Roboto", "Helvetica Neue", "Arial"; 3 | } 4 | 5 | .main-container { 6 | padding: 20px; 7 | text-align: center; 8 | display: flex; 9 | flex-wrap: wrap; 10 | justify-content: center; 11 | } 12 | 13 | .title { 14 | color: rgb(33, 37, 41); 15 | display: block; 16 | font-size: 2.5rem; 17 | font-weight: 700!important; 18 | margin-top: 0; 19 | text-align: center; 20 | } 21 | 22 | .subtitle { 23 | color: rgb(33, 37, 41); 24 | display: block; 25 | font-size: 2rem; 26 | font-weight: 400; 27 | margin-top: 20px!important; 28 | margin-top: 0; 29 | text-align: center; 30 | } 31 | 32 | .buttons-container { 33 | max-width: 450px; 34 | margin: 10px; 35 | } 36 | 37 | .feedback-container { 38 | max-width: 900px; 39 | margin: 10px; 40 | } 41 | 42 | .btn { 43 | cursor: pointer; 44 | margin-top: 0.5rem; 45 | max-width: 450px; 46 | display: block; 47 | width: 100%; 48 | padding: 0.5rem 1rem; 49 | font-size: 1.25rem; 50 | line-height: 1.5; 51 | border-radius: 0.3rem; 52 | color: #fff; 53 | background-color: #007bff; 54 | border-color: #007bff; 55 | font-weight: 400; 56 | text-align: center; 57 | white-space: nowrap; 58 | user-select: none; 59 | border: 1px solid transparent; 60 | transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out; 61 | -webkit-appearance: button; 62 | text-transform: none; 63 | overflow: visible; 64 | } 65 | 66 | .feedback-box { 67 | text-align: left; 68 | margin-top: 0.5rem; 69 | display: block; 70 | padding: 0.5rem 1rem; 71 | line-height: 1.5; 72 | border-radius: 0.3rem; 73 | background-color: #dadada; 74 | font-weight: 200; 75 | user-select: all; 76 | border: 1px solid #dcdcdc; 77 | text-transform: none; 78 | overflow: auto; 79 | } 80 | 81 | .btn:active { 82 | background-color: #3f9cff; 83 | } 84 | 85 | a { 86 | color: #007bff; 87 | cursor: pointer; 88 | line-height: 2.0; 89 | margin-top: 0.5rem; 90 | text-decoration: none; 91 | background-color: transparent; 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/middleware/middleware.ts: -------------------------------------------------------------------------------- 1 | import JWTService from '../helper/jwtService'; 2 | import LevelUtils from '../helper/levelUtils'; 3 | import { Logger } from '../logger/logger'; 4 | import RecordFactory from '../logger/recordFactory'; 5 | import RecordWriter from '../logger/recordWriter'; 6 | import Context from '../logger/context'; 7 | import RootLogger from '../logger/rootLogger'; 8 | import RequestAccessor from './requestAccessor'; 9 | import Config from '../config/config'; 10 | import ResponseAccessor from './responseAccessor'; 11 | 12 | export default class Middleware { 13 | 14 | static logNetwork(req: any, res: any, next?: any) { 15 | let logSent = false; 16 | 17 | const context = new Context(req); 18 | const parentLogger = RootLogger.getInstance(); 19 | const reqReceivedAt = Date.now(); 20 | 21 | // initialize Logger with parent to set registered fields 22 | const networkLogger = new Logger(parentLogger, context); 23 | const jwtService = JWTService.getInstance(); 24 | 25 | const dynLogLevelHeader = jwtService.getDynLogLevelHeaderName(); 26 | const token = RequestAccessor.getInstance().getHeaderField(req, dynLogLevelHeader); 27 | if (token) { 28 | const dynLevel = jwtService.getDynLogLevel(token); 29 | if (dynLevel != null) { 30 | networkLogger.setLoggingLevel(dynLevel); 31 | } 32 | } 33 | req.logger = networkLogger; 34 | req._receivedAt = reqReceivedAt; 35 | 36 | const finishLog = () => { 37 | if (!logSent) { 38 | res._sentAt = Date.now() 39 | const levelName = Config.getInstance().getReqLoggingLevel() 40 | const level = LevelUtils.getLevel(levelName); 41 | const threshold = LevelUtils.getLevel(req.logger.getLoggingLevel()); 42 | if (LevelUtils.isLevelEnabled(threshold, level)) { 43 | const record = RecordFactory.getInstance().buildReqRecord(levelName, req, res, context); 44 | RecordWriter.getInstance().writeLog(record); 45 | } 46 | logSent = true; 47 | } 48 | } 49 | 50 | ResponseAccessor.getInstance().onFinish(res, finishLog); 51 | 52 | next ? next() : null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2017", 5 | "outDir": "build/main", 6 | "rootDir": "src", 7 | "moduleResolution": "node", 8 | "module": "commonjs", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 13 | "strict": true /* Enable all strict type-checking options. */, 14 | /* Strict Type-Checking Options */ 15 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 16 | // "strictNullChecks": true /* Enable strict null checks. */, 17 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 18 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 19 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 20 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 21 | /* Additional Checks */ 22 | "noUnusedLocals": true /* Report errors on unused locals. */, 23 | "noUnusedParameters": true /* Report errors on unused parameters. */, 24 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 25 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 26 | /* Debugging Options */ 27 | "traceResolution": false /* Report module resolution log messages. */, 28 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 29 | "listFiles": false /* Print names of files part of the compilation. */, 30 | "pretty": true /* Stylize errors and messages using color and context. */, 31 | "lib": [ 32 | "es2017" 33 | ], 34 | "types": [ 35 | "node" 36 | ], 37 | "typeRoots": [ 38 | "node_modules/@types", 39 | "src/types" 40 | ] 41 | }, 42 | "include": [ 43 | "src/**/*.ts" 44 | ], 45 | "exclude": [ 46 | "node_modules/**" 47 | ], 48 | "compileOnSave": false 49 | } 50 | -------------------------------------------------------------------------------- /docs/advanced-usage/03-sensitive-data-reduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Sensitive Data Redaction 4 | parent: Advanced Usage 5 | nav_order: 3 6 | permalink: /advanced-usage/sensitive-data-redaction 7 | --- 8 | 9 | # Sensitive data redaction 10 | 11 | Version 3.0.0 and above implement a sensitive data redaction system which disables logging of sensitive fields. 12 | These fields will contain 'redacted' instead of the original content or are omitted. 13 | 14 | Following fields are *redacted* by default: 15 | 16 | - `remote_ip` 17 | - `remote_host` 18 | - `remote_port` 19 | - `x_forwarded_for` 20 | - `x_forwarded_host` 21 | - `x_forwarded_proto` 22 | - `x_custom_host` 23 | - `remote_user` 24 | - `referer` 25 | 26 | Following fields are *omitted* by default: 27 | 28 | - `x_ssl_client` 29 | - `x_ssl_client_verify` 30 | - `x_ssl_client_subject_dn` 31 | - `x_ssl_client_subject_cn` 32 | - `x_ssl_client_issuer_dn` 33 | - `x_ssl_client_notbefore` 34 | - `x_ssl_client_notafter` 35 | - `x_ssl_client_session_id` 36 | 37 | In order to activate usual logging for all or some of these fields you have to set specific environment variables: 38 | 39 | | Environment Variable | Optional fields | 40 | |-------------------------------------------|------------------------------------------------------------------------------------------------------| 41 | | ```LOG_SENSITIVE_CONNECTION_DATA: true``` | activates the fields `remote_ip`, `remote_host`, `remote_port`, `x_forwarded_*` and `x_custom_host` | 42 | | ```LOG_REMOTE_USER: true``` | activates the field `remote_user` | 43 | | ```LOG_REFERER: true``` | activates the field `referer` | 44 | | ```LOG_SSL_HEADERS: true``` | activates the ssl header fields `x_ssl_*` | 45 | 46 | This behavior matches with the corresponding mechanism in the [CF Java Logging Support](https://github.com/SAP/cf-java-logging-support/wiki/Overview#logging-sensitive-user-data) library. 47 | 48 | If you want to override the default behaviour of sensitive data redaction please go to [Configuration Fields](/cf-nodejs-logging-support/configuration/fields#sensitive-data-redaction) 49 | -------------------------------------------------------------------------------- /src/test/acceptance-test/express/app.js: -------------------------------------------------------------------------------- 1 | // saves public key 2 | process.env.DYN_LOG_LEVEL_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fzU8StO511QYoC+BZp4riR2eVQM8FPPB2mF4I78WBDzloAVTaz0Z7hkMog1rAy8+Xva+fLiMuxDmN7kQZKBc24O4VeKNjOt8ZtNhz3vlMTZrNQ7bi+j8TS8ycUgKqe4/hSmjJBfXoduZ8Ye90u8RRfPLzbuutctLfCnL/ZhEehqfilt1iQb/CRCEsJou5XahmvOO5Gt+9kTBmY+2rS/+HKKdAhI3OpxwvXXNi8m9LrdHosMD7fTUpLUgdcIp8k3ACp9wCIIxbv1ssDeWKy7bKePihTl7vJq6RkopS6GvhO6yiD1IAJF/iDOrwrJAWzanrtavUc1RJZvbOvD0DFFOwIDAQAB"; 3 | 4 | const importFresh = require('import-fresh'); 5 | const express = importFresh("express"); 6 | const log = importFresh('../../../../build/main/index'); 7 | const app = express(); 8 | 9 | // add logger to the server network queue to log all incoming requests. 10 | app.use(log.logNetwork); 11 | 12 | app.get("/simplelog", function (req, res) { 13 | req.logger.logMessage("info", "test-message"); 14 | res.send(); 15 | }); 16 | 17 | app.get("/requestcontext", function (req, res) { 18 | req.headers["referer"] = "test-referer"; 19 | req.user = { 20 | id: "test-user" 21 | }; 22 | var reqLogger = req.logger; 23 | reqLogger.logMessage("debug", "debug-message"); 24 | reqLogger.logMessage("info", "test-message"); 25 | res.send(); 26 | }); 27 | 28 | app.get("/setloglevel", function (req, res) { 29 | req.logger.setLoggingLevel("warn"); 30 | req.logger.logMessage("info", "test-message"); 31 | res.send(); 32 | }); 33 | 34 | app.get("/setcorrelationandtenantid", function (req, res) { 35 | req.logger.setCorrelationId("cbc2654f-1c35-45d0-96fc-f32efac20986"); 36 | req.logger.setTenantId("abc2654f-5t15-12h0-78gt-n73jeuc01847"); 37 | req.logger.setTenantSubdomain("test-subdomain"); 38 | req.logger.logMessage("info", "test-message"); 39 | res.send(); 40 | }); 41 | 42 | app.get("/getcorrelationandtenantid", function (req, res) { 43 | var correlationId = "cbc2654f-1c35-45d0-96fc-f32efac20986"; 44 | var tenantId = "abc2654f-5t15-12h0-78gt-n73jeuc01847"; 45 | var tenantSubdomain = "test-subdomain"; 46 | 47 | req.logger.setCorrelationId(correlationId); 48 | req.logger.setTenantId(tenantId); 49 | req.logger.setTenantSubdomain(tenantSubdomain); 50 | 51 | if (req.logger.getCorrelationId() == correlationId && req.logger.getTenantId() == tenantId && req.logger.getTenantSubdomain() == tenantSubdomain) { 52 | req.logger.logMessage("info", "successful"); 53 | } 54 | res.send(); 55 | }); 56 | 57 | module.exports = app; 58 | -------------------------------------------------------------------------------- /src/lib/config/default/config-core.json: -------------------------------------------------------------------------------- 1 | { 2 | "outputStartupMsg": true, 3 | "framework": "express", 4 | "fields": [ 5 | { 6 | "name": "logger", 7 | "source": { 8 | "type": "static", 9 | "value": "nodejs-logger" 10 | }, 11 | "output": [ 12 | "msg-log", 13 | "req-log" 14 | ] 15 | }, 16 | { 17 | "name": "type", 18 | "source": [ 19 | { 20 | "type": "static", 21 | "value": "request", 22 | "output": "req-log" 23 | }, 24 | { 25 | "type": "static", 26 | "value": "log", 27 | "output": "msg-log" 28 | } 29 | ], 30 | "output": [ 31 | "req-log", 32 | "msg-log" 33 | ] 34 | }, 35 | { 36 | "name": "msg", 37 | "source": { 38 | "type": "detail", 39 | "detailName": "message" 40 | }, 41 | "output": [ 42 | "msg-log" 43 | ] 44 | }, 45 | { 46 | "name": "level", 47 | "source": { 48 | "type": "detail", 49 | "detailName": "level" 50 | }, 51 | "output": [ 52 | "msg-log", 53 | "req-log" 54 | ] 55 | }, 56 | { 57 | "name": "stacktrace", 58 | "source": { 59 | "type": "detail", 60 | "detailName": "stacktrace" 61 | }, 62 | "output": [ 63 | "msg-log" 64 | ] 65 | }, 66 | { 67 | "name": "written_at", 68 | "source": { 69 | "type": "detail", 70 | "detailName": "writtenAt" 71 | }, 72 | "output": [ 73 | "msg-log", 74 | "req-log" 75 | ] 76 | }, 77 | { 78 | "name": "written_ts", 79 | "source": { 80 | "type": "detail", 81 | "detailName": "writtenTs" 82 | }, 83 | "output": [ 84 | "msg-log", 85 | "req-log" 86 | ] 87 | } 88 | ] 89 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cf-nodejs-logging-support", 3 | "version": "7.4.1", 4 | "description": "Logging tool for Cloud Foundry", 5 | "keywords": [ 6 | "logging", 7 | "cloud-foundry" 8 | ], 9 | "main": "build/main/index.js", 10 | "types": "build/main/index.d.ts", 11 | "typings": "build/main/index.d.ts", 12 | "module": "build/main/index.js", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/SAP/cf-nodejs-logging-support" 16 | }, 17 | "license": "Apache-2.0", 18 | "author": { 19 | "name": "Christian Dinse, Nicklas Dohrn, Federico Romagnoli" 20 | }, 21 | "homepage": "https://sap.github.io/cf-nodejs-logging-support/", 22 | "scripts": { 23 | "build": "npm run build-json-schema && tsc -p tsconfig.json", 24 | "test": "npm run build-json-schema && tsc -p tsconfig.json && node build/main/index.js && mocha 'src/test/**/*.test.js'", 25 | "test-lint": "eslint src --ext .ts", 26 | "test-performance": "mocha 'src/performance-test/*.test.js'", 27 | "build-json-schema": "typescript-json-schema 'src/lib/config/interfaces.ts' ConfigObject --noExtraProps --required --out 'src/lib/config/default/config-schema.json'", 28 | "coverage": "nyc --reporter=lcov --reporter=text-summary npm run test" 29 | }, 30 | "engines": { 31 | "node": ">=14.14" 32 | }, 33 | "devDependencies": { 34 | "@types/json-stringify-safe": "^5.0.0", 35 | "@types/node": "^17.0.45", 36 | "@types/triple-beam": "^1.3.2", 37 | "@types/uuid": "^9.0.1", 38 | "@typescript-eslint/eslint-plugin": "^5.42.1", 39 | "@typescript-eslint/parser": "^5.42.1", 40 | "chai": "^4.3.6", 41 | "connect": "^3.7.0", 42 | "eslint": "^8.27.0", 43 | "eslint-plugin-eslint-comments": "^3.2.0", 44 | "eslint-plugin-import": "^2.26.0", 45 | "express": "^4.18.2", 46 | "fastify": "^4.26.1", 47 | "import-fresh": "^3.3.0", 48 | "mocha": "^11.7.5", 49 | "node-mocks-http": "^1.11.0", 50 | "nyc": "^15.1.0", 51 | "restify": "^11.1.0", 52 | "rewire": "^6.0.0", 53 | "supertest": "^6.2.2", 54 | "typescript": "^5.1.3", 55 | "typescript-json-schema": "^0.55.0" 56 | }, 57 | "files": [ 58 | "/.reuse/", 59 | "/build/", 60 | "LICENSE", 61 | "README.md" 62 | ], 63 | "dependencies": { 64 | "ajv": "^8.11.0", 65 | "json-stringify-safe": "^5.0.1", 66 | "jsonwebtoken": "^9.0.0", 67 | "triple-beam": "^1.3.0", 68 | "uuid": "^9.0.0", 69 | "winston-transport": "^4.5.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/lib/config/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigObject { 2 | fields?: ConfigField[]; 3 | customFieldsFormat?: CustomFieldsFormat; 4 | customFieldsTypeConversion?: CustomFieldsTypeConversion; 5 | outputStartupMsg?: boolean; 6 | reqLoggingLevel?: string; 7 | framework?: Framework; 8 | } 9 | 10 | export interface ConfigField { 11 | name: string; 12 | envVarRedact?: string; 13 | envVarSwitch?: string; 14 | source?: Source | Source[]; 15 | output: Output[]; 16 | convert?: Conversion; 17 | disable?: boolean; 18 | default?: string | number | boolean; 19 | isContext?: boolean; 20 | settable?: boolean; 21 | _meta?: ConfigFieldMeta; 22 | } 23 | 24 | export interface Source { 25 | type: SourceType; 26 | value?: string; 27 | path?: string[]; 28 | fieldName?: string; 29 | varName?: string; 30 | detailName?: DetailName; 31 | regExp?: string; 32 | framework?: Framework; 33 | output?: Output; 34 | } 35 | 36 | export interface ConfigFieldMeta { 37 | isRedacted: boolean; 38 | isEnabled: boolean; 39 | isCache: boolean; 40 | isContext: boolean; 41 | } 42 | 43 | export enum Framework { 44 | Express = "express", 45 | Restify = "restify", 46 | Connect = "connect", 47 | Fastify = "fastify", 48 | NodeJsHttp = "plainhttp" 49 | } 50 | 51 | export enum Output { 52 | MsgLog = "msg-log", 53 | ReqLog = "req-log" 54 | } 55 | 56 | export enum CustomFieldsFormat { 57 | ApplicationLogging = "application-logging", 58 | CloudLogging = "cloud-logging", 59 | All = "all", 60 | Disabled = "disabled", 61 | Default = "default" 62 | } 63 | 64 | export enum CustomFieldsTypeConversion { 65 | Retain = "retain", 66 | Stringify = "stringify" 67 | } 68 | 69 | export enum SourceType { 70 | Static = "static", 71 | Env = "env", 72 | ConfigField = "config-field", 73 | ReqHeader = "req-header", 74 | ResHeader = "res-header", 75 | ReqObject = "req-object", 76 | ResObject = "res-object", 77 | Detail = "detail", 78 | UUID = "uuid" 79 | } 80 | 81 | export enum DetailName { 82 | RequestReceivedAt = "requestReceivedAt", 83 | ResponseSentAt = "responseSentAt", 84 | ResponseTimeMs = "responseTimeMs", 85 | WrittenAt = "writtenAt", 86 | WrittenTs = "writtenTs", 87 | Message = "message", 88 | Stacktrace = "stacktrace", 89 | Level = "level" 90 | } 91 | 92 | 93 | export enum Conversion { 94 | ToString = "toString", 95 | ParseInt = "parseInt", 96 | ParseFloat = "parseFloat", 97 | ParseBoolean = "parseBoolean" 98 | } 99 | -------------------------------------------------------------------------------- /src/test/acceptance-test/config.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const importFresh = require('import-fresh'); 3 | const customConfig = require('../config-test.json'); 4 | 5 | describe('Test configuration', function () { 6 | 7 | var result; 8 | 9 | beforeEach(function () { 10 | log = importFresh("../../../build/main/index"); 11 | }); 12 | 13 | describe('Add custom configuration', function () { 14 | beforeEach(function () { 15 | log.addConfig(customConfig); 16 | result = log.getConfigFields(); 17 | }); 18 | 19 | it('gets configuration', function () { 20 | expect(result.length).to.be.gt(0); 21 | }); 22 | 23 | it('overrides existing field', function () { 24 | expect(result[0].source).to.have.property("value", "TEST"); 25 | }); 26 | 27 | it('adds new field', function () { 28 | const index = (result.length - 1); 29 | expect(result[index]).to.have.property("name", "new_field"); 30 | }); 31 | }); 32 | 33 | describe('Set custom fields format', function () { 34 | describe('using config file', function () { 35 | beforeEach(function () { 36 | log.addConfig(customConfig); 37 | result = log.getConfig(); 38 | }); 39 | 40 | it('sets format to cloud-logging', function () { 41 | expect(result.customFieldsFormat).to.be.eql("cloud-logging"); 42 | }); 43 | }); 44 | describe('using api method', function () { 45 | beforeEach(function () { 46 | log.setCustomFieldsFormat("cloud-logging"); 47 | result = log.getConfig(); 48 | }); 49 | 50 | it('sets format to cloud-logging', function () { 51 | expect(result.customFieldsFormat).to.be.eql("cloud-logging"); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('Set startup message', function () { 57 | describe('using config file', function () { 58 | beforeEach(function () { 59 | log.addConfig(customConfig); 60 | result = log.getConfig(); 61 | }); 62 | 63 | it('sets output startup msg to false', function () { 64 | expect(result.outputStartupMsg).to.be.eql(false); 65 | }); 66 | }); 67 | describe('using convenience method', function () { 68 | beforeEach(function () { 69 | log.setStartupMessageEnabled(false); 70 | result = log.getConfig(); 71 | }); 72 | 73 | it('sets output startup msg to false', function () { 74 | expect(result.outputStartupMsg).to.be.eql(false); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/lib/helper/stacktraceUtils.ts: -------------------------------------------------------------------------------- 1 | export default class StacktraceUtils { 2 | 3 | private static instance: StacktraceUtils 4 | private readonly MAX_STACKTRACE_SIZE = 55 * 1024; 5 | 6 | static getInstance(): StacktraceUtils { 7 | if (!StacktraceUtils.instance) { 8 | StacktraceUtils.instance = new StacktraceUtils(); 9 | } 10 | 11 | return StacktraceUtils.instance; 12 | } 13 | 14 | // check if the given object is an Error with stacktrace using duck typing 15 | isErrorWithStacktrace(obj: any): boolean { 16 | if (obj && obj.stack && obj.message && typeof obj.stack === "string" && typeof obj.message === "string") { 17 | return true; 18 | } 19 | return false; 20 | } 21 | 22 | // Split stacktrace into string array and truncate lines if required by size limitation 23 | // Truncation strategy: Take one line from the top and two lines from the bottom of the stacktrace until limit is reached. 24 | prepareStacktrace(stacktraceStr: string): string[] { 25 | let fullStacktrace = stacktraceStr.split('\n'); 26 | let totalLineLength = fullStacktrace.reduce((acc: any, line: any) => acc + line.length, 0); 27 | 28 | if (totalLineLength > this.MAX_STACKTRACE_SIZE) { 29 | let truncatedStacktrace = []; 30 | let stackA = []; 31 | let stackB = []; 32 | let indexA = 0; 33 | let indexB = fullStacktrace.length - 1; 34 | let currentLength = 73; // set to approx. character count for "truncated" and "omitted" labels 35 | 36 | for (let i = 0; i < fullStacktrace.length; i++) { 37 | if (i % 3 == 0) { 38 | let line = fullStacktrace[indexA++]; 39 | if (currentLength + line.length > this.MAX_STACKTRACE_SIZE) { 40 | break; 41 | } 42 | currentLength += line.length; 43 | stackA.push(line); 44 | } else { 45 | let line = fullStacktrace[indexB--]; 46 | if (currentLength + line.length > this.MAX_STACKTRACE_SIZE) { 47 | break; 48 | } 49 | currentLength += line.length; 50 | stackB.push(line); 51 | } 52 | } 53 | 54 | truncatedStacktrace.push("-------- STACK TRACE TRUNCATED --------"); 55 | truncatedStacktrace = [...truncatedStacktrace, ...stackA]; 56 | truncatedStacktrace.push(`-------- OMITTED ${fullStacktrace.length - (stackA.length + stackB.length)} LINES --------`); 57 | truncatedStacktrace = [...truncatedStacktrace, ...stackB.reverse()]; 58 | return truncatedStacktrace; 59 | } 60 | return fullStacktrace; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | # 11 | # If you need help with YAML syntax, here are some quick references for you: 12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml 13 | # https://learnxinyminutes.com/docs/yaml/ 14 | # 15 | # Site settings 16 | # These are used to personalize your new site. If you look in the HTML files, 17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 18 | # You can create any custom variable you would like, and they will be accessible 19 | # in the templates via {{ site.myvariable }}. 20 | 21 | title: "CF Node.js Logging Support" 22 | description: >- # this means to ignore newlines until "baseurl:" 23 | Node.js Logging Support for Cloud Foundry provides the creation of structured log messages and the collection of request metrics. 24 | baseurl: "/cf-nodejs-logging-support" # the subpath of your site, e.g. /blog 25 | url: "https://sap.github.io" # the base hostname & protocol for your site, e.g. http://example.com 26 | 27 | # Build settings 28 | markdown: kramdown 29 | remote_theme: just-the-docs/just-the-docs 30 | plugins: 31 | - jekyll-feed 32 | 33 | # Exclude from processing. 34 | # The following items will not be processed, by default. 35 | # Any item listed under the `exclude:` key here will be automatically added to 36 | # the internal "default list". 37 | # 38 | # Excluded items can be processed by explicitly listing the directories or 39 | # their entries' file path in the `include:` list. 40 | # 41 | # exclude: 42 | # - .sass-cache/ 43 | # - .jekyll-cache/ 44 | # - gemfiles/ 45 | # - Gemfile 46 | # - Gemfile.lock 47 | # - node_modules/ 48 | # - vendor/bundle/ 49 | # - vendor/cache/ 50 | # - vendor/gems/ 51 | # - vendor/ruby/ 52 | 53 | # Search config 54 | search_enabled: true 55 | search: 56 | tokenizer_separator: /[\s\-\.\(\)]+/ 57 | 58 | # Back to top link 59 | back_to_top: true 60 | back_to_top_text: "Back to top" 61 | 62 | # Footer copyright 63 | footer_content: "Copyright © 2024 SAP SE | SAP Privacy Statement" 64 | 65 | # Footer last edited timestamp 66 | last_edit_timestamp: true # show or hide edit time - page must have `last_modified_date` defined in the frontmatter 67 | last_edit_time_format: "%b %e %Y at %I:%M %p" # uses ruby's time format: https://ruby-doc.org/stdlib-2.7.0/libdoc/time/rdoc/Time.html 68 | -------------------------------------------------------------------------------- /docs/general-usage/03-logging-contexts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Logging Contexts 4 | parent: General Usage 5 | nav_order: 3 6 | permalink: /general-usage/logging-contexts 7 | --- 8 | 9 | # Logging Contexts 10 | {: .no_toc } 11 | 12 |
13 | 14 | Table of contents 15 | 16 | {: .text-delta } 17 | 1. TOC 18 | {:toc} 19 |
20 | 21 | ## Concept 22 | Logging contexts are an essential concept of this library to extend the metadata attached to each log message. 23 | It offers correlation of log messages by enriching them with shared properties, such as the `correlation_id` of a request, class names or even details on the method being executed. 24 | While all log messages contain various information on the runtime environment or various request/response details for request logs, logging contexts are more flexible and allow extension at runtime. 25 | 26 | You can create [Child Loggers](/cf-nodejs-logging-support/advanced-usage/child-loggers) to inherit or create new logging contexts. 27 | 28 | There are three *kinds* of contexts: 29 | 30 | * global context 31 | * request context 32 | * custom context 33 | 34 | ## Global Context 35 | 36 | As messages written in *global* context are considered uncorrelated, it is always empty and immutable. 37 | Use the imported `log` object to log messages in *global* context: 38 | 39 | ```js 40 | var log = require("cf-nodejs-logging-support"); 41 | ... 42 | log.info("Message logged in global context"); 43 | ``` 44 | 45 | Adding context properties is not possible, which is indicated by the return value of the setter methods: 46 | 47 | ```js 48 | var log = require("cf-nodejs-logging-support"); 49 | ... 50 | var result = log.setContextProperty("my-property", "my-value") 51 | // result: false 52 | ``` 53 | 54 | ## Request context 55 | 56 | The library adds context bound functions to request objects in case it has been [attached as middleware](/cf-nodejs-logging-support/general-usage/request-logs#attaching-to-server-frameworks). 57 | Use the provided `req.logger` object to log messages in *request* contexts: 58 | 59 | ```js 60 | app.get('/', function (req, res) { 61 | var reqLogger = req.logger; // reqLogger logs in request context 62 | ... 63 | reqLogger.info("Message logged in request context"); 64 | }); 65 | ``` 66 | 67 | By default, *request* contexts provide following additional request related fields to logs: 68 | 69 | * `correlation_id` 70 | * `request_id` 71 | * `tenant_id` 72 | * `tenant_subdomain` 73 | 74 | ## Custom context 75 | 76 | Child loggers can also be equipped with a newly created context, including an auto-generated `correlation_id`. 77 | This can be useful when log messages should be correlated without being in context of an HTTP request. 78 | Switch over to the [Child loggers with custom context](/cf-nodejs-logging-support/advanced-usage/child-loggers#child-loggers-with-custom-context) section to learn more about this topic. 79 | -------------------------------------------------------------------------------- /src/test/unit-test/winston-transport.test.js: -------------------------------------------------------------------------------- 1 | const { SPLAT } = require('triple-beam'); 2 | var rewire = require('rewire'); 3 | var chai = require("chai"); 4 | chai.should(); 5 | 6 | 7 | describe('Test winston-transport.js', function () { 8 | describe('Test parameter forwarding', function () { 9 | 10 | var transport; 11 | var logger = rewire("../../../build/main/index.js"); 12 | var catchedArgs; 13 | 14 | before(function () { 15 | logger.__set__("rootLogger.logMessage", function () { 16 | catchedArgs = Array.prototype.slice.call(arguments); 17 | }); 18 | 19 | transport = logger.createWinstonTransport(); 20 | }); 21 | 22 | after(function () { 23 | 24 | }); 25 | 26 | it('Test log method (simple message)', function () { 27 | 28 | var info = {}; 29 | info.level = "error"; 30 | info.message = "test" 31 | 32 | var called = false; 33 | var callback = () => { called = true } 34 | 35 | transport.log(info, callback); 36 | 37 | catchedArgs.length.should.equal(2); 38 | catchedArgs[0].should.equal("error"); 39 | catchedArgs[1].should.equal("test"); 40 | called.should.equal(true) 41 | }); 42 | 43 | it('Test log method (message with additional variables)', function () { 44 | 45 | var info = {}; 46 | info.level = "error"; 47 | info.message = "test %d %s" 48 | info[SPLAT] = [42, "abc"] 49 | 50 | var called = false; 51 | var callback = () => { called = true } 52 | 53 | transport.log(info, callback); 54 | 55 | catchedArgs.length.should.equal(4); 56 | catchedArgs[0].should.equal("error"); 57 | catchedArgs[1].should.equal("test %d %s"); 58 | catchedArgs[2].should.equal(42); 59 | catchedArgs[3].should.equal("abc"); 60 | called.should.equal(true) 61 | }); 62 | }); 63 | 64 | describe('Test option initialization', function () { 65 | 66 | var logger = require("../../../build/main/index.js"); 67 | 68 | it('Test default initialization', function () { 69 | var transport = logger.createWinstonTransport(); 70 | transport.level.should.equal("info"); 71 | transport.logger.should.equal(logger); 72 | }); 73 | 74 | it('Test custom initialization', function () { 75 | var transport = logger.createWinstonTransport({ level: "error" }); 76 | transport.level.should.equal("error"); 77 | transport.logger.should.equal(logger); 78 | }); 79 | 80 | it('Test incomplete initialization', function () { 81 | var transport = logger.createWinstonTransport({}); 82 | transport.level.should.equal("info"); 83 | transport.logger.should.equal(logger); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to cf-nodejs-logging-support 2 | 3 | ## General Remarks 4 | 5 | You are welcome to contribute content (code, documentation etc.) to this open source project. 6 | 7 | There are some important things to know: 8 | 9 | 1. You must **comply to the license of this project**, **accept the Developer Certificate of Origin** (see below) before being able to contribute. The acknowledgement to the DCO will usually be requested from you as part of your first pull request to this project. 10 | 2. Please **adhere to our [Code of Conduct](CODE_OF_CONDUCT.md)**. 11 | 3. If you plan to use **generative AI for your contribution**, please see our guideline below. 12 | 4. **Not all proposed contributions can be accepted**. Some features may fit another project better or doesn't fit the general direction of this project. Of course, this doesn't apply to most bug fixes, but a major feature implementation for instance needs to be discussed with one of the maintainers first. Possibly, one who touched the related code or module recently. The more effort you invest, the better you should clarify in advance whether the contribution will match the project's direction. The best way would be to just open an issue to discuss the feature you plan to implement (make it clear that you intend to contribute). We will then forward the proposal to the respective code owner. This avoids disappointment. 13 | 14 | ## Developer Certificate of Origin (DCO) 15 | 16 | Contributors will be asked to accept a DCO before they submit the first pull request to this projects, this happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). 17 | 18 | ## Contributing with AI-generated code 19 | 20 | As artificial intelligence evolves, AI-generated code is becoming valuable for many software projects, including open-source initiatives. While we recognize the potential benefits of incorporating AI-generated content into our open-source projects there a certain requirements that need to be reflected and adhered to when making contributions. 21 | 22 | Please see our [guideline for AI-generated code contributions to SAP Open Source Software Projects](CONTRIBUTING_USING_GENAI.md) for these requirements. 23 | 24 | ## How to Contribute 25 | 26 | 1. Make sure the change is welcome (see [General Remarks](#general-remarks)). 27 | 2. Create a branch by forking the repository and apply your change. 28 | 3. Commit and push your change on that branch. 29 | 4. Create a pull request in the repository using this branch. 30 | 5. Follow the link posted by the CLA assistant to your pull request and accept it, as described above. 31 | 6. Wait for our code review and approval, possibly enhancing your change on request. 32 | - Note that the maintainers have many duties. So, depending on the required effort for reviewing, testing, and clarification, this may take a while. 33 | 7. Once the change has been approved and merged, we will inform you in a comment. 34 | 8. Celebrate! -------------------------------------------------------------------------------- /src/lib/config/default/config-sap-passport.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { 4 | "name": "sap_passport", 5 | "source": { 6 | "type": "req-header", 7 | "fieldName": "sap-passport" 8 | }, 9 | "output": [ 10 | "req-log" 11 | ] 12 | }, 13 | { 14 | "name": "sap_passport_Action", 15 | "settable": true, 16 | "output": [ 17 | "msg-log", 18 | "req-log" 19 | ] 20 | }, 21 | { 22 | "name": "sap_passport_ActionType", 23 | "settable": true, 24 | "output": [ 25 | "msg-log", 26 | "req-log" 27 | ] 28 | }, 29 | { 30 | "name": "sap_passport_ClientNumber", 31 | "settable": true, 32 | "output": [ 33 | "msg-log", 34 | "req-log" 35 | ] 36 | }, 37 | { 38 | "name": "sap_passport_ConnectionCounter", 39 | "settable": true, 40 | "output": [ 41 | "msg-log", 42 | "req-log" 43 | ] 44 | }, 45 | { 46 | "name": "sap_passport_ConnectionId", 47 | "settable": true, 48 | "output": [ 49 | "msg-log", 50 | "req-log" 51 | ] 52 | }, 53 | { 54 | "name": "sap_passport_ComponentName", 55 | "settable": true, 56 | "output": [ 57 | "msg-log", 58 | "req-log" 59 | ] 60 | }, 61 | { 62 | "name": "sap_passport_ComponentType", 63 | "settable": true, 64 | "output": [ 65 | "msg-log", 66 | "req-log" 67 | ] 68 | }, 69 | { 70 | "name": "sap_passport_PreviousComponentName", 71 | "settable": true, 72 | "output": [ 73 | "msg-log", 74 | "req-log" 75 | ] 76 | }, 77 | { 78 | "name": "sap_passport_TraceFlags", 79 | "settable": true, 80 | "output": [ 81 | "msg-log", 82 | "req-log" 83 | ] 84 | }, 85 | { 86 | "name": "sap_passport_TransactionId", 87 | "settable": true, 88 | "output": [ 89 | "msg-log", 90 | "req-log" 91 | ] 92 | }, 93 | { 94 | "name": "sap_passport_RootContextId", 95 | "settable": true, 96 | "output": [ 97 | "msg-log", 98 | "req-log" 99 | ] 100 | }, 101 | { 102 | "name": "sap_passport_UserId", 103 | "settable": true, 104 | "output": [ 105 | "msg-log", 106 | "req-log" 107 | ] 108 | } 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /docs/general-usage/02-message-logs.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Message Logs 4 | parent: General Usage 5 | nav_order: 2 6 | permalink: /general-usage/message-logs 7 | --- 8 | 9 | # Message Logs 10 | {: .no_toc } 11 | 12 | In addition to request logging this library also supports logging of application messages. 13 | Message logs contain at least some message and also CF metadata. 14 | 15 |
16 | 17 | Table of contents 18 | 19 | {: .text-delta } 20 | 1. TOC 21 | {:toc} 22 |
23 | 24 | ## Logging levels 25 | 26 | Following common logging levels are supported: 27 | 28 | - `error` 29 | - `warn` 30 | - `info` 31 | - `verbose` 32 | - `debug` 33 | - `silly` 34 | 35 | Set the minimum logging level for logs as follows: 36 | 37 | ```js 38 | log.setLoggingLevel("info"); 39 | ``` 40 | 41 | In addition there is an off logging level available to disable log output completely. 42 | 43 | ## Writing message logs 44 | 45 | There are so called *convenience methods* available for all supported logging levels. 46 | These can be called to log a message using the corresponding level. 47 | It is also possible to use standard format placeholders equivalent to the [util.format](https://nodejs.org/api/util.html#util_util_format_format_args) method. 48 | 49 | You can find several usage examples below demonstrating options to be specified when calling a log method. 50 | All methods get called on a `logger` object, which provides a so called *logging context*. 51 | You can find more information about logging contexts in the [Logging Contexts](/cf-nodejs-logging-support/general-usage/logging-contexts) chapter. 52 | In the simplest case, `logger` is an instance of imported `log` module. 53 | 54 | - Simple message: 55 | 56 | ```js 57 | logger.info("Hello World"); 58 | // ... "msg":"Hello World" ... 59 | ``` 60 | 61 | - Message with additional numeric value: 62 | 63 | ```js 64 | logger.info("Listening on port %d", 5000); 65 | // ... "msg":"Listening on port 5000" ... 66 | ``` 67 | 68 | - Message with additional string values: 69 | 70 | ```js 71 | logger.info("This %s a %s", "is", "test"); 72 | // ... "msg":"This is a test" ... 73 | ``` 74 | 75 | - Message with additional json object to be embedded in to the message: 76 | 77 | ```js 78 | logger.info("Test data %j", {"field" :"value"}, {}); 79 | // ... "msg":"Test data {\"field\": \"value\"}" ... 80 | ``` 81 | 82 | - In case you want to set the actual logging level from a variable, you can use following method, which also supports format features described above: 83 | 84 | ```js 85 | var level = "debug"; 86 | logger.logMessage(level, "Hello World"); 87 | // ... "msg":"Hello World" ... 88 | ``` 89 | 90 | ## Checking log severity levels 91 | 92 | It can be useful to check if messages with a specific severity level would be logged. 93 | You can check if a logging level is active as follows: 94 | 95 | ```js 96 | var isInfoActive = log.isLoggingLevel("info"); 97 | if (isInfoActive) { 98 | log.info("message logged with severity 'info'"); 99 | } 100 | ``` 101 | 102 | There are convenience methods available for this feature: 103 | 104 | ```js 105 | var isDebugActive = log.isDebug(); 106 | ``` 107 | 108 | ## Get the logging level threshold 109 | 110 | For local testing purposes you can also get the current level of a logger instance by calling the getLoggingLevel method: 111 | 112 | ```js 113 | log.getLoggingLevel(); 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/advanced-usage/01-child-loggers.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Child Loggers 4 | parent: Advanced Usage 5 | nav_order: 1 6 | permalink: /advanced-usage/child-loggers 7 | --- 8 | 9 | # Child loggers 10 | 11 | You can create child loggers sharing the [Logging Context](/cf-nodejs-logging-support/general-usage/logging-contexts) of their parent logger: 12 | 13 | ```js 14 | app.get('/', function (req, res) { 15 | var logger = req.logger.createLogger(); // equivalent to req.createLogger(); 16 | ... 17 | logger.logMessage("This message is request correlated, but logged with a child logger"); 18 | }); 19 | ``` 20 | 21 | Each child logger can have its own set of custom fields, which will be added to each messages: 22 | 23 | ```js 24 | var logger = req.logger.createLogger(); 25 | logger.setCustomFields({"field-a" :"value"}) 26 | // OR 27 | var logger = req.logger.createLogger({"field-a" :"value"}); 28 | ``` 29 | 30 | Child loggers can have a different logging threshold, so you can customize your log output with them: 31 | 32 | ```js 33 | var logger = req.logger.createLogger(); 34 | logger.setLoggingLevel("info"); 35 | ``` 36 | 37 | ## Child loggers with global context 38 | 39 | You can also create child loggers inheriting from the *global* logging context like this: 40 | 41 | ```js 42 | var log = require("cf-nodejs-logging-support"); 43 | ... 44 | var logger = log.createLogger(); 45 | logger.setCustomFields({"field-a" :"value"}) 46 | // OR 47 | var logger = log.createLogger({"field-a" :"value"}); 48 | ``` 49 | 50 | Keep in mind that the *global* context is empty and immutable. 51 | As mentioned in the [logging context](/cf-nodejs-logging-support/general-usage/logging-contexts) docs, adding properties to it is not possible. 52 | 53 | ## Child loggers with custom context 54 | 55 | New child loggers can be equipped with a new extensible and inheritable context by providing `true` for the `createNewContext` parameter: 56 | 57 | ```js 58 | var log = require("cf-nodejs-logging-support"); 59 | ... 60 | var logger = log.createLogger(null, true); 61 | logger.setTenantId("my-tenant-id"); 62 | ``` 63 | 64 | Similar to a *request* context, a newly created *custom* context includes a generated `correlation_id` (uuid v4). 65 | 66 | A practical use case for this feature would be correlating logs on events that reach your application through channels other than HTTP(S). 67 | By creating a child logger and assigning it (or using the auto-generated) `correlation_id`, any kind of event handling could be made to log correlated messages. 68 | 69 | ## Custom field inheritance 70 | 71 | When using child loggers the custom fields of the parent logger will be inherited. 72 | In case you have set a field for a child logger, which is already set for the parent logger, the value set to the child logger will be used. 73 | 74 | Example for custom field inheritance: 75 | 76 | ```js 77 | // Register all custom fields that can occur. 78 | log.registerCustomFields(["field-a", "field-b", "field-c"]); 79 | 80 | // Set some fields which should be added to all messages 81 | log.setCustomFields({"field-a": "1"}); 82 | log.info("test"); 83 | // ... "custom_fields": {"field-a": "1"} ... 84 | 85 | 86 | // Define a child logger which adds another field 87 | var loggerA = log.createLogger({"field-b": "2"}); 88 | loggerA.info("test"); 89 | // ... "custom_fields": {"field-a": "1", "field-b": "2"} ... 90 | 91 | // Define a child logger which overwrites one field 92 | var loggerB = log.createLogger({"field-a": "3"}); 93 | loggerB.info("test"); 94 | // ... "custom_fields": {"field-a": "3", "field-b": "2"} ... 95 | 96 | // Request correlated logger instances inherit global custom fields and can overwrite them as well. 97 | app.get('/', function (req, res) { 98 | req.logger.setCustomFields({"field-b": "4", "field-c": "5"}); 99 | req.logger.info("test"); 100 | // ... "custom_fields": {"field-a": "1", "field-b": "4", "field-c": "5"} ... 101 | }); 102 | 103 | ``` 104 | -------------------------------------------------------------------------------- /tools/token-creator/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwt-creator", 3 | "version": "1.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "buffer-equal-constant-time": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 10 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 11 | }, 12 | "commander": { 13 | "version": "2.15.0", 14 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.0.tgz", 15 | "integrity": "sha512-7B1ilBwtYSbetCgTY1NJFg+gVpestg0fdA1MhC1Vs4ssyfSXnCAjFr+QcQM9/RedXC0EaUx1sG8Smgw2VfgKEg==" 16 | }, 17 | "ecdsa-sig-formatter": { 18 | "version": "1.0.11", 19 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 20 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 21 | "requires": { 22 | "safe-buffer": "^5.0.1" 23 | } 24 | }, 25 | "jsonwebtoken": { 26 | "version": "9.0.0", 27 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", 28 | "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", 29 | "requires": { 30 | "jws": "^3.2.2", 31 | "lodash": "^4.17.21", 32 | "ms": "^2.1.1", 33 | "semver": "^7.3.8" 34 | } 35 | }, 36 | "jwa": { 37 | "version": "1.4.1", 38 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 39 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 40 | "requires": { 41 | "buffer-equal-constant-time": "1.0.1", 42 | "ecdsa-sig-formatter": "1.0.11", 43 | "safe-buffer": "^5.0.1" 44 | } 45 | }, 46 | "jws": { 47 | "version": "3.2.2", 48 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 49 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 50 | "requires": { 51 | "jwa": "^1.4.1", 52 | "safe-buffer": "^5.0.1" 53 | } 54 | }, 55 | "lodash": { 56 | "version": "4.17.21", 57 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 58 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 59 | }, 60 | "lru-cache": { 61 | "version": "6.0.0", 62 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 63 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 64 | "requires": { 65 | "yallist": "^4.0.0" 66 | } 67 | }, 68 | "ms": { 69 | "version": "2.1.3", 70 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 71 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 72 | }, 73 | "safe-buffer": { 74 | "version": "5.2.1", 75 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 76 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 77 | }, 78 | "semver": { 79 | "version": "7.5.3", 80 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", 81 | "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", 82 | "requires": { 83 | "lru-cache": "^6.0.0" 84 | } 85 | }, 86 | "yallist": { 87 | "version": "4.0.0", 88 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 89 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/config/default/config-cf.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { 4 | "name": "component_type", 5 | "source": { 6 | "type": "static", 7 | "value": "application" 8 | }, 9 | "output": [ 10 | "msg-log", 11 | "req-log" 12 | ] 13 | }, 14 | { 15 | "name": "component_id", 16 | "source": { 17 | "type": "env", 18 | "path": [ 19 | "VCAP_APPLICATION", 20 | "application_id" 21 | ] 22 | }, 23 | "output": [ 24 | "msg-log", 25 | "req-log" 26 | ] 27 | }, 28 | { 29 | "name": "component_name", 30 | "source": { 31 | "type": "env", 32 | "path": [ 33 | "VCAP_APPLICATION", 34 | "application_name" 35 | ] 36 | }, 37 | "output": [ 38 | "msg-log", 39 | "req-log" 40 | ] 41 | }, 42 | { 43 | "name": "component_instance", 44 | "source": { 45 | "type": "env", 46 | "path": [ 47 | "VCAP_APPLICATION", 48 | "instance_index" 49 | ] 50 | }, 51 | "output": [ 52 | "msg-log", 53 | "req-log" 54 | ] 55 | }, 56 | { 57 | "name": "source_instance", 58 | "source": { 59 | "type": "env", 60 | "path": [ 61 | "VCAP_APPLICATION", 62 | "instance_index" 63 | ] 64 | }, 65 | "output": [ 66 | "msg-log", 67 | "req-log" 68 | ] 69 | }, 70 | { 71 | "name": "layer", 72 | "source": { 73 | "type": "static", 74 | "value": "[NODEJS]" 75 | }, 76 | "output": [ 77 | "msg-log", 78 | "req-log" 79 | ] 80 | }, 81 | { 82 | "name": "organization_name", 83 | "source": { 84 | "type": "env", 85 | "path": [ 86 | "VCAP_APPLICATION", 87 | "organization_name" 88 | ] 89 | }, 90 | "output": [ 91 | "msg-log", 92 | "req-log" 93 | ] 94 | }, 95 | { 96 | "name": "organization_id", 97 | "source": { 98 | "type": "env", 99 | "path": [ 100 | "VCAP_APPLICATION", 101 | "organization_id" 102 | ] 103 | }, 104 | "output": [ 105 | "msg-log", 106 | "req-log" 107 | ] 108 | }, 109 | { 110 | "name": "space_name", 111 | "source": { 112 | "type": "env", 113 | "path": [ 114 | "VCAP_APPLICATION", 115 | "space_name" 116 | ] 117 | }, 118 | "output": [ 119 | "msg-log", 120 | "req-log" 121 | ] 122 | }, 123 | { 124 | "name": "space_id", 125 | "source": { 126 | "type": "env", 127 | "path": [ 128 | "VCAP_APPLICATION", 129 | "space_id" 130 | ] 131 | }, 132 | "output": [ 133 | "msg-log", 134 | "req-log" 135 | ] 136 | }, 137 | { 138 | "name": "container_id", 139 | "source": { 140 | "type": "env", 141 | "varName": "CF_INSTANCE_IP" 142 | }, 143 | "output": [ 144 | "msg-log", 145 | "req-log" 146 | ] 147 | } 148 | ] 149 | } 150 | -------------------------------------------------------------------------------- /tools/token-creator/token-creator.js: -------------------------------------------------------------------------------- 1 | var program = require('commander'); 2 | var jwt = require('jsonwebtoken'); 3 | var fs = require('fs'); 4 | 5 | const levels = [ 6 | "error", "warn", "info", "verbose", "debug", "silly" 7 | ]; 8 | 9 | const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/g; 10 | 11 | 12 | program 13 | .arguments('') 14 | .option('-k, --key ', 'a private key to sign token with') 15 | .option('-f, --keyfile ', 'a path to a file containing the private key') 16 | .option('-p, --passphrase ', 'private key passphrase (optional)') 17 | .option('-v, --validityPeriod ', 'number of days the token will expire after') 18 | .option('-i, --issuer ', 'valid issuer e-mail address') 19 | .action(function (level) { 20 | var period = 2; // default period 2 days 21 | var key = null; 22 | var exp = null; 23 | var issuer = "firstname.lastname@sap.com"; 24 | 25 | console.log("\n=== TOKEN CREATOR ===\n"); 26 | 27 | if (!validateLevel(level)) { 28 | console.error("Error: Provided level is not valid. Please use \033[3merror, warn, info, verbose, debug or silly\033[0m"); 29 | return; 30 | } 31 | 32 | if (program.key != undefined && program.keyfile != undefined) { 33 | console.error("Error: Please provide either the --key option OR the --keyfile option."); 34 | return; 35 | } 36 | 37 | if (program.keyfile != undefined) { 38 | try { 39 | key = fs.readFileSync(program.keyfile); 40 | console.log("Using private key from keyfile (" + program.keyfile + ").\n"); 41 | } catch (err) { 42 | console.error(err); 43 | return; 44 | } 45 | } else if (program.key != undefined) { 46 | key = "-----BEGIN RSA PRIVATE KEY-----\n" + program.key + "\n-----END RSA PRIVATE KEY-----"; 47 | console.log("Using private key from --key option.\n"); 48 | } else { 49 | // Nice to have: KeyPair generation 50 | console.log("Error: Generating keypairs on-the-fly is currently not supported by this script. Please provide a private key by using the --key or --keyfile option."); 51 | return; 52 | } 53 | 54 | if (program.validityPeriod != undefined) { 55 | if (validatePeriod(program.validityPeriod)) { 56 | period = program.validityPeriod; 57 | } else { 58 | console.error("Error: Validity period is invalid. Must be a number >= 1."); 59 | return; 60 | } 61 | } 62 | 63 | exp = Math.floor(Date.now() / 1000) + period * 86400; // calculate expiration timestamp 64 | 65 | if (program.issuer != undefined) { 66 | if (validateEmail(program.issuer)) { 67 | issuer = program.issuer; 68 | } else { 69 | console.error("Error: The provided issuer e-mail address is invalid."); 70 | return; 71 | } 72 | } 73 | 74 | 75 | console.log("LOGGING LEVEL: " + level); 76 | console.log("ISSUER: " + issuer); 77 | console.log("EXPIRATION DATE: " + exp + " (" + (new Date(exp * 1000).toLocaleString()) + ")"); 78 | 79 | var passphrase = program.passphrase == undefined ? "" : program.passphrase; 80 | 81 | var token = createToken(level, key, passphrase, exp, issuer); 82 | if (token == null) { 83 | console.log("\nError: Failed to create token. Please check key/keyfile and provided options."); 84 | return; 85 | } 86 | 87 | console.log("\nTOKEN: \n" + token); 88 | }) 89 | .parse(process.argv); 90 | 91 | if (program.args.length === 0) program.help(); 92 | 93 | function validateLevel(level) { 94 | return levels.includes(level); 95 | } 96 | 97 | function validatePeriod(days) { 98 | if (!isNaN(days)) { 99 | return (days >= 1); 100 | } else { 101 | return null; 102 | } 103 | } 104 | 105 | function validateEmail(address) { 106 | return emailRegex.test(address); 107 | } 108 | 109 | function createToken(level, key, passphrase, exp, issuer) { 110 | try { 111 | return jwt.sign({ level: level, exp: exp, issuer: issuer }, { key: key, passphrase: passphrase }, { algorithm: 'RS256' }); 112 | } catch (err) { 113 | console.error(err); 114 | return null; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/performance-test/config-cf-with-defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { 4 | "name": "component_type", 5 | "source": { 6 | "type": "static", 7 | "value": "application" 8 | }, 9 | "output": [ 10 | "msg-log", 11 | "req-log" 12 | ], 13 | "default": "-" 14 | }, 15 | { 16 | "name": "component_id", 17 | "source": { 18 | "type": "env", 19 | "path": [ 20 | "VCAP_APPLICATION", 21 | "application_id" 22 | ] 23 | }, 24 | "output": [ 25 | "msg-log", 26 | "req-log" 27 | ], 28 | "default": "-" 29 | }, 30 | { 31 | "name": "component_name", 32 | "source": { 33 | "type": "env", 34 | "path": [ 35 | "VCAP_APPLICATION", 36 | "application_name" 37 | ] 38 | }, 39 | "output": [ 40 | "msg-log", 41 | "req-log" 42 | ], 43 | "default": "-" 44 | }, 45 | { 46 | "name": "component_instance", 47 | "source": { 48 | "type": "env", 49 | "path": [ 50 | "VCAP_APPLICATION", 51 | "instance_index" 52 | ] 53 | }, 54 | "output": [ 55 | "msg-log", 56 | "req-log" 57 | ], 58 | "default": "-" 59 | }, 60 | { 61 | "name": "source_instance", 62 | "source": { 63 | "type": "env", 64 | "path": [ 65 | "VCAP_APPLICATION", 66 | "instance_index" 67 | ] 68 | }, 69 | "output": [ 70 | "msg-log", 71 | "req-log" 72 | ], 73 | "default": "0" 74 | }, 75 | { 76 | "name": "layer", 77 | "source": { 78 | "type": "static", 79 | "value": "[NODEJS]" 80 | }, 81 | "output": [ 82 | "msg-log", 83 | "req-log" 84 | ] 85 | }, 86 | { 87 | "name": "organization_name", 88 | "source": { 89 | "type": "env", 90 | "path": [ 91 | "VCAP_APPLICATION", 92 | "organization_name" 93 | ] 94 | }, 95 | "output": [ 96 | "msg-log", 97 | "req-log" 98 | ], 99 | "default": "-" 100 | }, 101 | { 102 | "name": "organization_id", 103 | "source": { 104 | "type": "env", 105 | "path": [ 106 | "VCAP_APPLICATION", 107 | "organization_id" 108 | ] 109 | }, 110 | "output": [ 111 | "msg-log", 112 | "req-log" 113 | ], 114 | "default": "-" 115 | }, 116 | { 117 | "name": "space_name", 118 | "source": { 119 | "type": "env", 120 | "path": [ 121 | "VCAP_APPLICATION", 122 | "space_name" 123 | ] 124 | }, 125 | "output": [ 126 | "msg-log", 127 | "req-log" 128 | ], 129 | "default": "-" 130 | }, 131 | { 132 | "name": "space_id", 133 | "source": { 134 | "type": "env", 135 | "path": [ 136 | "VCAP_APPLICATION", 137 | "space_id" 138 | ] 139 | }, 140 | "output": [ 141 | "msg-log", 142 | "req-log" 143 | ], 144 | "default": "-" 145 | }, 146 | { 147 | "name": "container_id", 148 | "source": { 149 | "type": "env", 150 | "varName": "CF_INSTANCE_IP" 151 | }, 152 | "output": [ 153 | "msg-log", 154 | "req-log" 155 | ], 156 | "default": "-" 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /docs/getting-started/02-framework-samples.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Framework Samples 4 | parent: Getting Started 5 | nav_order: 1 6 | permalink: /getting-started/framework-samples/ 7 | --- 8 | 9 | # Samples for supported Server Frameworks 10 | {: .no_toc } 11 | 12 | This library can be used in combination with several different server frameworks. 13 | You can find small code samples for each supported framework below. 14 | 15 |
16 | 17 | Table of contents 18 | 19 | {: .text-delta } 20 | 1. TOC 21 | {:toc} 22 |
23 | 24 | ## Express 25 | 26 | ```js 27 | const app = require('express')() 28 | const log = require('cf-nodejs-logging-support') 29 | 30 | // Configure logger for working with Express framework 31 | log.setFramework(log.Framework.Express) 32 | 33 | // Add the logger middleware to write access logs 34 | app.use(log.logNetwork) 35 | 36 | // Handle '/' path 37 | app.get("/", (req, res) => { 38 | // Write a log message bound to request context 39 | req.logger.info(`Sending a greeting`) 40 | res.send("Hello Express") 41 | }) 42 | 43 | // Listen on specified port 44 | const listener = app.listen(3000, () => { 45 | // Formatted log message 46 | log.info("Server is listening on port %d", listener.address().port) 47 | }) 48 | ``` 49 | 50 | ## Connect 51 | 52 | ```js 53 | const app = require('connect')() 54 | const http = require('http') 55 | const log = require('cf-nodejs-logging-support') 56 | 57 | // Configure logger for working with Connect framework 58 | log.setFramework(log.Framework.Connect) 59 | 60 | // Add the logger middleware to write access logs 61 | app.use(log.logNetwork) 62 | 63 | // Handle '/' path 64 | app.use("/", (req, res) => { 65 | // Write a log message bound to request context 66 | req.logger.info(`Sending a greeting`) 67 | res.end("Hello Connect") 68 | }) 69 | 70 | // Listen on specified port 71 | const server = http.createServer(app).listen(3000, () => { 72 | // Formatted log message 73 | log.info("Server is listening on port %d", server.address().port) 74 | }) 75 | ``` 76 | 77 | ## Restify 78 | 79 | ```js 80 | const restify = require('restify') 81 | const log = require('cf-nodejs-logging-support') 82 | const app = restify.createServer() 83 | 84 | // Configure logger for working with Restify framework 85 | log.setFramework(log.Framework.Restify) 86 | 87 | // Add the logger middleware to write access logs 88 | app.use(log.logNetwork) 89 | 90 | // Handle '/' path 91 | app.get("/", (req, res, next) => { 92 | // Write a log message bound to request context 93 | req.logger.info(`Sending a greeting`) 94 | res.send("Hello Restify") 95 | next() 96 | }) 97 | 98 | // Listen on specified port 99 | app.listen(3000, () => { 100 | // Formatted log message 101 | log.info("Server is listening on port %d", app.address().port) 102 | }) 103 | ``` 104 | 105 | ## Fastify 106 | 107 | ```js 108 | const log = require('cf-nodejs-logging-support') 109 | const app = require('fastify')() 110 | 111 | // Configure logger for working with Fastify framework 112 | log.setFramework(log.Framework.Fastify) 113 | 114 | // Add the logger middleware to write access logs 115 | app.addHook("onRequest", log.logNetwork) 116 | 117 | // Handle '/' path 118 | app.get("/", (request, reply) => { 119 | // Write a log message bound to request context 120 | request.logger.info(`Sending a greeting`) 121 | reply.send("Hello Fastify") 122 | }) 123 | 124 | // Listen on specified port 125 | app.listen({ port: 3000 }, (err, address) => { 126 | if (err) { 127 | // Formatted error message 128 | log.error("Failed to run server", err.message) 129 | process.exit(1) 130 | } 131 | // Formatted log message 132 | log.info(`Server is listening on ${address}`) 133 | }) 134 | ``` 135 | 136 | ## Node.js HTTP 137 | 138 | ```js 139 | const log = require('cf-nodejs-logging-support') 140 | const http = require('http') 141 | 142 | // Configure logger for working with Node.js http framework 143 | log.setFramework(log.Framework.NodeJsHttp) 144 | 145 | const server = http.createServer((req, res) => { 146 | // Call logger middleware to write access logs 147 | log.logNetwork(req, res) 148 | 149 | // Write a log message bound to request context 150 | req.logger.info(`Sending a greeting`) 151 | res.send("Hello Node.js HTTP") 152 | }) 153 | 154 | // Listen on specified port 155 | server.listen(3000, () => { 156 | // Formatted log message 157 | log.info("Server is listening on port %d", server.address().port) 158 | }) 159 | ``` 160 | -------------------------------------------------------------------------------- /src/lib/logger/rootLogger.ts: -------------------------------------------------------------------------------- 1 | import Config from '../config/config'; 2 | import { 3 | ConfigObject, CustomFieldsFormat, CustomFieldsTypeConversion, Framework, Output, SourceType 4 | } from '../config/interfaces'; 5 | import EnvService from '../helper/envService'; 6 | import Middleware from '../middleware/middleware'; 7 | import RequestAccessor from '../middleware/requestAccessor'; 8 | import ResponseAccessor from '../middleware/responseAccessor'; 9 | import createTransport from '../winston/winstonTransport'; 10 | import { Level } from './level'; 11 | import { Logger } from './logger'; 12 | import RecordWriter from './recordWriter'; 13 | 14 | export default class RootLogger extends Logger { 15 | private static instance: RootLogger; 16 | private config = Config.getInstance(); 17 | 18 | private constructor() { 19 | super() 20 | this.loggingLevelThreshold = Level.Info 21 | } 22 | 23 | static getInstance(): RootLogger { 24 | if (!RootLogger.instance) { 25 | RootLogger.instance = new RootLogger(); 26 | } 27 | 28 | return RootLogger.instance; 29 | } 30 | 31 | getConfig() { 32 | return this.config.getConfig(); 33 | } 34 | 35 | getConfigFields(...fieldNames: string[]) { 36 | return this.config.getConfigFields(fieldNames); 37 | } 38 | 39 | addConfig(...configObject: ConfigObject[]) { 40 | return this.config.addConfig(configObject); 41 | } 42 | 43 | clearFieldsConfig() { 44 | return this.config.clearFieldsConfig(); 45 | } 46 | 47 | setCustomFieldsFormat(format: CustomFieldsFormat) { 48 | return this.config.setCustomFieldsFormat(format); 49 | } 50 | 51 | setCustomFieldsTypeConversion(conversion: CustomFieldsTypeConversion) { 52 | return this.config.setCustomFieldsTypeConversion(conversion); 53 | } 54 | 55 | setStartupMessageEnabled(enabled: boolean) { 56 | return this.config.setStartupMessageEnabled(enabled); 57 | } 58 | 59 | setSinkFunction(func: (level: string, payload: string) => any) { 60 | RecordWriter.getInstance().setSinkFunction(func); 61 | } 62 | 63 | enableTracing(input: string | string[]) { 64 | return this.config.enableTracing(input); 65 | } 66 | 67 | logNetwork(req: any, res: any, next: any) { 68 | Middleware.logNetwork(req, res, next); 69 | } 70 | 71 | getBoundServices() { 72 | return EnvService.getInstance().getBoundServices() 73 | } 74 | 75 | createWinstonTransport(options?: any) { 76 | if (!options) { 77 | options = {}; 78 | } 79 | if (!options.rootLogger) { 80 | options.rootLogger = this; 81 | } 82 | return createTransport(options); 83 | } 84 | 85 | setFramework(framework: Framework) { 86 | Config.getInstance().setFramework(framework); 87 | RequestAccessor.getInstance().setFrameworkService(); 88 | ResponseAccessor.getInstance().setFrameworkService(); 89 | } 90 | 91 | // legacy methods 92 | 93 | forceLogger(framework: Framework) { 94 | this.setFramework(framework) 95 | } 96 | 97 | overrideNetworkField(field: string, value: string): boolean { 98 | if (field == null && typeof field != "string") { 99 | return false; 100 | } 101 | // get field and override config 102 | const configField = this.config.getConfigFields([field]); 103 | 104 | // if new field, then add as static field 105 | if (configField.length == 0) { 106 | this.config.addConfig([ 107 | { 108 | "fields": 109 | [ 110 | { 111 | "name": field, 112 | "source": { 113 | "type": SourceType.Static, 114 | "value": value 115 | }, 116 | "output": [ 117 | Output.ReqLog 118 | ] 119 | }, 120 | ] 121 | } 122 | ]); 123 | return true; 124 | } 125 | 126 | // set static source and override 127 | configField[0].source = { 128 | "type": SourceType.Static, 129 | "value": value 130 | }; 131 | this.config.addConfig([ 132 | { 133 | "fields": [configField[0]] 134 | } 135 | ]); 136 | return true; 137 | } 138 | 139 | overrideCustomFieldFormat(format: CustomFieldsFormat) { 140 | return this.setCustomFieldsFormat(format); 141 | } 142 | 143 | setLogPattern() { } // no longer supported 144 | } 145 | -------------------------------------------------------------------------------- /docs/advanced-usage/04-dynamic-logging-level-threshold.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Dynamic Logging Level Threshold 4 | parent: Advanced Usage 5 | nav_order: 4 6 | permalink: /advanced-usage/dynamic-logging-level-threshold 7 | --- 8 | 9 | # Dynamic Logging Level Threshold 10 | {: .no_toc } 11 | For debugging purposes it can be useful to change the logging level threshold for specific requests. 12 | This can be achieved using a special header field or setting directly within the corresponding request handler. 13 | Changing the logging level threshold affects if logs with a specific level are written. 14 | It has no effect on the level reported as part of the logs. 15 | 16 |
17 | 18 | Table of contents 19 | 20 | {: .text-delta } 21 | 1. TOC 22 | {:toc} 23 |
24 | 25 | ## Change logging level threshold via header field 26 | 27 | You can change the logging level threshold for a specific request by providing a JSON Web Token ([JWT](https://de.wikipedia.org/wiki/JSON_Web_Token)) via the request header. 28 | Using this feature allows changing the logging level threshold dynamically without the need to redeploy your app. 29 | 30 | ### 1. Creating a key-pair 31 | 32 | To sign and verify JWTs a PEM encoded private key and a matching public key is required. 33 | 34 | You can create a private key using the following command: 35 | 36 | ```sh 37 | openssl genrsa -out private.pem 2048 38 | ``` 39 | 40 | To create a public key from a private key use following command: 41 | 42 | ```sh 43 | openssl rsa -in private.pem -outform PEM -pubout -out public.pem 44 | ``` 45 | 46 | The generated key-pair can be found in `private.pem` and `public.pem` files. 47 | 48 | ### 2. Creating a JWT 49 | 50 | JWTs are signed claims, which consist of a header, a payload, and a signature. 51 | They can be signed using RSA or HMAC signing algorithms. 52 | For this use-case we decided to support RSA algorithms (RS256, RS384 and RS512) only. 53 | In contrast to HMAC algorithms (HS256, HS384 and HS512), RSA algorithms are asymmetric and therefore require key pairs (public and private key). 54 | 55 | You can create JWTs by using the provided [TokenCreator](https://github.com/SAP/cf-nodejs-logging-support/tree/master/tools/token-creator): 56 | 57 | ```sh 58 | cd tools/token-creator/ 59 | npm install 60 | node token-creator.js -f -v -i 61 | ``` 62 | 63 | The `` sets the number of days the JWT will be valid. 64 | Once the created JWT expired, it can no longer be used for setting logging level threshold. 65 | Provide a numeric input for this placeholder. 66 | 67 | Provide a valid e-mail address for the `` parameter. 68 | 69 | Specify one of the seven supported logging levels for the `` argument: *off*, *error*, *warn*, *info*, *verbose*, *debug*, and *silly*. 70 | 71 | The payload of the created JWT has the following structure: 72 | 73 | ```js 74 | { 75 | "issuer": "", 76 | "level": "debug", 77 | "iat": 1506016127, 78 | "exp": 1506188927 79 | } 80 | ``` 81 | 82 | ### 3. Providing the public key 83 | 84 | The logging library will verify JWTs attached to incoming requests. 85 | In order to do so, the public key (from `public.pem` file) needs to be provided via an environment variable called DYN_LOG_LEVEL_KEY: 86 | 87 | ```text 88 | DYN_LOG_LEVEL_KEY: 89 | ``` 90 | 91 | Typically your public key file should have following structure: 92 | 93 | ```text 94 | -----BEGIN PUBLIC KEY----- 95 | 96 | -----END PUBLIC KEY----- 97 | ``` 98 | 99 | Instead of using the whole content of the `public.pem` file, you can also only provide the `` section to the environment variable. 100 | 101 | Redeploy your app after setting the environment variable. 102 | 103 | ### 4. Attaching JWTs to requests 104 | 105 | Provide the created JWT via a header field named 'SAP-LOG-LEVEL'. The logging level threshold will be set to the provided level for this request and corresponding custom log messages. 106 | 107 | **Note**: If the provided JWT cannot be verified, is expired, or contains an invalid logging level, the library ignores it and uses the global logging level threshold. 108 | 109 | If you want to use another header name for the JWT, you can specify it using an environment variable: 110 | 111 | ```text 112 | DYN_LOG_HEADER: MY-HEADER-FIELD 113 | ``` 114 | 115 | ## Change logging level threshold within request handlers 116 | 117 | You can also change the logging level threshold for all requests of a specific request handler as follows: 118 | 119 | ```js 120 | req.setLoggingLevel("verbose"); 121 | ``` 122 | 123 | Alternatively you can also do this by setting a configuration file as explained in [Default Request Level](/cf-nodejs-logging-support/configuration/default-request-level). 124 | 125 | This feature is also available for [Child Loggers](/cf-nodejs-logging-support/advanced-usage/child-loggers#). 126 | -------------------------------------------------------------------------------- /src/performance-test/test-app/app.js: -------------------------------------------------------------------------------- 1 | process.env.VCAP_SERVICES = "CF"; 2 | process.env.VCAP_APPLICATION = JSON.stringify( 3 | { 4 | "cf_api": "test-cf-api", 5 | "limits": { 6 | "fds": 32768 7 | }, 8 | "application_name": "test-application-name", 9 | "application_uris": [ 10 | "test-application-uris" 11 | ], 12 | "name": "test-name", 13 | "space_name": "test-space-name", 14 | "space_id": "test-space-id", 15 | "organization_id": "test-organization-id", 16 | "organization_name": "test-organization-name", 17 | "uris": [ 18 | "test-uris" 19 | ], 20 | "users": null, 21 | "application_id": "test-application-id" 22 | } 23 | ); 24 | process.env.LOG_SENSITIVE_CONNECTION_DATA = false; 25 | process.env.LOG_REMOTE_USER = false; 26 | process.env.LOG_REFERER = false; 27 | 28 | process.env.TEST_SENSITIVE_DATA = false; 29 | // saves public key 30 | process.env.DYN_LOG_LEVEL_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fzU8StO511QYoC+BZp4riR2eVQM8FPPB2mF4I78WBDzloAVTaz0Z7hkMog1rAy8+Xva+fLiMuxDmN7kQZKBc24O4VeKNjOt8ZtNhz3vlMTZrNQ7bi+j8TS8ycUgKqe4/hSmjJBfXoduZ8Ye90u8RRfPLzbuutctLfCnL/ZhEehqfilt1iQb/CRCEsJou5XahmvOO5Gt+9kTBmY+2rS/+HKKdAhI3OpxwvXXNi8m9LrdHosMD7fTUpLUgdcIp8k3ACp9wCIIxbv1ssDeWKy7bKePihTl7vJq6RkopS6GvhO6yiD1IAJF/iDOrwrJAWzanrtavUc1RJZvbOvD0DFFOwIDAQAB"; 31 | 32 | const express = require("express"); 33 | // var log = require("cf-nodejs-logging-support"); 34 | var log = require("../../../build/main/index"); 35 | const app = express(); 36 | 37 | // roots the views directory to public 38 | app.set('views', 'public'); 39 | 40 | app.set('view engine', 'html'); 41 | 42 | // tells express that the public folder is the static folder 43 | app.use(express.static(__dirname + "/public")); 44 | 45 | // add logger to the server network queue to log all incoming requests. 46 | app.use(log.logNetwork); 47 | 48 | // set the logging level threshold 49 | log.setLoggingLevel("info"); 50 | 51 | // register names of custom fields 52 | log.registerCustomFields(["global-field-a", "node_version", "pid", "platform", "custom-field-a", "custom-field-b", "new-field"]); 53 | 54 | // set a custom field globally, so that it will be logged for all following messages independent of their request/child context. 55 | log.setCustomFields({ "global-field-a": "value" }); 56 | 57 | // home route 58 | app.get("/", function (req, res) { 59 | res.send(); 60 | }); 61 | 62 | // demonstrate log in global context 63 | app.get("/globalcontext", function (req, res) { 64 | 65 | for (let index = 0; index < 10000; index++) { 66 | log.logMessage("info", "Message logged in global context"); 67 | } 68 | res.send(); 69 | }); 70 | 71 | // demonstrate log in global context 72 | app.get("/testlognetwork", function (req, res) { 73 | res.send(); 74 | }); 75 | 76 | // demonstrate log in request context 77 | app.get("/requestcontext", function (req, res) { 78 | var reqLogger = req.logger; // reqLogger logs in request context 79 | 80 | 81 | for (let index = 0; index < 100000; index++) { 82 | reqLogger.logMessage("info", "Message logged in request context"); 83 | } 84 | 85 | res.send(); 86 | }); 87 | 88 | // log message with some custom fields in global context 89 | app.get("/customfields", function (req, res) { 90 | log.setCustomFieldsFormat("application-logging"); 91 | log.logMessage("info", "Message logged in global context with some custom fields", { "custom-field-a": "value-a", "custom-field-b": "value-b" }); 92 | res.send(); 93 | }); 94 | 95 | // demonstrate an error stack trace logging 96 | app.get("/stacktrace", function (req, res) { 97 | try { 98 | alwaysError(); 99 | res.send("request succesful"); 100 | } catch (e) { 101 | log.logMessage("error", "Error occurred", e) 102 | res.status(500).send("error ocurred"); 103 | } 104 | }); 105 | 106 | function alwaysError() { 107 | throw new Error("An error happened. Stacktrace will be displayed."); 108 | } 109 | 110 | // create a new child from the logger object and overide/create new fields 111 | app.get("/childlogger", function (req, res) { 112 | var subLogger = log.createLogger({ "new-field": "value" }); 113 | subLogger.setLoggingLevel("warn"); 114 | subLogger.logMessage("warn", "Message logged from child logger."); 115 | res.send(); 116 | }); 117 | 118 | // get the correlation and tenant ID by calling req.logger.getCorrelationId() and .getTenantId() 119 | app.get("/correlationandtenantid", function (req, res) { 120 | var reqLogger = req.logger; // reqLogger logs in request context 121 | var correlationId = reqLogger.getCorrelationId(); 122 | var tenantId = reqLogger.getTenantId(); 123 | reqLogger.logMessage("info", "Correlation ID: %s Tenant ID: %s", correlationId, tenantId); 124 | res.send(); 125 | }); 126 | 127 | 128 | // binds the express module to 'app', set port and run server 129 | var port = Number(process.env.VCAP_APP_PORT || 8081); 130 | app.listen(port, function () { 131 | log.logMessage("info", "listening on port: %d", port); 132 | }); 133 | -------------------------------------------------------------------------------- /sample/cf-nodejs-logging-support-sample/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const log = require("cf-nodejs-logging-support"); 3 | const app = express(); 4 | var lastMessage = null; 5 | 6 | // roots the views directory to public 7 | app.set('views', 'public'); 8 | 9 | app.set('view engine', 'html'); 10 | 11 | // tells express that the public folder is the static folder 12 | app.use(express.static(__dirname + "/public")); 13 | 14 | // add logger to the server network queue to log all incoming requests. 15 | app.use(log.logNetwork); 16 | 17 | log.setSinkFunction((_, msg) => { 18 | lastMessage = msg 19 | console.log(msg) 20 | }); 21 | 22 | // set the logging level threshold 23 | log.setLoggingLevel("info"); 24 | 25 | // register names of custom fields 26 | log.registerCustomFields(["global-field-a", "node_version", "pid", "platform", "custom-field-a", "custom-field-b", "new-field"]); 27 | 28 | // set a custom field globally, so that it will be logged for all following messages independent of their request/child context. 29 | log.setCustomFields({ "global-field-a": "value" }); 30 | 31 | // log some custom fields 32 | var stats = { 33 | node_version: process.version, 34 | pid: process.pid, 35 | platform: process.platform, 36 | }; 37 | log.info("Message logged in global context with custom fields", stats); 38 | 39 | // home route 40 | app.get("/", function (req, res) { 41 | res.send(); 42 | }); 43 | 44 | // demonstrate log in global context 45 | // https://sap.github.io/cf-nodejs-logging-support/general-usage/logging-contexts#global-context 46 | app.get("/globalcontext", function (req, res) { 47 | log.info("Message logged in global context"); 48 | res.send(lastMessage); 49 | }); 50 | 51 | // demonstrate log in request context 52 | // https://sap.github.io/cf-nodejs-logging-support/general-usage/logging-contexts#request-context 53 | app.get("/requestcontext", function (req, res) { 54 | var reqLogger = req.logger; // reqLogger logs in request context 55 | reqLogger.info("Message logged in request context"); 56 | res.send(lastMessage); 57 | }); 58 | 59 | // log message with some custom fields in global context 60 | // https://sap.github.io/cf-nodejs-logging-support/general-usage/custom-fields 61 | app.get("/customfields", function (req, res) { 62 | log.info("Message logged in global context with some custom fields", { "custom-field-a": "value-a", "custom-field-b": "value-b" }); 63 | res.send(lastMessage); 64 | }); 65 | 66 | // log message with some custom fields in global context 67 | // https://sap.github.io/cf-nodejs-logging-support/general-usage/custom-fields 68 | app.get("/passport", function (req, res) { 69 | log.info("Message with passport", { "sap_passport": "2A54482A0300E60000756E64657465726D696E6564202020202020202020202020202020202020202000005341505F4532455F54415F557365722020202020202020202020202020202020756E64657465726D696E65645F737461727475705F302020202020202020202020202020202020200005756E64657465726D696E6564202020202020202020202020202020202020202034323946383939424439414334374342393330314345463933443144453039432020200007793BCF7D8152423B8A7FD073109C45CE0000000000000000000000000000000000000000000000E22A54482A" }); 70 | res.send(lastMessage); 71 | }); 72 | 73 | // demonstrate an error stack trace logging 74 | // https://sap.github.io/cf-nodejs-logging-support/general-usage/stacktraces 75 | app.get("/stacktrace", function (req, res) { 76 | try { 77 | alwaysError(); 78 | res.send("request succesful"); 79 | } catch (e) { 80 | log.error("Error occurred", e) 81 | res.status(500).send(lastMessage); 82 | } 83 | }); 84 | 85 | function alwaysError() { 86 | throw new Error("An error happened. Stacktrace will be displayed."); 87 | } 88 | 89 | // create a new child from the logger object and overide/create new fields 90 | // https://sap.github.io/cf-nodejs-logging-support/advanced-usage/child-loggers 91 | app.get("/childlogger", function (req, res) { 92 | var subLogger = log.createLogger({ "new-field": "value" }); 93 | subLogger.setLoggingLevel("warn"); 94 | subLogger.warn("Message logged from child logger."); 95 | res.send(lastMessage); 96 | }); 97 | 98 | // get the correlation and tenant ID by calling req.logger.getCorrelationId() and .getTenantId() 99 | // https://sap.github.io/cf-nodejs-logging-support/advanced-usage/correlation-and-tenant-data 100 | app.get("/correlationandtenantid", function (req, res) { 101 | var reqLogger = req.logger; // reqLogger logs in request context 102 | var correlationId = reqLogger.getCorrelationId(); 103 | var tenantId = reqLogger.getTenantId(); 104 | reqLogger.info("Correlation ID: %s Tenant ID: %s", correlationId, tenantId); 105 | res.send(lastMessage); 106 | }); 107 | 108 | app.get("/binding_information", function (_, res) { 109 | result = "There are the following bindings:
" 110 | var services 111 | if (process.env.VCAP_SERVICES) 112 | services = JSON.parse(process.env.VCAP_SERVICES) 113 | if (services == undefined) 114 | res.send("There are no bindings present for this application") 115 | else { 116 | for (var service in services) 117 | result += service + "
" 118 | res.send(result) 119 | } 120 | }) 121 | 122 | 123 | // binds the express module to 'app', set port and run server 124 | var port = Number(process.env.VCAP_APP_PORT || 8080); 125 | app.listen(port, function () { 126 | log.info("listening on port: %d", port); 127 | }); 128 | -------------------------------------------------------------------------------- /docs/general-usage/04-custom-fields.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Custom Fields 4 | parent: General Usage 5 | nav_order: 4 6 | permalink: /general-usage/custom-fields 7 | --- 8 | 9 | # Custom Fields 10 | 11 | Custom fields allow you to enrich your log messages with additional context by attaching key-value pairs. 12 | This is particularly useful for adding metadata that can help with filtering, searching, and analyzing logs. 13 | 14 | ## Format and type transformation for SAP logging services 15 | 16 | While the library can be used independently of SAP logging services, it provides built-in support for logging custom fields compatible with [SAP Application Logging Service](https://help.sap.com/docs/application-logging-service) and [SAP Cloud Logging](https://help.sap.com/docs/cloud-logging). 17 | This includes proper formatting and type conversion of custom fields to ensure compatibility with the respective logging service: 18 | 19 | - **SAP Application Logging**: Custom fields are formatted as key-value pairs within a special `#cf` field in the log message. The values are converted to strings to ensure compatibility with the service's field type requirements. 20 | - **SAP Cloud Logging**: Custom fields are added as top-level fields in the log message. By default, values are also converted to strings to prevent type mismatches during indexing. However, you can configure the library to retain the original types of custom field values if needed. 21 | 22 | When deploying your application to SAP BTP Cloud Foundry, the library automatically detects logging service bindings and sets the custom field format accordingly. 23 | 24 | ### Overriding the custom fields format 25 | 26 | There are cases where you might want to override the default behavior: 27 | - When using the library in an environment without a logging service binding, e.g., on Kubernetes. 28 | - When using a user-provided service where the logging service type cannot be detected correctly. 29 | - When you want to disable custom fields logging entirely. 30 | 31 | In such cases, you can explicitly set the desired custom fields format programmatically: 32 | 33 | ```js 34 | import { CustomFieldsFormat } from "@sap/cf-nodejs-logging-support"; 35 | 36 | log.setCustomFieldsFormat(CustomFieldsFormat.ApplicationLogging); 37 | // or 38 | log.setCustomFieldsFormat("application-logging"); 39 | ``` 40 | 41 | Available formats are: 42 | 43 | | Format Constant | String Value | Description | 44 | |-----------------|--------------|-------------| 45 | | `CustomFieldsFormat.ApplicationLogging` | `application-logging` | Use this format to log custom fields compatible with SAP Application Logging Service. | 46 | | `CustomFieldsFormat.CloudLogging` | `cloud-logging` | Use this format to log custom fields compatible with SAP Cloud Logging. | 47 | | `CustomFieldsFormat.All` | `all` | Use this format to log custom fields compatible with both SAP Application Logging Service and SAP Cloud Logging. | 48 | | `CustomFieldsFormat.Disabled` | `disabled` | Disable custom fields logging. | 49 | | `CustomFieldsFormat.Default` | `default` | Same as `cloud-logging` format. | 50 | 51 | Alternatively, you can force the logging format setting using a configuration file as explained in [Advanced Configuration](/cf-nodejs-logging-support/configuration). 52 | 53 | ### Overriding the custom fields type conversion 54 | 55 | By default, custom field values are converted to strings to avoid type mismatches during indexing. 56 | While this behavior cannot be changed for the custom fields format used for SAP Application Logging, it can be adjusted for SAP Cloud Logging as follows: 57 | 58 | ```js 59 | import { CustomFieldsTypeConversion } from "@sap/cf-nodejs-logging-support"; 60 | 61 | log.setCustomFieldsTypeConversion(CustomFieldsTypeConversion.Retain); 62 | ``` 63 | 64 | Available type conversion options are: 65 | | Type Conversion Constant | Description | 66 | |--------------------------|-------------| 67 | | `CustomFieldsTypeConversion.Stringify` | Convert all custom field values to strings. This is the default behavior. | 68 | | `CustomFieldsTypeConversion.Retain` | Retain the original types of custom field values. | 69 | 70 | Alternatively, you can force the field type conversion setting using a configuration file as explained in [Advanced Configuration](/cf-nodejs-logging-support/configuration). 71 | 72 | Keep in mind that retaining original types may lead to indexing issues if the same custom field is logged with different types across log entries. 73 | 74 | ### Registering custom fields for SAP Application Logging 75 | 76 | When using SAP Application Logging Service, it is necessary to register custom fields before logging them. 77 | Please make sure to do the registration exactly once globally before logging any custom fields. 78 | Registered fields will be indexed based on the order given by the provided field array. 79 | 80 | ```js 81 | log.registerCustomFields(["field"]); 82 | info("Test data", {"field": 42}); 83 | // ... "msg":"Test data" 84 | // ... "#cf": {"string": [{"k":"field","v":"42","i":0}]}... 85 | ``` 86 | 87 | ## Logging custom fields 88 | 89 | In addition to logging messages as described in [Message Logs](/cf-nodejs-logging-support/general-usage/message-logs), you can attach custom fields by passing an object of key-value pairs as the last parameter. 90 | This allows enriching log messages with additional context information. 91 | 92 | ```js 93 | logger.info("My log message", {"field-a" :"value"}); 94 | ``` 95 | 96 | Another way to add custom fields is by setting them on (child) logger instances, either globally or within a request context. 97 | When you do this, all messages logged by that logger will automatically include the specified custom fields, providing consistent context information across your logs. 98 | 99 | ```js 100 | logger.setCustomFields({"field-a": "value"}) 101 | logger.info("My log message"); 102 | ``` 103 | 104 | You can also define custom fields globally by invoking the same method on the global `log` instance. 105 | This ensures that all logs, including request logs and those emitted by child loggers, will automatically include the specified custom fields and their values. 106 | 107 | ```js 108 | log.setCustomFields({"field-b": "test"}); 109 | ``` 110 | 111 | In case you want to remove custom fields from a logger instance you can provide an empty object: 112 | 113 | ```js 114 | logger.setCustomFields({}); 115 | ``` 116 | -------------------------------------------------------------------------------- /src/lib/logger/logger.ts: -------------------------------------------------------------------------------- 1 | import LevelUtils from '../helper/levelUtils'; 2 | import { isValidObject } from '../middleware/utils'; 3 | import { Level } from './level'; 4 | import RecordFactory from './recordFactory'; 5 | import RecordWriter from './recordWriter'; 6 | import Context from './context'; 7 | 8 | export class Logger { 9 | private parent?: Logger = undefined 10 | private context?: Context; 11 | private registeredCustomFields: Array = []; 12 | private customFields: Map = new Map() 13 | private recordFactory: RecordFactory; 14 | private recordWriter: RecordWriter; 15 | protected loggingLevelThreshold: Level = Level.Inherit 16 | 17 | constructor(parent?: Logger, context?: Context) { 18 | if (parent) { 19 | this.parent = parent; 20 | this.registeredCustomFields = parent.registeredCustomFields; 21 | } 22 | if (context) { 23 | this.context = context; 24 | } 25 | this.recordFactory = RecordFactory.getInstance(); 26 | this.recordWriter = RecordWriter.getInstance(); 27 | } 28 | 29 | createLogger(customFields?: Map | Object, createNewContext?: boolean): Logger { 30 | let context = createNewContext == true ? new Context() : this.context 31 | let logger = new Logger(this, context); 32 | // assign custom fields, if provided 33 | if (customFields) { 34 | logger.setCustomFields(customFields); 35 | } 36 | return logger; 37 | } 38 | 39 | setLoggingLevel(level: string | Level) { 40 | if (typeof level === 'string') { 41 | this.loggingLevelThreshold = LevelUtils.getLevel(level) 42 | } else { 43 | this.loggingLevelThreshold = level 44 | } 45 | } 46 | 47 | getLoggingLevel(): string { 48 | if (this.loggingLevelThreshold == Level.Inherit) { 49 | return this.parent!.getLoggingLevel() 50 | } 51 | return LevelUtils.getName(this.loggingLevelThreshold) 52 | } 53 | 54 | isLoggingLevel(level: string | Level): boolean { 55 | if (this.loggingLevelThreshold == Level.Inherit) { 56 | return this.parent!.isLoggingLevel(level) 57 | } 58 | if (typeof level === 'string') { 59 | return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, LevelUtils.getLevel(level)) 60 | } else { 61 | return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, level) 62 | } 63 | } 64 | 65 | logMessage(level: string | Level, ...args: any) { 66 | if (!this.isLoggingLevel(level)) return; 67 | const loggerCustomFields = this.getCustomFieldsFromLogger(this); 68 | 69 | let levelName: string; 70 | if (typeof level === 'string') { 71 | levelName = level; 72 | } else { 73 | levelName = LevelUtils.getName(level); 74 | } 75 | 76 | const record = this.recordFactory.buildMsgRecord(this.registeredCustomFields, loggerCustomFields, levelName, args, this.context); 77 | this.recordWriter.writeLog(record); 78 | } 79 | 80 | error(...args: any) { 81 | this.logMessage("error", ...args); 82 | } 83 | 84 | warn(...args: any) { 85 | this.logMessage("warn", ...args); 86 | } 87 | 88 | info(...args: any) { 89 | this.logMessage("info", ...args); 90 | } 91 | 92 | verbose(...args: any) { 93 | this.logMessage("verbose", ...args); 94 | } 95 | 96 | debug(...args: any) { 97 | this.logMessage("debug", ...args); 98 | } 99 | 100 | silly(...args: any) { 101 | this.logMessage("silly", ...args); 102 | } 103 | 104 | isError(): boolean { 105 | return this.isLoggingLevel("error"); 106 | } 107 | 108 | isWarn(): boolean { 109 | return this.isLoggingLevel("warn"); 110 | } 111 | 112 | isInfo(): boolean { 113 | return this.isLoggingLevel("info"); 114 | } 115 | 116 | isVerbose(): boolean { 117 | return this.isLoggingLevel("verbose"); 118 | } 119 | 120 | isDebug(): boolean { 121 | return this.isLoggingLevel("debug"); 122 | } 123 | 124 | isSilly(): boolean { 125 | return this.isLoggingLevel("silly"); 126 | } 127 | 128 | registerCustomFields(fieldNames: Array) { 129 | this.registeredCustomFields.splice(0, this.registeredCustomFields.length); 130 | this.registeredCustomFields.push(...fieldNames); 131 | } 132 | 133 | setCustomFields(customFields: Map | Object) { 134 | if (customFields instanceof Map) { 135 | this.customFields = customFields; 136 | } else if (isValidObject(customFields)) { 137 | this.customFields = new Map(Object.entries(customFields)) 138 | } 139 | } 140 | 141 | getCustomFields(): Map { 142 | return this.getCustomFieldsFromLogger(this) 143 | } 144 | 145 | getContextProperty(name: string): string | undefined { 146 | return this.context?.getProperty(name); 147 | } 148 | 149 | setContextProperty(name: string, value: string) : boolean { 150 | if (this.context) { 151 | this.context.setProperty(name, value); 152 | return true 153 | } 154 | return false 155 | } 156 | 157 | getCorrelationId(): string | undefined { 158 | return this.getContextProperty("correlation_id"); 159 | } 160 | 161 | setCorrelationId(value: string) : boolean { 162 | return this.setContextProperty("correlation_id", value); 163 | } 164 | 165 | getTenantId(): string | undefined { 166 | return this.getContextProperty("tenant_id"); 167 | } 168 | 169 | setTenantId(value: string) : boolean { 170 | return this.setContextProperty("tenant_id", value); 171 | } 172 | 173 | getTenantSubdomain(): string | undefined { 174 | return this.getContextProperty("tenant_subdomain"); 175 | } 176 | 177 | setTenantSubdomain(value: string) : boolean { 178 | return this.setContextProperty("tenant_subdomain", value); 179 | } 180 | 181 | private getCustomFieldsFromLogger(logger: Logger): Map { 182 | if (logger.parent && logger.parent !== this) { 183 | let parentFields = this.getCustomFieldsFromLogger(logger.parent); 184 | return new Map([...parentFields, ...logger.customFields]); 185 | } 186 | return logger.customFields; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/test/acceptance-test/global-context.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | const expect = require('chai').expect; 3 | const importFresh = require('import-fresh'); 4 | 5 | var log; 6 | var lastLevel; 7 | var lastOutput; 8 | var logCount; 9 | 10 | describe('Test logging in global context', function () { 11 | 12 | beforeEach(function () { 13 | log = importFresh("../../../build/main/index"); 14 | 15 | logCount = 0; 16 | lastLevel = ""; 17 | lastOutput = ""; 18 | 19 | log.setSinkFunction((level, output) => { 20 | lastLevel = level; 21 | lastOutput = JSON.parse(output); 22 | logCount++; 23 | }); 24 | }); 25 | 26 | describe('Write a log with a simple message', function () { 27 | beforeEach(function () { 28 | log.logMessage("info", "test-message"); 29 | }); 30 | 31 | it('writes exactly one log', function () { 32 | assert.equal(logCount, 1); 33 | }); 34 | 35 | it('writes a log containing with the message', function () { 36 | assert(lastOutput.msg, "test-message"); 37 | }); 38 | 39 | it('writes with level info', function () { 40 | expect(lastOutput).to.have.property('level', 'info'); 41 | }); 42 | 43 | it('writes log with all core properties', function () { 44 | const expectedKeys = [ 45 | 'logger', 46 | 'written_at', 47 | 'written_ts' 48 | ]; 49 | expect(lastOutput).to.include.all.keys(expectedKeys); 50 | }); 51 | }); 52 | 53 | describe('Write a log with convenience method', function () { 54 | 55 | beforeEach(function () { 56 | log.error("Error message logged in global context"); 57 | }); 58 | 59 | it('writes a log containing the message', function () { 60 | assert(lastOutput.msg, "Error message logged in global context"); 61 | }); 62 | 63 | it('check log level', function () { 64 | expect(lastOutput).to.have.property('level', 'error'); 65 | }); 66 | }); 67 | 68 | describe('Has convenience methods', function () { 69 | 70 | it('error', function () { 71 | expect(log.error).to.be.a('function'); 72 | }); 73 | 74 | it('warn', function () { 75 | expect(log.warn).to.be.a('function'); 76 | }); 77 | 78 | it('info', function () { 79 | expect(log.info).to.be.a('function'); 80 | }); 81 | 82 | it('verbose', function () { 83 | expect(log.verbose).to.be.a('function'); 84 | }); 85 | 86 | it('debug', function () { 87 | expect(log.debug).to.be.a('function'); 88 | }); 89 | 90 | it('silly', function () { 91 | expect(log.silly).to.be.a('function'); 92 | }); 93 | 94 | }); 95 | 96 | describe('Write message with formating', function () { 97 | beforeEach(function () { 98 | log.logMessage("info", "Listening on test port %d", 5000); 99 | }); 100 | 101 | it('writes exactly one log', function () { 102 | assert.equal(logCount, 1); 103 | }); 104 | 105 | it('writes a log containing the message', function () { 106 | assert(lastOutput.msg, "Listening on test port 5000"); 107 | }); 108 | }); 109 | 110 | describe('overrideNetworkField', function () { 111 | 112 | beforeEach(function () { 113 | log.overrideNetworkField("logger", "new-value"); 114 | log.logMessage("info", "test-message"); 115 | }); 116 | 117 | it('overrides field', function () { 118 | expect(lastOutput).to.have.property('logger', 'new-value'); 119 | }); 120 | 121 | 122 | afterEach(function () { 123 | log.overrideNetworkField("logger", "TEST"); 124 | }) 125 | }); 126 | 127 | describe('Test regExp constraint', function () { 128 | beforeEach(function () { 129 | log.logMessage("info", "test-message"); 130 | }); 131 | 132 | it('writes field with correct uuid format', function () { 133 | expect(lastOutput).to.have.property('uuid_field'); 134 | }); 135 | 136 | it('does not write field with incorrect uuid format', function () { 137 | expect(lastOutput).to.not.have.property('will_never_log'); 138 | }); 139 | }); 140 | 141 | describe('Set log severity level', function () { 142 | 143 | beforeEach(function () { 144 | log.setLoggingLevel("error"); 145 | log.logMessage("info", "test-message"); 146 | }); 147 | 148 | it('Does not write log', function () { 149 | expect(logCount).to.be.eql(0); 150 | }); 151 | }); 152 | 153 | describe('Check log severity level', function () { 154 | 155 | beforeEach(function () { 156 | log.setLoggingLevel("error"); 157 | }); 158 | 159 | it('Checks with isLoggingLevel()', function () { 160 | var isErrorActive = log.isLoggingLevel("error"); 161 | expect(isErrorActive).to.be.true; 162 | }); 163 | 164 | it('Checks with convencience method', function () { 165 | var isDebugActive = log.isDebug(); 166 | expect(isDebugActive).to.be.false; 167 | }); 168 | }); 169 | 170 | describe('Log a stacktrace', function () { 171 | 172 | beforeEach(function () { 173 | const e = new Error("An error happened."); 174 | log.error("Error occurred", e); 175 | }); 176 | 177 | it('logs stacktrace field', function () { 178 | expect(lastOutput).to.have.property('stacktrace'); 179 | expect(lastOutput.stacktrace).to.be.an('array'); 180 | var isArrayOfStrings = lastOutput.stacktrace.every(x => typeof (x) === 'string'); 181 | expect(isArrayOfStrings).to.be.true; 182 | }); 183 | 184 | }); 185 | 186 | describe('Test disabled context', function () { 187 | 188 | it('cannot set context properties', function () { 189 | expect(log.setContextProperty("some-field", "some-value")).to.be.false; 190 | }); 191 | 192 | it('cannot set the correlation_id', function () { 193 | expect(log.setCorrelationId("f79ed23f-cff6-4599-8668-12838c898b70")).to.be.false; 194 | }); 195 | 196 | it('cannot set the tenant_id', function () { 197 | expect(log.setTenantId("some-value")).to.be.false; 198 | }); 199 | 200 | it('cannot set the correlation_id', function () { 201 | expect(log.setTenantSubdomain("some-value")).to.be.false; 202 | }); 203 | 204 | }); 205 | 206 | after(function () { 207 | log.setLoggingLevel("info"); 208 | }) 209 | }); 210 | -------------------------------------------------------------------------------- /src/lib/config/default/config-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "additionalProperties": false, 4 | "definitions": { 5 | "ConfigField": { 6 | "additionalProperties": false, 7 | "properties": { 8 | "_meta": { 9 | "$ref": "#/definitions/ConfigFieldMeta" 10 | }, 11 | "convert": { 12 | "$ref": "#/definitions/Conversion" 13 | }, 14 | "default": { 15 | "type": [ 16 | "string", 17 | "number", 18 | "boolean" 19 | ] 20 | }, 21 | "disable": { 22 | "type": "boolean" 23 | }, 24 | "envVarRedact": { 25 | "type": "string" 26 | }, 27 | "envVarSwitch": { 28 | "type": "string" 29 | }, 30 | "isContext": { 31 | "type": "boolean" 32 | }, 33 | "name": { 34 | "type": "string" 35 | }, 36 | "output": { 37 | "items": { 38 | "$ref": "#/definitions/Output" 39 | }, 40 | "type": "array" 41 | }, 42 | "settable": { 43 | "type": "boolean" 44 | }, 45 | "source": { 46 | "anyOf": [ 47 | { 48 | "$ref": "#/definitions/Source" 49 | }, 50 | { 51 | "items": { 52 | "$ref": "#/definitions/Source" 53 | }, 54 | "type": "array" 55 | } 56 | ] 57 | } 58 | }, 59 | "required": [ 60 | "name", 61 | "output" 62 | ], 63 | "type": "object" 64 | }, 65 | "ConfigFieldMeta": { 66 | "additionalProperties": false, 67 | "properties": { 68 | "isCache": { 69 | "type": "boolean" 70 | }, 71 | "isContext": { 72 | "type": "boolean" 73 | }, 74 | "isEnabled": { 75 | "type": "boolean" 76 | }, 77 | "isRedacted": { 78 | "type": "boolean" 79 | } 80 | }, 81 | "required": [ 82 | "isCache", 83 | "isContext", 84 | "isEnabled", 85 | "isRedacted" 86 | ], 87 | "type": "object" 88 | }, 89 | "Conversion": { 90 | "enum": [ 91 | "parseBoolean", 92 | "parseFloat", 93 | "parseInt", 94 | "toString" 95 | ], 96 | "type": "string" 97 | }, 98 | "CustomFieldsFormat": { 99 | "enum": [ 100 | "all", 101 | "application-logging", 102 | "cloud-logging", 103 | "default", 104 | "disabled" 105 | ], 106 | "type": "string" 107 | }, 108 | "CustomFieldsTypeConversion": { 109 | "enum": [ 110 | "retain", 111 | "stringify" 112 | ], 113 | "type": "string" 114 | }, 115 | "DetailName": { 116 | "enum": [ 117 | "level", 118 | "message", 119 | "requestReceivedAt", 120 | "responseSentAt", 121 | "responseTimeMs", 122 | "stacktrace", 123 | "writtenAt", 124 | "writtenTs" 125 | ], 126 | "type": "string" 127 | }, 128 | "Framework": { 129 | "enum": [ 130 | "connect", 131 | "express", 132 | "fastify", 133 | "plainhttp", 134 | "restify" 135 | ], 136 | "type": "string" 137 | }, 138 | "Output": { 139 | "enum": [ 140 | "msg-log", 141 | "req-log" 142 | ], 143 | "type": "string" 144 | }, 145 | "Source": { 146 | "additionalProperties": false, 147 | "properties": { 148 | "detailName": { 149 | "$ref": "#/definitions/DetailName" 150 | }, 151 | "fieldName": { 152 | "type": "string" 153 | }, 154 | "framework": { 155 | "$ref": "#/definitions/Framework" 156 | }, 157 | "output": { 158 | "$ref": "#/definitions/Output" 159 | }, 160 | "path": { 161 | "items": { 162 | "type": "string" 163 | }, 164 | "type": "array" 165 | }, 166 | "regExp": { 167 | "type": "string" 168 | }, 169 | "type": { 170 | "$ref": "#/definitions/SourceType" 171 | }, 172 | "value": { 173 | "type": "string" 174 | }, 175 | "varName": { 176 | "type": "string" 177 | } 178 | }, 179 | "required": [ 180 | "type" 181 | ], 182 | "type": "object" 183 | }, 184 | "SourceType": { 185 | "enum": [ 186 | "config-field", 187 | "detail", 188 | "env", 189 | "req-header", 190 | "req-object", 191 | "res-header", 192 | "res-object", 193 | "static", 194 | "uuid" 195 | ], 196 | "type": "string" 197 | } 198 | }, 199 | "properties": { 200 | "customFieldsFormat": { 201 | "$ref": "#/definitions/CustomFieldsFormat" 202 | }, 203 | "customFieldsTypeConversion": { 204 | "$ref": "#/definitions/CustomFieldsTypeConversion" 205 | }, 206 | "fields": { 207 | "items": { 208 | "$ref": "#/definitions/ConfigField" 209 | }, 210 | "type": "array" 211 | }, 212 | "framework": { 213 | "$ref": "#/definitions/Framework" 214 | }, 215 | "outputStartupMsg": { 216 | "type": "boolean" 217 | }, 218 | "reqLoggingLevel": { 219 | "type": "string" 220 | } 221 | }, 222 | "type": "object" 223 | } 224 | 225 | -------------------------------------------------------------------------------- /src/test/acceptance-test/child-logger.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | const importFresh = require('import-fresh'); 3 | 4 | var log; 5 | var lastOutput; 6 | var logCount; 7 | var childLogger; 8 | 9 | describe('Test child logger', function () { 10 | 11 | beforeEach(function () { 12 | log = importFresh("../../../build/main/index"); 13 | 14 | lastOutput = ""; 15 | logCount = 0; 16 | childLogger = ""; 17 | 18 | log.setSinkFunction(function (level, output) { 19 | lastLevel = level; 20 | lastOutput = JSON.parse(output); 21 | logCount++; 22 | 23 | }); 24 | }); 25 | 26 | describe('Create child logger with new custom field', function () { 27 | 28 | beforeEach(function () { 29 | log.registerCustomFields(["child-field"]); 30 | childLogger = log.createLogger({ "child-field": "value" }); 31 | childLogger.logMessage("info", "test-message"); 32 | }); 33 | 34 | it('is an object', function () { 35 | expect(childLogger).to.be.an('object'); 36 | }); 37 | 38 | it('is not the same object as parent', function () { 39 | expect(childLogger == log).to.be.false; 40 | }); 41 | 42 | it('logs with new custom field', function () { 43 | expect(lastOutput).to.have.property('child-field', 'value'); 44 | }); 45 | 46 | it('implements info()', function () { 47 | expect(childLogger.info).to.be.a('function'); 48 | }); 49 | 50 | it('implements logMessage()', function () { 51 | expect(childLogger.logMessage).to.be.a('function'); 52 | }); 53 | 54 | it('implements isLoggingLevel()', function () { 55 | expect(childLogger.isLoggingLevel).to.be.a('function'); 56 | }); 57 | 58 | it('implements getCorrelationId()', function () { 59 | expect(childLogger.getCorrelationId).to.be.a('function'); 60 | }); 61 | 62 | it('implements setCorrelationId()', function () { 63 | expect(childLogger.setCorrelationId).to.be.a('function'); 64 | }); 65 | 66 | it('implements setTenantId()', function () { 67 | expect(childLogger.setTenantId).to.be.a('function'); 68 | }); 69 | 70 | it('implements getTenantId()', function () { 71 | expect(childLogger.getTenantId).to.be.a('function'); 72 | }); 73 | 74 | it('implements setTenantSubdomain()', function () { 75 | expect(childLogger.setTenantSubdomain).to.be.a('function'); 76 | }); 77 | 78 | it('implements getTenantSubdomain()', function () { 79 | expect(childLogger.getTenantSubdomain).to.be.a('function'); 80 | }); 81 | 82 | it('implements setLoggingLevel()', function () { 83 | expect(childLogger.setLoggingLevel).to.be.a('function'); 84 | }); 85 | 86 | it('implements getLoggingLevel()', function () { 87 | expect(childLogger.getLoggingLevel).to.be.a('function'); 88 | }); 89 | 90 | it('implements setCustomFields()', function () { 91 | expect(childLogger.setCustomFields).to.be.a('function'); 92 | }); 93 | 94 | it('implements createLogger()', function () { 95 | expect(childLogger.createLogger).to.be.a('function'); 96 | }); 97 | 98 | it('inherit registered custom fields from parent', function () { 99 | expect(childLogger.registeredCustomFields).to.eql(log.registeredCustomFields); 100 | }); 101 | }); 102 | 103 | describe('Test convenience method', function () { 104 | 105 | beforeEach(function () { 106 | childLogger = log.createLogger(); 107 | childLogger.warn("test-message"); 108 | }); 109 | 110 | it('logs in warn level', function () { 111 | expect(lastOutput).to.have.property('level', 'warn'); 112 | }); 113 | 114 | it('logs with a message', function () { 115 | expect(lastOutput).to.have.property('msg', 'test-message'); 116 | }); 117 | }); 118 | 119 | describe('Test context features', function () { 120 | 121 | describe('Create child logger without context', function () { 122 | 123 | beforeEach(function () { 124 | childLogger = log.createLogger(); 125 | }); 126 | 127 | it('cannot set context properties', function () { 128 | expect(childLogger.setContextProperty('some-field', 'some-value')).to.be.false; 129 | }); 130 | 131 | it('does not log a custom context property', function () { 132 | childLogger.setContextProperty('some-field', 'some-value'); 133 | childLogger.warn('test-message'); 134 | expect(lastOutput).to.not.include.key('some-field'); 135 | }); 136 | 137 | it('does not log the correlation_id', function () { 138 | childLogger.warn("test-message"); 139 | expect(lastOutput).to.not.include.key('correlation_id'); 140 | }); 141 | }); 142 | 143 | describe('Create child logger with new context', function () { 144 | 145 | beforeEach(function () { 146 | childLogger = log.createLogger(null, true); 147 | }); 148 | 149 | it('sets context properties', function () { 150 | expect(childLogger.setContextProperty('some-field', 'some-value')).to.be.true; 151 | }); 152 | 153 | it('logs a custom context property', function () { 154 | childLogger.setContextProperty('some-field', 'some-value'); 155 | childLogger.warn('test-message'); 156 | expect(lastOutput).to.include.property('some-field', 'some-value'); 157 | }); 158 | 159 | it('inherits the context', function () { 160 | childLogger.setContextProperty('some-field', 'some-value'); 161 | childLogger.createLogger().warn('test-message'); 162 | expect(lastOutput).to.include.property('some-field', 'some-value'); 163 | }); 164 | 165 | it('generates a correlation_id', function () { 166 | childLogger.warn("test-message"); 167 | expect(lastOutput).to.include.key('correlation_id'); 168 | expect(lastOutput.correlation_id).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}/); 169 | }); 170 | }); 171 | }); 172 | 173 | describe('Set logging level threshold per child logger', function () { 174 | 175 | describe('Child logger sets treshold', function () { 176 | 177 | beforeEach(function () { 178 | childLogger = log.createLogger(); 179 | childLogger.setLoggingLevel('debug'); 180 | childLogger.logMessage("debug", "test-message"); 181 | }); 182 | 183 | it('logs in debug level', function () { 184 | expect(lastOutput).to.have.property('msg', 'test-message'); 185 | }); 186 | }); 187 | 188 | describe('Parent logger is not affected', function () { 189 | 190 | beforeEach(function () { 191 | childLogger = log.createLogger(); 192 | childLogger.setLoggingLevel('error'); 193 | log.logMessage("info", "test-message"); 194 | }); 195 | 196 | it('parent logs in info level', function () { 197 | expect(lastOutput).to.have.property('msg', 'test-message'); 198 | }); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /src/lib/logger/recordFactory.ts: -------------------------------------------------------------------------------- 1 | import jsonStringifySafe from 'json-stringify-safe'; 2 | import util from 'util'; 3 | 4 | import Config from '../config/config'; 5 | import { CustomFieldsFormat, CustomFieldsTypeConversion, Output } from '../config/interfaces'; 6 | import StacktraceUtils from '../helper/stacktraceUtils'; 7 | import { isValidObject } from '../middleware/utils'; 8 | import Cache from './cache'; 9 | import Record from './record'; 10 | import Context from './context'; 11 | import SourceUtils from './sourceUtils'; 12 | 13 | export default class RecordFactory { 14 | 15 | private static instance: RecordFactory; 16 | private config: Config; 17 | private stacktraceUtils: StacktraceUtils; 18 | private sourceUtils: SourceUtils; 19 | private cache: Cache; 20 | 21 | private constructor() { 22 | this.config = Config.getInstance(); 23 | this.sourceUtils = SourceUtils.getInstance(); 24 | this.cache = Cache.getInstance(); 25 | this.stacktraceUtils = StacktraceUtils.getInstance(); 26 | } 27 | 28 | static getInstance(): RecordFactory { 29 | if (!RecordFactory.instance) { 30 | RecordFactory.instance = new RecordFactory(); 31 | } 32 | 33 | return RecordFactory.instance; 34 | } 35 | 36 | // init a new record and assign fields with output "msg-log" 37 | buildMsgRecord(registeredCustomFields: Array, loggerCustomFields: Map, levelName: string, args: Array, context?: Context): Record { 38 | const lastArg = args[args.length - 1]; 39 | let customFieldsFromArgs = new Map(); 40 | let record = new Record(levelName) 41 | 42 | 43 | if (typeof lastArg === "object") { 44 | if (this.stacktraceUtils.isErrorWithStacktrace(lastArg)) { 45 | record.metadata.stacktrace = this.stacktraceUtils.prepareStacktrace(lastArg.stack); 46 | } else if (isValidObject(lastArg)) { 47 | if (this.stacktraceUtils.isErrorWithStacktrace(lastArg._error)) { 48 | record.metadata.stacktrace = this.stacktraceUtils.prepareStacktrace(lastArg._error.stack); 49 | delete lastArg._error; 50 | } 51 | customFieldsFromArgs = new Map(Object.entries(lastArg)); 52 | } else if (lastArg instanceof Map) { 53 | customFieldsFromArgs = lastArg; 54 | } 55 | args.pop(); 56 | } 57 | 58 | // assign static fields from cache 59 | const cacheFields = this.config.getCacheMsgFields(); 60 | const cacheMsgRecord = this.cache.getCacheMsgRecord(cacheFields); 61 | Object.assign(record.payload, cacheMsgRecord); 62 | 63 | record.metadata.message = util.format.apply(util, args); 64 | 65 | // assign dynamic fields 66 | this.addDynamicFields(record, Output.MsgLog); 67 | 68 | // assign values from request context if present 69 | if (context) { 70 | this.addContext(record, context); 71 | } 72 | 73 | // assign custom fields 74 | this.addCustomFields(record, registeredCustomFields, loggerCustomFields, customFieldsFromArgs); 75 | 76 | return record; 77 | } 78 | 79 | // init a new record and assign fields with output "req-log" 80 | buildReqRecord(levelName: string, req: any, res: any, context: Context): Record { 81 | let record = new Record(levelName) 82 | 83 | // assign static fields from cache 84 | const cacheFields = this.config.getCacheReqFields(); 85 | const cacheReqRecord = this.cache.getCacheReqRecord(cacheFields, req, res); 86 | Object.assign(record.payload, cacheReqRecord); 87 | 88 | // assign dynamic fields 89 | this.addDynamicFields(record, Output.ReqLog, req, res); 90 | 91 | // assign values request context 92 | this.addContext(record, context); 93 | 94 | // assign custom fields 95 | const loggerCustomFields = req.logger.getCustomFieldsFromLogger(req.logger); 96 | this.addCustomFields(record, req.logger.registeredCustomFields, loggerCustomFields); 97 | 98 | return record; 99 | } 100 | 101 | private addCustomFields(record: Record, registeredCustomFields: Array, loggerCustomFields: Map, 102 | customFieldsFromArgs: Map = new Map()) { 103 | const providedFields = new Map([...loggerCustomFields, ...customFieldsFromArgs]); 104 | const customFieldsFormat = this.config.getConfig().customFieldsFormat!; 105 | const customFieldsTypeConversion = this.config.getConfig().customFieldsTypeConversion!; 106 | 107 | // if format "disabled", do not log any custom fields 108 | if (customFieldsFormat == CustomFieldsFormat.Disabled) { 109 | return; 110 | } 111 | 112 | let indexedCustomFields: any = {}; 113 | providedFields.forEach((value, key) => { 114 | if ([CustomFieldsFormat.CloudLogging, CustomFieldsFormat.All, CustomFieldsFormat.Default].includes(customFieldsFormat) 115 | || record.payload[key] != null || this.config.isSettable(key)) { 116 | // Stringify, if conversion type 'stringify' is selected and value is not a string already. 117 | if (customFieldsTypeConversion == CustomFieldsTypeConversion.Stringify && (typeof value) != "string") { 118 | record.payload[key] = jsonStringifySafe(value); 119 | } else { 120 | record.payload[key] = value; 121 | } 122 | } 123 | 124 | if ([CustomFieldsFormat.ApplicationLogging, CustomFieldsFormat.All].includes(customFieldsFormat)) { 125 | // Stringify, if necessary. 126 | if ((typeof value) != "string") { 127 | indexedCustomFields[key] = jsonStringifySafe(value); 128 | } else { 129 | indexedCustomFields[key] = value; 130 | } 131 | } 132 | }); 133 | 134 | // Write custom fields in the correct order and correlates i to the place in registeredCustomFields 135 | if (Object.keys(indexedCustomFields).length > 0) { 136 | let res: any = {}; 137 | res.string = []; 138 | let key; 139 | for (let i = 0; i < registeredCustomFields.length; i++) { 140 | key = registeredCustomFields[i] 141 | if (indexedCustomFields[key]) { 142 | let value = indexedCustomFields[key]; 143 | res.string.push({ 144 | "k": key, 145 | "v": value, 146 | "i": i 147 | }) 148 | } 149 | } 150 | if (res.string.length > 0) { 151 | record.payload["#cf"] = res; 152 | } 153 | } 154 | } 155 | 156 | // read and copy values from context 157 | private addContext(record: Record, context: Context) { 158 | const contextFields = context.getProperties(); 159 | for (let key in contextFields) { 160 | if (contextFields[key] != null) { 161 | record.payload[key] = contextFields[key]; 162 | } 163 | } 164 | } 165 | 166 | private addDynamicFields(record: Record, output: Output, req?: any, res?: object) { 167 | // assign dynamic fields 168 | const fields = (output == Output.MsgLog) ? this.config.noCacheMsgFields : this.config.noCacheReqFields; 169 | fields.forEach( 170 | field => { 171 | // ignore context fields because they are handled in addContext() 172 | if (field._meta?.isContext == true) { 173 | return; 174 | } 175 | 176 | record.payload[field.name] = this.sourceUtils.getValue(field, record, output, req, res); 177 | } 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/lib/logger/sourceUtils.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | 3 | import Config from '../config/config'; 4 | import { ConfigField, Conversion, DetailName, Output, Source, SourceType } from '../config/interfaces'; 5 | import EnvVarHelper from '../helper/envVarHelper'; 6 | import RequestAccessor from '../middleware/requestAccessor'; 7 | import ResponseAccessor from '../middleware/responseAccessor'; 8 | import Record from './record'; 9 | 10 | const NS_PER_MS = 1e6; 11 | const REDACTED_PLACEHOLDER = "redacted"; 12 | 13 | export default class SourceUtils { 14 | private static instance: SourceUtils; 15 | private requestAccessor: RequestAccessor; 16 | private responseAccessor: ResponseAccessor; 17 | private config: Config; 18 | private lastTimestamp = 0; 19 | 20 | private constructor() { 21 | this.requestAccessor = RequestAccessor.getInstance(); 22 | this.responseAccessor = ResponseAccessor.getInstance(); 23 | this.config = Config.getInstance(); 24 | } 25 | 26 | static getInstance(): SourceUtils { 27 | if (!SourceUtils.instance) { 28 | SourceUtils.instance = new SourceUtils(); 29 | } 30 | 31 | return SourceUtils.instance; 32 | } 33 | 34 | getValue(field: ConfigField, record: Record, output: Output, req?: any, res?: any): string | number | boolean | undefined { 35 | if (!field.source) return undefined 36 | 37 | let sources = Array.isArray(field.source) ? field.source : [field.source] 38 | let value: string | number | boolean | undefined; 39 | 40 | let sourceIndex = 0; 41 | 42 | while (value == null) { 43 | sourceIndex = this.getNextValidSourceIndex(sources, output, sourceIndex); 44 | if (sourceIndex == -1) { 45 | break; 46 | } 47 | 48 | let source = sources[sourceIndex]; 49 | 50 | value = this.getValueFromSource(source, record, output, req, res) 51 | sourceIndex++; 52 | } 53 | 54 | // Handle default 55 | if (value == null && field.default != null) { 56 | value = field.default; 57 | } 58 | 59 | if (value != null && field.convert != null) { 60 | switch(field.convert) { 61 | case Conversion.ToString: 62 | value = value.toString ? value.toString() : undefined 63 | break; 64 | case Conversion.ParseBoolean: 65 | value = this.parseBooleanValue(value) 66 | break; 67 | case Conversion.ParseInt: 68 | value = this.parseIntValue(value) 69 | break; 70 | case Conversion.ParseFloat: 71 | value = this.parseFloatValue(value) 72 | break; 73 | } 74 | } 75 | 76 | // Replaces all fields, which are marked to be reduced and do not equal to their default value to REDUCED_PLACEHOLDER. 77 | if (field._meta!.isRedacted == true && value != null && value != field.default) { 78 | value = REDACTED_PLACEHOLDER; 79 | } 80 | 81 | return value 82 | } 83 | 84 | private getValueFromSource(source: Source, record: Record, output: Output, req?: any, res?: any): string | number | boolean | undefined { 85 | let value: string | number | boolean | undefined; 86 | switch (source.type) { 87 | case SourceType.ReqHeader: 88 | value = req ? this.requestAccessor.getHeaderField(req, source.fieldName!) : undefined; 89 | break; 90 | case SourceType.ReqObject: 91 | value = req ? this.requestAccessor.getField(req, source.fieldName!) : undefined; 92 | break; 93 | case SourceType.ResHeader: 94 | value = res ? this.responseAccessor.getHeaderField(res, source.fieldName!) : undefined; 95 | break; 96 | case SourceType.ResObject: 97 | value = res ? this.responseAccessor.getField(res, source.fieldName!) : undefined; 98 | break; 99 | case SourceType.Static: 100 | value = source.value; 101 | break; 102 | case SourceType.Env: 103 | value = this.getEnvFieldValue(source); 104 | break; 105 | case SourceType.ConfigField: 106 | let fields = this.config.getConfigFields([source.fieldName!]) 107 | value = fields.length >= 1 ? this.getValue(fields[0], record, output, req, res) : undefined 108 | break; 109 | case SourceType.Detail: 110 | value = this.getDetail(source.detailName!, record, req, res) 111 | break; 112 | case SourceType.UUID: 113 | value = uuid(); 114 | break; 115 | } 116 | 117 | if (source.regExp && value != null && typeof value == "string") { 118 | value = this.validateRegExp(value, source.regExp); 119 | } 120 | 121 | return value 122 | } 123 | 124 | private getDetail(detailName: DetailName, record: Record, req?: any, res?: any) : string | number | undefined { 125 | let value: string | number | undefined; 126 | switch (detailName as DetailName) { 127 | case DetailName.RequestReceivedAt: 128 | value = req ? new Date(req._receivedAt).toJSON() : undefined; 129 | break; 130 | case DetailName.ResponseSentAt: 131 | value = res ? new Date(res._sentAt).toJSON() : undefined; 132 | break; 133 | case DetailName.ResponseTimeMs: 134 | value = req && res ? (res._sentAt - req._receivedAt) : undefined; 135 | break; 136 | case DetailName.WrittenAt: 137 | value = new Date().toJSON(); 138 | break; 139 | case DetailName.WrittenTs: 140 | const lower = process.hrtime()[1] % NS_PER_MS 141 | const higher = Date.now() * NS_PER_MS 142 | let writtenTs = higher + lower; 143 | 144 | // This reorders written_ts, if the new timestamp seems to be smaller 145 | // due to different rollover times for process.hrtime and reqReceivedAt.getTime 146 | if (writtenTs < this.lastTimestamp) { 147 | writtenTs += NS_PER_MS; 148 | } 149 | this.lastTimestamp = writtenTs; 150 | value = writtenTs; 151 | break; 152 | case DetailName.Message: 153 | value = record.metadata.message 154 | break; 155 | case DetailName.Stacktrace: 156 | value = record.metadata.stacktrace 157 | break; 158 | case DetailName.Level: 159 | value = record.metadata.level 160 | break; 161 | } 162 | return value; 163 | } 164 | 165 | private getEnvFieldValue(source: Source): string | number | undefined { 166 | if (source.path) { 167 | return EnvVarHelper.getInstance().resolveNestedVar(source.path); 168 | } else { 169 | return process.env[source.varName!]; 170 | } 171 | } 172 | 173 | // returns -1 when all sources were iterated 174 | private getNextValidSourceIndex(sources: Source[], output: Output, startIndex: number): number { 175 | const framework = this.config.getFramework(); 176 | 177 | for (let i = startIndex; i < sources.length; i++) { 178 | let source = sources[i]; 179 | if (!source.framework || source.framework == framework) { 180 | if (!source.output || source.output == output) { 181 | return i; 182 | } 183 | } 184 | } 185 | return -1; 186 | } 187 | 188 | private validateRegExp(value: string, regEx: string): string | undefined { 189 | const regExp = new RegExp(regEx); 190 | const isValid = regExp.test(value); 191 | if (isValid) { 192 | return value; 193 | } 194 | return undefined; 195 | } 196 | 197 | private parseIntValue(value: string | number | boolean): number { 198 | switch (typeof value) { 199 | case 'string': 200 | return parseInt(value, 0) 201 | case 'number': 202 | return value 203 | case 'boolean': 204 | return value ? 1 : 0 205 | } 206 | } 207 | 208 | private parseFloatValue(value: string | number | boolean): number { 209 | switch (typeof value) { 210 | case 'string': 211 | return parseFloat(value) 212 | case 'number': 213 | return value 214 | case 'boolean': 215 | return value ? 1 : 0 216 | } 217 | } 218 | 219 | private parseBooleanValue(value: string | number | boolean) : boolean { 220 | return value === 'true' || value === 'TRUE' || value === 'True' || value === 1 || value === true 221 | } 222 | } 223 | --------------------------------------------------------------------------------