diff --git a/package-lock.json b/package-lock.json
index de87e5fe..b34adb6f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,8 @@
"version": "8.0.0",
"license": "Apache-2.0",
"dependencies": {
+ "@opentelemetry/api": "^1.9.1",
+ "@opentelemetry/api-logs": "^0.222.0",
"ajv": "^8.18.0",
"json-stringify-safe": "^5.0.1",
"jsonwebtoken": "^9.0.3",
@@ -17,6 +19,9 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@opentelemetry/context-async-hooks": "^2.7.1",
+ "@opentelemetry/sdk-logs": "^0.222.0",
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
"@types/json-stringify-safe": "^5.0.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.6.0",
@@ -407,9 +412,9 @@
}
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -417,7 +422,7 @@
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/@eslint/config-array/node_modules/minimatch": {
@@ -975,6 +980,139 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.222.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz",
+ "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/context-async-hooks": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz",
+ "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz",
+ "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz",
+ "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs": {
+ "version": "0.222.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.222.0.tgz",
+ "integrity": "sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.222.0",
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/resources": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.4.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz",
+ "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/resources": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz",
+ "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/resources": "2.11.0",
+ "@opentelemetry/sdk-trace": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@paralleldrive/cuid2": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
@@ -1275,16 +1413,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
@@ -1581,21 +1719,21 @@
}
},
"node_modules/body-parser": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
- "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
- "content-type": "^1.0.5",
+ "content-type": "^2.0.0",
"debug": "^4.4.3",
- "http-errors": "^2.0.0",
- "iconv-lite": "^0.7.0",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
"on-finished": "^2.4.1",
- "qs": "^6.14.1",
- "raw-body": "^3.0.1",
- "type-is": "^2.0.1"
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
},
"engines": {
"node": ">=18"
@@ -1605,6 +1743,20 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
+ "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/body-parser/node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@@ -1661,24 +1813,10 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/body-parser/node_modules/type-is/node_modules/content-type": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
- "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2405,9 +2543,9 @@
}
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -2415,7 +2553,7 @@
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
@@ -2672,27 +2810,6 @@
"node": ">= 0.8"
}
},
- "node_modules/express/node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/express/node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@@ -2780,16 +2897,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/express/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/express/node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
@@ -3532,19 +3639,24 @@
"license": "MIT"
},
"node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
@@ -4567,16 +4679,16 @@
}
},
"node_modules/nyc/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/nyc/node_modules/cliui": {
@@ -5140,12 +5252,14 @@
}
},
"node_modules/qs": {
- "version": "6.15.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
- "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.1.0"
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
@@ -5184,35 +5298,6 @@
"node": ">= 0.10"
}
},
- "node_modules/raw-body/node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "dev": true,
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/raw-body/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "dev": true,
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/readdirp": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
@@ -5552,16 +5637,16 @@
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/rimraf/node_modules/glob": {
@@ -5727,27 +5812,6 @@
"node": ">= 0.8"
}
},
- "node_modules/serve-static/node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/serve-static/node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -5802,16 +5866,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/serve-static/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -5854,15 +5908,15 @@
}
},
"node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@@ -5874,14 +5928,14 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -5996,10 +6050,11 @@
"license": "BSD-3-Clause"
},
"node_modules/statuses": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -6165,16 +6220,16 @@
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/test-exclude/node_modules/glob": {
diff --git a/package.json b/package.json
index 468deecf..8e953642 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,9 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+ "@opentelemetry/context-async-hooks": "^2.7.1",
+ "@opentelemetry/sdk-logs": "^0.222.0",
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
"@types/json-stringify-safe": "^5.0.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.6.0",
@@ -75,6 +78,8 @@
"README.md"
],
"dependencies": {
+ "@opentelemetry/api": "^1.9.1",
+ "@opentelemetry/api-logs": "^0.222.0",
"ajv": "^8.18.0",
"json-stringify-safe": "^5.0.1",
"jsonwebtoken": "^9.0.3",
diff --git a/src/index.ts b/src/index.ts
index f2edbb2e..b5ccaebc 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,3 +5,5 @@ export default rootLogger;
export * from "./lib/config/interfaces.js";
export * from "./lib/logger/level.js";
export * from "./lib/logger/logger.js";
+export * from "./lib/plugins/stdoutOutput.js";
+export * from "./lib/plugins/otelOutput.js";
diff --git a/src/lib/config/default/config-schema.json b/src/lib/config/default/config-schema.json
index a2d65f01..aa002bdd 100644
--- a/src/lib/config/default/config-schema.json
+++ b/src/lib/config/default/config-schema.json
@@ -121,6 +121,9 @@
"writtenTs",
"message",
"stacktrace",
+ "rawStacktrace",
+ "errorName",
+ "errorMessage",
"level"
],
"type": "string"
diff --git a/src/lib/config/interfaces.ts b/src/lib/config/interfaces.ts
index 39e9c6b6..6db5ede6 100644
--- a/src/lib/config/interfaces.ts
+++ b/src/lib/config/interfaces.ts
@@ -85,6 +85,9 @@ export enum DetailName {
WrittenTs = "writtenTs",
Message = "message",
Stacktrace = "stacktrace",
+ RawStacktrace = "rawStacktrace",
+ ErrorName = "errorName",
+ ErrorMessage = "errorMessage",
Level = "level"
}
diff --git a/src/lib/helper/levelUtils.ts b/src/lib/helper/levelUtils.ts
deleted file mode 100644
index 75d6c72c..00000000
--- a/src/lib/helper/levelUtils.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Level } from '../logger/level.js';
-
-export default class LevelUtils {
-
- private static readonly defaultLevel: Level = Level.Info
-
- static getLevel(name: string): Level {
- const key = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
- const level: Level = Level[key as keyof typeof Level]
- if (level === undefined) {
- return LevelUtils.defaultLevel;
- }
- return level
- }
-
- static getName(level: Level): string {
- return Level[level].toLowerCase()
- }
-
- static isLevelEnabled(threshold: Level, level: Level) {
- if (level <= Level.Off) return false;
- return level <= threshold
- }
-}
diff --git a/src/lib/helper/pluginProvider.ts b/src/lib/helper/pluginProvider.ts
new file mode 100644
index 00000000..8535ffd2
--- /dev/null
+++ b/src/lib/helper/pluginProvider.ts
@@ -0,0 +1,31 @@
+import { OutputPlugin } from "../plugins/interfaces";
+
+export default class PluginProvider {
+ private static instance: PluginProvider;
+ private outputPlugins: OutputPlugin[];
+
+ private constructor() {
+ this.outputPlugins = [];
+ }
+
+ static getInstance(): PluginProvider {
+ if (!PluginProvider.instance) {
+ PluginProvider.instance = new PluginProvider();
+ }
+ return PluginProvider.instance;
+ }
+
+ addOutputPlugin(outputPlugin: OutputPlugin) {
+ this.outputPlugins.push(outputPlugin);
+ }
+
+ setOutputPlugins(outputPlugins: OutputPlugin[]) {
+ this.outputPlugins = outputPlugins;
+ }
+
+ getOutputPlugins(): OutputPlugin[] {
+ return this.outputPlugins;
+ }
+}
+
+
diff --git a/src/lib/logger/level.ts b/src/lib/logger/level.ts
index d417c874..4bdac7fe 100644
--- a/src/lib/logger/level.ts
+++ b/src/lib/logger/level.ts
@@ -8,3 +8,29 @@ export enum Level {
Debug = 4,
Silly = 5
}
+
+export class LevelUtils {
+ private static readonly defaultLevel: Level = Level.Info
+
+ static getLevel(level: string | Level): Level {
+ if (typeof level === 'string') {
+ const key = level.charAt(0).toUpperCase() + level.slice(1).toLowerCase();
+ const lvl: Level = Level[key as keyof typeof Level]
+ if (lvl !== undefined) {
+ return lvl;
+ }
+ } else {
+ return level as Level
+ }
+ return LevelUtils.defaultLevel;
+ }
+
+ static getName(level: Level): string {
+ return Level[level].toLowerCase()
+ }
+
+ static isLevelEnabled(threshold: Level, level: Level) {
+ if (level <= Level.Off) return false;
+ return level <= threshold
+ }
+}
diff --git a/src/lib/logger/logger.ts b/src/lib/logger/logger.ts
index c39dfa61..e3acc00e 100644
--- a/src/lib/logger/logger.ts
+++ b/src/lib/logger/logger.ts
@@ -1,9 +1,8 @@
-import LevelUtils from '../helper/levelUtils.js';
import { isValidObject } from '../middleware/utils.js';
-import { Level } from './level.js';
+import { Level, LevelUtils } from './level.js';
import RecordFactory from './recordFactory.js';
-import RecordWriter from './recordWriter.js';
import Context from './context.js';
+import PluginProvider from '../helper/pluginProvider.js';
export class Logger {
private parent?: Logger = undefined
@@ -11,7 +10,7 @@ export class Logger {
private registeredCustomFields: Array
= [];
private customFields: Map = new Map()
private recordFactory: RecordFactory;
- private recordWriter: RecordWriter;
+
protected loggingLevelThreshold: Level = Level.Inherit
constructor(parent?: Logger, context?: Context) {
@@ -23,7 +22,6 @@ export class Logger {
this.context = context;
}
this.recordFactory = RecordFactory.getInstance();
- this.recordWriter = RecordWriter.getInstance();
}
createLogger(customFields?: Map | object, createNewContext?: boolean): Logger {
@@ -37,11 +35,7 @@ export class Logger {
}
setLoggingLevel(level: string | Level) {
- if (typeof level === 'string') {
- this.loggingLevelThreshold = LevelUtils.getLevel(level)
- } else {
- this.loggingLevelThreshold = level
- }
+ this.loggingLevelThreshold = LevelUtils.getLevel(level)
}
getLoggingLevel(): string {
@@ -55,26 +49,14 @@ export class Logger {
if (this.loggingLevelThreshold == Level.Inherit) {
return this.parent!.isLoggingLevel(level)
}
- if (typeof level === 'string') {
- return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, LevelUtils.getLevel(level))
- } else {
- return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, level)
- }
+ return LevelUtils.isLevelEnabled(this.loggingLevelThreshold, LevelUtils.getLevel(level))
}
logMessage(level: string | Level, ...args: any) {
if (!this.isLoggingLevel(level)) return;
const loggerCustomFields = this.getCustomFieldsFromLogger(this);
-
- let levelName: string;
- if (typeof level === 'string') {
- levelName = level;
- } else {
- levelName = LevelUtils.getName(level);
- }
-
- const record = this.recordFactory.buildMsgRecord(this.registeredCustomFields, loggerCustomFields, levelName, args, this.context);
- this.recordWriter.writeLog(record);
+ const record = this.recordFactory.buildMsgRecord(this.registeredCustomFields, loggerCustomFields, LevelUtils.getLevel(level), args, this.context);
+ PluginProvider.getInstance().getOutputPlugins().forEach(output => { output.writeRecord(record) })
}
error(...args: any) {
diff --git a/src/lib/logger/record.ts b/src/lib/logger/record.ts
index 1d9fd579..a4d0a75e 100644
--- a/src/lib/logger/record.ts
+++ b/src/lib/logger/record.ts
@@ -1,11 +1,34 @@
-export default class Record {
- payload: any
- metadata: any
+import { Level } from "./level"
- constructor(level: string) {
+export class Record {
+ payload: { [key: string]: any }
+ metadata: RecordMetadata
+
+ constructor(type: RecordType, level: Level) {
this.payload = {}
- this.metadata = {
- level: level
- }
+ this.metadata = new RecordMetadata(type, level)
+ }
+}
+
+export class RecordMetadata {
+ type: RecordType
+ level: Level
+ message?: string
+ rawStacktrace?: string
+ stacktrace?: string[]
+ errorName?: string
+ errorMessage?: string
+ customFieldNames: string[]
+
+ constructor(type: RecordType, level: Level) {
+ this.type = type
+ this.level = level
+ this.customFieldNames = new Array()
}
-}
\ No newline at end of file
+}
+
+export type RecordFieldValue = string | string[] | number | boolean
+
+export enum RecordType {
+ Request, Message
+}
diff --git a/src/lib/logger/recordFactory.ts b/src/lib/logger/recordFactory.ts
index e1529ddd..b00be29d 100644
--- a/src/lib/logger/recordFactory.ts
+++ b/src/lib/logger/recordFactory.ts
@@ -6,9 +6,10 @@ import { CustomFieldsFormat, CustomFieldsTypeConversion, Output } from '../confi
import StacktraceUtils from '../helper/stacktraceUtils.js';
import { isValidObject } from '../middleware/utils.js';
import Cache from './cache.js';
-import Record from './record.js';
+import { Record, RecordType } from './record.js';
import Context from './context.js';
import SourceUtils from './sourceUtils.js';
+import { Level } from './level.js';
export default class RecordFactory {
@@ -34,19 +35,25 @@ export default class RecordFactory {
}
// init a new record and assign fields with output "msg-log"
- buildMsgRecord(registeredCustomFields: Array, loggerCustomFields: Map, levelName: string, args: Array, context?: Context): Record {
+ buildMsgRecord(registeredCustomFields: Array, loggerCustomFields: Map, level: Level, args: Array, context?: Context): Record {
const lastArg = args[args.length - 1];
let customFieldsFromArgs = new Map();
- const record = new Record(levelName)
-
-
+ const record = new Record(RecordType.Message, level)
+
+
if (typeof lastArg === "object") {
if (this.stacktraceUtils.isErrorWithStacktrace(lastArg)) {
record.metadata.stacktrace = this.stacktraceUtils.prepareStacktrace(lastArg.stack);
+ record.metadata.rawStacktrace = lastArg.stack
+ record.metadata.errorMessage = lastArg.message
+ record.metadata.errorName = lastArg.name
} else if (isValidObject(lastArg)) {
customFieldsFromArgs = new Map(Object.entries(lastArg));
if (this.stacktraceUtils.isErrorWithStacktrace(lastArg._error)) {
record.metadata.stacktrace = this.stacktraceUtils.prepareStacktrace(lastArg._error.stack);
+ record.metadata.rawStacktrace = lastArg._error.stack
+ record.metadata.errorMessage = lastArg._error.message
+ record.metadata.errorName = lastArg._error.name
customFieldsFromArgs.delete("_error");
}
} else if (lastArg instanceof Map) {
@@ -77,8 +84,8 @@ export default class RecordFactory {
}
// init a new record and assign fields with output "req-log"
- buildReqRecord(levelName: string, req: any, res: any, context: Context): Record {
- const record = new Record(levelName)
+ buildReqRecord(level: Level, req: any, res: any, context: Context): Record {
+ const record = new Record(RecordType.Request, level)
// assign static fields from cache
const cacheFields = this.config.getCacheReqFields();
@@ -88,7 +95,7 @@ export default class RecordFactory {
// assign dynamic fields
this.addDynamicFields(record, Output.ReqLog, req, res);
- // assign values request context
+ // assign values request context
this.addContext(record, context);
// assign custom fields
@@ -98,7 +105,7 @@ export default class RecordFactory {
return record;
}
- private addCustomFields(record: Record, registeredCustomFields: Array, loggerCustomFields: Map,
+ private addCustomFields(record: Record, registeredCustomFields: Array, loggerCustomFields: Map,
customFieldsFromArgs: Map = new Map()) {
const providedFields = new Map([...loggerCustomFields, ...customFieldsFromArgs]);
const customFieldsFormat = this.config.getConfig().customFieldsFormat!;
@@ -129,6 +136,8 @@ export default class RecordFactory {
indexedCustomFields[key] = value;
}
}
+
+ record.metadata.customFieldNames.push(key)
});
// Write custom fields in the correct order and correlates i to the place in registeredCustomFields
diff --git a/src/lib/logger/recordWriter.ts b/src/lib/logger/recordWriter.ts
deleted file mode 100644
index a7746ad7..00000000
--- a/src/lib/logger/recordWriter.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import os from 'os';
-
-import Record from './record.js';
-
-export default class RecordWriter {
-
- private static instance: RecordWriter;
- private customSinkFunction: ((level: string, payload: string) => any) | undefined;
-
- private constructor() {}
-
- static getInstance(): RecordWriter {
- if (!RecordWriter.instance) {
- RecordWriter.instance = new RecordWriter();
- }
-
- return RecordWriter.instance;
- }
-
- writeLog(record: Record): void {
- const level = record.metadata.level;
- if (this.customSinkFunction) {
- this.customSinkFunction(level, JSON.stringify(record.payload));
- } else {
- // default to stdout
- process.stdout.write(JSON.stringify(record.payload) + os.EOL);
- }
- }
-
- setSinkFunction(f: (level: string, payload: string) => any) {
- this.customSinkFunction = f;
- }
-}
diff --git a/src/lib/logger/rootLogger.ts b/src/lib/logger/rootLogger.ts
index cc21e170..3185e4d7 100644
--- a/src/lib/logger/rootLogger.ts
+++ b/src/lib/logger/rootLogger.ts
@@ -6,18 +6,23 @@ import EnvService from '../helper/envService.js';
import Middleware from '../middleware/middleware.js';
import RequestAccessor from '../middleware/requestAccessor.js';
import ResponseAccessor from '../middleware/responseAccessor.js';
+import { StdoutOutputPlugin } from '../plugins/stdoutOutput.js';
+import { OutputPlugin } from '../plugins/interfaces.js';
+import PluginProvider from '../helper/pluginProvider.js';
import createTransport from '../winston/winstonTransport.js';
import { Level } from './level.js';
import { Logger } from './logger.js';
-import RecordWriter from './recordWriter.js';
export default class RootLogger extends Logger {
private static instance: RootLogger;
+ private stdoutOutput: StdoutOutputPlugin;
private config = Config.getInstance();
private constructor() {
super()
this.loggingLevelThreshold = Level.Info
+ this.stdoutOutput = new StdoutOutputPlugin();
+ PluginProvider.getInstance().setOutputPlugins([this.stdoutOutput]);
}
static getInstance(): RootLogger {
@@ -56,8 +61,20 @@ export default class RootLogger extends Logger {
return this.config.setStartupMessageEnabled(enabled);
}
- setSinkFunction(func: (level: string, payload: string) => void) {
- RecordWriter.getInstance().setSinkFunction(func);
+ setSinkFunction(func: (level: string, payload: string) => any) {
+ this.stdoutOutput.setSinkFunction(func);
+ }
+
+ addOutputPlugin(outputPlugin: OutputPlugin) {
+ PluginProvider.getInstance().addOutputPlugin(outputPlugin);
+ }
+
+ setOutputPlugins(...outputPlugin: OutputPlugin[]) {
+ PluginProvider.getInstance().setOutputPlugins(outputPlugin);
+ }
+
+ getOutputPlugins(): OutputPlugin[] {
+ return PluginProvider.getInstance().getOutputPlugins();
}
enableTracing(input: string | string[]) {
diff --git a/src/lib/logger/sourceUtils.ts b/src/lib/logger/sourceUtils.ts
index 034e4293..9486bc90 100644
--- a/src/lib/logger/sourceUtils.ts
+++ b/src/lib/logger/sourceUtils.ts
@@ -5,7 +5,8 @@ import { ConfigField, Conversion, DetailName, Output, Source, SourceType } from
import EnvVarHelper from '../helper/envVarHelper.js';
import RequestAccessor from '../middleware/requestAccessor.js';
import ResponseAccessor from '../middleware/responseAccessor.js';
-import Record from './record.js';
+import { Record, RecordFieldValue } from './record.js';
+import { LevelUtils } from '../logger/level.js';
const NS_PER_MS = 1e6;
const REDACTED_PLACEHOLDER = "redacted";
@@ -31,11 +32,11 @@ export default class SourceUtils {
return SourceUtils.instance;
}
- getValue(field: ConfigField, record: Record, output: Output, req?: any, res?: any): string | number | boolean | undefined {
+ getValue(field: ConfigField, record: Record, output: Output, req?: any, res?: any): RecordFieldValue | undefined {
if (!field.source) return undefined
const sources = Array.isArray(field.source) ? field.source : [field.source]
- let value: string | number | boolean | undefined;
+ let value: RecordFieldValue | undefined;
let sourceIndex = 0;
@@ -81,8 +82,8 @@ export default class SourceUtils {
return value
}
- private getValueFromSource(source: Source, record: Record, output: Output, req?: any, res?: any): string | number | boolean | undefined {
- let value: string | number | boolean | undefined;
+ private getValueFromSource(source: Source, record: Record, output: Output, req?: any, res?: any): RecordFieldValue | undefined {
+ let value: RecordFieldValue | undefined;
switch (source.type) {
case SourceType.ReqHeader:
value = req ? this.requestAccessor.getHeaderField(req, source.fieldName!) : undefined;
@@ -122,8 +123,8 @@ export default class SourceUtils {
return value
}
- private getDetail(detailName: DetailName, record: Record, req?: any, res?: any): string | number | undefined {
- let value: string | number | undefined;
+ private getDetail(detailName: DetailName, record: Record, req?: any, res?: any): RecordFieldValue | undefined {
+ let value: RecordFieldValue | undefined;
switch (detailName as DetailName) {
case DetailName.RequestReceivedAt:
value = req ? new Date(req._receivedAt).toJSON() : undefined;
@@ -157,8 +158,17 @@ export default class SourceUtils {
case DetailName.Stacktrace:
value = record.metadata.stacktrace
break;
+ case DetailName.RawStacktrace:
+ value = record.metadata.rawStacktrace
+ break
+ case DetailName.ErrorMessage:
+ value = record.metadata.errorMessage
+ break;
+ case DetailName.ErrorName:
+ value = record.metadata.errorName
+ break;
case DetailName.Level:
- value = record.metadata.level
+ value = LevelUtils.getName(record.metadata.level)
break;
}
return value;
@@ -196,7 +206,7 @@ export default class SourceUtils {
return undefined;
}
- private parseIntValue(value: string | number | boolean): number {
+ private parseIntValue(value: RecordFieldValue): number {
switch (typeof value) {
case 'string':
return parseInt(value, 0)
@@ -205,9 +215,10 @@ export default class SourceUtils {
case 'boolean':
return value ? 1 : 0
}
+ return 0
}
- private parseFloatValue(value: string | number | boolean): number {
+ private parseFloatValue(value: RecordFieldValue): number {
switch (typeof value) {
case 'string':
return parseFloat(value)
@@ -216,9 +227,10 @@ export default class SourceUtils {
case 'boolean':
return value ? 1 : 0
}
+ return 0
}
- private parseBooleanValue(value: string | number | boolean): boolean {
+ private parseBooleanValue(value: RecordFieldValue): boolean {
return value === 'true' || value === 'TRUE' || value === 'True' || value === 1 || value === true
}
}
diff --git a/src/lib/middleware/middleware.ts b/src/lib/middleware/middleware.ts
index 6b84a54f..aa0e941b 100644
--- a/src/lib/middleware/middleware.ts
+++ b/src/lib/middleware/middleware.ts
@@ -1,13 +1,13 @@
import JWTService from '../helper/jwtService.js';
-import LevelUtils from '../helper/levelUtils.js';
+import { LevelUtils } from '../logger/level.js';
import { Logger } from '../logger/logger.js';
import RecordFactory from '../logger/recordFactory.js';
-import RecordWriter from '../logger/recordWriter.js';
import Context from '../logger/context.js';
import RootLogger from '../logger/rootLogger.js';
import RequestAccessor from './requestAccessor.js';
import Config from '../config/config.js';
import ResponseAccessor from './responseAccessor.js';
+import PluginRegistry from '../helper/pluginProvider.js';
export default class Middleware {
static logNetwork(req: any, res: any, next?: any) {
@@ -20,7 +20,7 @@ export default class Middleware {
// initialize Logger with parent to set registered fields
const networkLogger = new Logger(parentLogger, context);
const jwtService = JWTService.getInstance();
-
+
const dynLogLevelHeader = jwtService.getDynLogLevelHeaderName();
const token = RequestAccessor.getInstance().getHeaderField(req, dynLogLevelHeader);
if (token) {
@@ -39,8 +39,8 @@ export default class Middleware {
const level = LevelUtils.getLevel(levelName);
const threshold = LevelUtils.getLevel(req.logger.getLoggingLevel());
if (LevelUtils.isLevelEnabled(threshold, level)) {
- const record = RecordFactory.getInstance().buildReqRecord(levelName, req, res, context);
- RecordWriter.getInstance().writeLog(record);
+ const record = RecordFactory.getInstance().buildReqRecord(level, req, res, context);
+ PluginRegistry.getInstance().getOutputPlugins().forEach(output => { output.writeRecord(record) })
}
logSent = true;
}
diff --git a/src/lib/plugins/interfaces.ts b/src/lib/plugins/interfaces.ts
new file mode 100644
index 00000000..0b357f50
--- /dev/null
+++ b/src/lib/plugins/interfaces.ts
@@ -0,0 +1,7 @@
+import { Record } from "../logger/record"
+
+// Interface for output plugins that handle log records.
+export interface OutputPlugin {
+ // Writes a log record to the output plugin. Implementations should handle the record appropriately.
+ writeRecord(record: Record): void
+}
diff --git a/src/lib/plugins/otelOutput.ts b/src/lib/plugins/otelOutput.ts
new file mode 100644
index 00000000..fd4a42d9
--- /dev/null
+++ b/src/lib/plugins/otelOutput.ts
@@ -0,0 +1,138 @@
+import type { Context } from '@opentelemetry/api'
+import { logs as logsAPI, Logger, LoggerProvider, SeverityNumber, LogAttributes} from '@opentelemetry/api-logs'
+import { OutputPlugin } from './interfaces.js'
+import { Record, RecordType } from '../logger/record.js'
+import { Level } from '../logger/level.js'
+
+/** @experimental Requires `@opentelemetry/api-logs`, which is itself experimental and subject to breaking changes. */
+export type OpenTelemetryLogContextResolver = (record: Record) => Context | undefined
+
+/** @experimental Requires `@opentelemetry/api-logs`, which is itself experimental and subject to breaking changes. */
+export type OpenTelemetryLogContext = Context | OpenTelemetryLogContextResolver
+
+/**
+ * @experimental Used with {@link OpenTelemetryLogsOutputPlugin}, which is experimental.
+ */
+export enum FieldInclusionMode {
+ AllFields = "all",
+ CustomFieldsOnly = "custom-fields",
+ None = "none"
+}
+
+/**
+ * Output plugin that emits log records via the OpenTelemetry Logs API.
+ *
+ * @experimental This plugin depends on `@opentelemetry/api-logs`, which is marked as experimental
+ * by the OpenTelemetry project and may introduce breaking changes in future releases.
+ */
+export class OpenTelemetryLogsOutputPlugin implements OutputPlugin {
+ private logger: Logger
+ private includeFieldsAsAttributes: FieldInclusionMode
+ private context?: OpenTelemetryLogContext
+
+ /**
+ * Constructs a new OpenTelemetryLogsOutputPlugin.
+ * @param loggerProvider Optional OTel LoggerProvider to use. Defaults to the global provider.
+ * @param context Optional OTel context or context resolver function to attach to emitted log records.
+ */
+ public constructor(loggerProvider?: LoggerProvider, context?: OpenTelemetryLogContext) {
+ if (loggerProvider) {
+ this.logger = loggerProvider.getLogger('default')
+ } else {
+ this.logger = logsAPI.getLoggerProvider().getLogger("default")
+ }
+ this.includeFieldsAsAttributes = FieldInclusionMode.CustomFieldsOnly
+ this.context = context
+ }
+
+ /**
+ * Sets the mode for including fields as attributes in emitted OTel log records.
+ * @param includeFieldsAsAttributes The field inclusion mode to use.
+ */
+ public setIncludeFieldsAsAttributes(includeFieldsAsAttributes: FieldInclusionMode) {
+ this.includeFieldsAsAttributes = includeFieldsAsAttributes
+ }
+
+ /**
+ * Writes a log record to the output plugin. Request logs are ignored; only message logs are emitted.
+ * @param record The log record to write.
+ */
+ public writeRecord(record: Record): void {
+ if (record.metadata.type == RecordType.Request) {
+ return // ignore request logs
+ }
+
+ const attributes = {} as LogAttributes
+ this.populateExceptionAttributes(record, attributes)
+ this.populateAdditionalAttributes(record, attributes)
+
+ const severityNumber = this.mapLevelToSeverityNumber(record.metadata.level)
+ const context = this.resolveContext(record)
+
+ this.logger.emit({
+ severityNumber: severityNumber,
+ severityText: SeverityNumber[severityNumber],
+ body: record.metadata.message,
+ attributes: attributes,
+ ...(context && { context })
+ })
+ }
+
+ private resolveContext(record: Record): Context | undefined {
+ if (typeof this.context === 'function') {
+ return this.context(record)
+ }
+ return this.context
+ }
+
+ private mapLevelToSeverityNumber(level: Level): SeverityNumber {
+ switch (level) {
+ case Level.Error:
+ return SeverityNumber.ERROR
+ case Level.Warn:
+ return SeverityNumber.WARN
+ case Level.Info:
+ return SeverityNumber.INFO
+ case Level.Verbose:
+ return SeverityNumber.DEBUG2
+ case Level.Debug:
+ return SeverityNumber.DEBUG
+ case Level.Silly:
+ return SeverityNumber.TRACE
+ }
+ return SeverityNumber.UNSPECIFIED
+ }
+
+ private populateExceptionAttributes(record: Record, attributes: LogAttributes) {
+ if (record.metadata.errorName) {
+ attributes["exception.type"] = record.metadata.errorName
+ }
+ if (record.metadata.errorMessage) {
+ attributes["exception.message"] = record.metadata.errorMessage
+ }
+ if (record.metadata.rawStacktrace) {
+ attributes["exception.stacktrace"] = record.metadata.rawStacktrace
+ }
+ }
+
+ private populateAdditionalAttributes(record: Record, attributes: LogAttributes) {
+ switch(this.includeFieldsAsAttributes) {
+ case FieldInclusionMode.AllFields:
+ for (const key in record.payload) {
+ attributes[key] = record.payload[key]
+ }
+ break;
+ case FieldInclusionMode.CustomFieldsOnly:
+ for (const key of record.metadata.customFieldNames) {
+ if (record.payload[key] !== undefined) {
+ attributes[key] = record.payload[key]
+ }
+ }
+ break;
+ case FieldInclusionMode.None:
+ default:
+ return;
+ }
+ }
+}
+
diff --git a/src/lib/plugins/stdoutOutput.ts b/src/lib/plugins/stdoutOutput.ts
new file mode 100644
index 00000000..a642a63f
--- /dev/null
+++ b/src/lib/plugins/stdoutOutput.ts
@@ -0,0 +1,23 @@
+import os from 'os';
+import { Record } from "../logger/record.js";
+import { OutputPlugin } from "./interfaces.js";
+import { LevelUtils } from '../logger/level.js';
+
+export class StdoutOutputPlugin implements OutputPlugin {
+
+ private sinkFunction: ((level: string, payload: string) => any) | undefined;
+
+ writeRecord(record: Record): void {
+ const jsonStr: string = JSON.stringify(record.payload);
+ if (this.sinkFunction) {
+ const level: string = LevelUtils.getName(record.metadata.level);
+ this.sinkFunction(level, jsonStr);
+ } else {
+ process.stdout.write(jsonStr + os.EOL);
+ }
+ }
+
+ setSinkFunction(callback: (level: string, payload: string) => any | undefined) {
+ this.sinkFunction = callback;
+ }
+}
diff --git a/src/test/unit-test/helper.test.js b/src/test/unit-test/helper.test.js
index 98c473b3..f3ad3d1e 100644
--- a/src/test/unit-test/helper.test.js
+++ b/src/test/unit-test/helper.test.js
@@ -1,8 +1,7 @@
const { BUILD_CJS_LIB } = require('../paths');
const expect = require('chai').expect;
const JSONHelper = require(`${BUILD_CJS_LIB}/helper/jsonHelper.js`).default;
-const LevelUtils = require(`${BUILD_CJS_LIB}/helper/levelUtils.js`).default;
-const { Level } = require(`${BUILD_CJS_LIB}/logger/level.js`);
+const { Level, LevelUtils } = require(`${BUILD_CJS_LIB}/logger/level.js`);
const EnvVarHelper = require(`${BUILD_CJS_LIB}/helper/envVarHelper.js`).default;
const StacktraceUtils = require(`${BUILD_CJS_LIB}/helper/stacktraceUtils.js`).default;
diff --git a/src/test/unit-test/otel-output.test.js b/src/test/unit-test/otel-output.test.js
new file mode 100644
index 00000000..23e4f134
--- /dev/null
+++ b/src/test/unit-test/otel-output.test.js
@@ -0,0 +1,118 @@
+const { context, SpanKind, trace, TraceFlags } = require('@opentelemetry/api');
+const { SeverityNumber } = require('@opentelemetry/api-logs');
+const { AsyncLocalStorageContextManager } = require('@opentelemetry/context-async-hooks');
+const {
+ InMemoryLogRecordExporter,
+ LoggerProvider,
+ SimpleLogRecordProcessor
+} = require('@opentelemetry/sdk-logs');
+const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base');
+const { expect } = require('chai');
+
+const { BUILD_CJS_LIB } = require('../paths');
+const { Level } = require(`${BUILD_CJS_LIB}/logger/level.js`);
+const { Record, RecordType } = require(`${BUILD_CJS_LIB}/logger/record.js`);
+const {
+ FieldInclusionMode,
+ OpenTelemetryLogsOutputPlugin
+} = require(`${BUILD_CJS_LIB}/plugins/otelOutput.js`);
+
+describe('OpenTelemetryLogsOutputPlugin', function () {
+ let contextManager;
+ let exporter;
+ let loggerProvider;
+ let tracerProvider;
+
+ beforeEach(function () {
+ contextManager = new AsyncLocalStorageContextManager().enable();
+ context.setGlobalContextManager(contextManager);
+ exporter = new InMemoryLogRecordExporter();
+ loggerProvider = new LoggerProvider({
+ processors: [new SimpleLogRecordProcessor({ exporter })]
+ });
+ });
+
+ afterEach(async function () {
+ await loggerProvider.shutdown();
+ if (tracerProvider) {
+ await tracerProvider.shutdown();
+ }
+ context.disable();
+ contextManager.disable();
+ });
+
+ it('uses the active context when no context is configured', function () {
+ const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider);
+ const nonRecordingSpanContext = {
+ traceId: '11111111111111111111111111111111',
+ spanId: '2222222222222222',
+ traceFlags: TraceFlags.NONE
+ };
+ const activeContext = trace.setSpanContext(context.active(), nonRecordingSpanContext);
+
+ context.with(activeContext, () => plugin.writeRecord(createRecord()));
+
+ const [logRecord] = exporter.getFinishedLogRecords();
+ expect(logRecord.spanContext).to.deep.equal(nonRecordingSpanContext);
+ expect(trace.getSpan(activeContext).isRecording()).to.equal(false);
+ });
+
+ it('uses an explicitly provided context', function () {
+ const explicitSpanContext = {
+ traceId: '33333333333333333333333333333333',
+ spanId: '4444444444444444',
+ traceFlags: TraceFlags.SAMPLED
+ };
+ const explicitContext = trace.setSpanContext(context.active(), explicitSpanContext);
+ const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider, explicitContext);
+
+ plugin.writeRecord(createRecord());
+
+ const [logRecord] = exporter.getFinishedLogRecords();
+ expect(logRecord.spanContext).to.deep.equal(explicitSpanContext);
+ });
+
+ it('uses a resolved recording server context while preserving log fields', function () {
+ tracerProvider = new BasicTracerProvider();
+ const serverSpan = tracerProvider.getTracer('test').startSpan('request', {
+ kind: SpanKind.SERVER
+ });
+ const serverContext = trace.setSpan(context.active(), serverSpan);
+ const droppedSpanContext = {
+ traceId: '55555555555555555555555555555555',
+ spanId: '6666666666666666',
+ traceFlags: TraceFlags.NONE
+ };
+ const droppedContext = trace.setSpanContext(context.active(), droppedSpanContext);
+ const record = createRecord();
+ let resolvedRecord;
+ const plugin = new OpenTelemetryLogsOutputPlugin(loggerProvider, currentRecord => {
+ resolvedRecord = currentRecord;
+ return serverContext;
+ });
+ plugin.setIncludeFieldsAsAttributes(FieldInclusionMode.CustomFieldsOnly);
+
+ context.with(droppedContext, () => plugin.writeRecord(record));
+
+ const [logRecord] = exporter.getFinishedLogRecords();
+ expect(resolvedRecord).to.equal(record);
+ expect(serverSpan.isRecording()).to.equal(true);
+ expect(logRecord.spanContext).to.deep.equal(serverSpan.spanContext());
+ expect(logRecord.spanContext.spanId).not.to.equal(droppedSpanContext.spanId);
+ expect(logRecord.severityNumber).to.equal(SeverityNumber.WARN);
+ expect(logRecord.severityText).to.equal('WARN');
+ expect(logRecord.body).to.equal('context test');
+ expect(logRecord.attributes).to.deep.equal({ custom: 'value' });
+
+ serverSpan.end();
+ });
+
+ function createRecord() {
+ const record = new Record(RecordType.Message, Level.Warn);
+ record.metadata.message = 'context test';
+ record.metadata.customFieldNames.push('custom');
+ record.payload.custom = 'value';
+ record.payload.internal = 'not included';
+ return record;
+ }
+});