16 |
31 |
32 |
The message sent:
33 |
34 |
35 |
Binding information:
36 |
39 |
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 |
--------------------------------------------------------------------------------