diff --git a/package-lock.json b/package-lock.json index 1800503d83..54d0deda1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "packages/cli/", "packages/cache/", "packages/feature-toggle/", + "packages/observability/", "packages/file-utils/", "packages/custom-sf-changelog/", "services/*", @@ -444,6 +445,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1004.0.tgz", "integrity": "sha512-m0zNfpsona9jQdX1cHtHArOiuvSGZPsgp/KRZS2YjJhKah96G2UN3UNGZQ6aVjXIQjCY6UanCJo0uW9Xf2U41w==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", @@ -1193,6 +1195,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2090,6 +2093,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2112,6 +2116,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2689,7 +2694,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "optional": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -2703,7 +2707,6 @@ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", "license": "Apache-2.0", - "optional": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -4957,7 +4960,6 @@ "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", "license": "MIT", - "optional": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" @@ -5720,6 +5722,7 @@ "resolved": "https://registry.npmjs.org/@loopback/boot/-/boot-8.0.10.tgz", "integrity": "sha512-r7wE+I0VY6mlBDSmt+C8th+nxITVH7xlJsvOMwhH2plZdbZ1wmj4ZpP6naeMPP+3tlP8kMGtb6JUUmxeRzIt3Q==", "license": "MIT", + "peer": true, "dependencies": { "@loopback/model-api-builder": "^7.0.10", "@loopback/repository": "^8.0.9", @@ -6337,6 +6340,7 @@ "resolved": "https://registry.npmjs.org/@loopback/context/-/context-8.0.9.tgz", "integrity": "sha512-D36orXqHsBs7gOJ2ssGfFhYgrjBrU7EdUVZ0fFdvLtmabVZwxGqx0Qt2PCMDhteoJ8eHT/zUS7ayOHZiefz2Ng==", "license": "MIT", + "peer": true, "dependencies": { "@loopback/metadata": "^8.0.9", "@types/debug": "^4.1.12", @@ -6389,6 +6393,7 @@ "resolved": "https://registry.npmjs.org/@loopback/core/-/core-7.0.9.tgz", "integrity": "sha512-VdK2AmAfUI0rBnupMJWupiDv8QDV7rc9CmpaEwL3bsbJ4EbKijETBrmnRN848qBU77ARhKzh+2wWWBpX5Tpusw==", "license": "MIT", + "peer": true, "dependencies": { "@loopback/context": "^8.0.9", "debug": "^4.4.3", @@ -6536,6 +6541,7 @@ "resolved": "https://registry.npmjs.org/@loopback/openapi-v3/-/openapi-v3-11.0.10.tgz", "integrity": "sha512-FzMgJ5gf5RYob+DXcVG14kCWRklMR7UoO/f15y5DCe8EwIbcauKbE5VPUXNmCwcWstWltX6wIZIPLf2ZOvrSsQ==", "license": "MIT", + "peer": true, "dependencies": { "@loopback/repository-json-schema": "^9.0.10", "debug": "^4.4.3", @@ -6557,6 +6563,7 @@ "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-8.0.9.tgz", "integrity": "sha512-0pFW/pbK2P45zLyuzU9TVn7l9QgiGgqHLPz0zsbpAUsSXw652stPNKqkm/VzlFBrORyQBrfE2495cj1elYz5Cw==", "license": "MIT", + "peer": true, "dependencies": { "@loopback/filter": "^6.0.9", "@types/debug": "^4.1.12", @@ -6744,6 +6751,7 @@ "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-15.0.10.tgz", "integrity": "sha512-aHYZUBhmp/+54xDACE2tS4wBuptkhF8qdVDcyFW9mN5GotaVlxvjt+PCJPRcIpdzNzrXvP9elPr7rywdixlUTw==", "license": "MIT", + "peer": true, "dependencies": { "@loopback/express": "^8.0.9", "@loopback/http-server": "^7.0.9", @@ -6789,6 +6797,7 @@ "resolved": "https://registry.npmjs.org/@loopback/rest-explorer/-/rest-explorer-8.0.10.tgz", "integrity": "sha512-RVhgzpPaL0JVGUMFRLPqDyKKaH4R83ZqpW2+9YuLIkAEYqE+IrH/BddYPXl5dBdoIzxzRNuCQSYEpXmIUO7F1A==", "license": "MIT", + "peer": true, "dependencies": { "ejs": "^3.1.10", "swagger-ui-dist": "5.31.0", @@ -6849,6 +6858,7 @@ "resolved": "https://registry.npmjs.org/@loopback/service-proxy/-/service-proxy-8.0.9.tgz", "integrity": "sha512-g0LsoAt6oHfR+bE0DJvhxkNtCBl7fpERxMNw3fmZMXCqHO8PK2uIdCIa+2RNttPCfWJDNi0wnqZbTe1msL/TCQ==", "license": "MIT", + "peer": true, "dependencies": { "loopback-datasource-juggler": "^6.0.2", "tslib": "^2.8.1" @@ -7769,6 +7779,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7831,6 +7842,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.16.tgz", "integrity": "sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -7935,6 +7947,7 @@ "integrity": "sha512-tXWXyCiqWthelJjrE0KLFjf0O98VEt+WPVx5CrqCf+059kIxJ8y1Vw7Cy7N4fwQafWNrmFL2AfN87DDMbVAY0w==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -8005,6 +8018,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.16.tgz", "integrity": "sha512-IOegr5+ZfUiMKgk+garsSU4MOkPRhm46e6w8Bp1GcO4vCdl9Piz6FlWAzKVfa/U3Hn/DdzSVJOW3TWcQQFdBDw==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -10305,6 +10319,7 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -10504,10 +10519,23 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", + "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@opentelemetry/context-async-hooks": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", @@ -10554,6 +10582,381 @@ "@opentelemetry/api": "^1.0.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.57.2.tgz", + "integrity": "sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/otlp-exporter-base": "0.57.2", + "@opentelemetry/otlp-grpc-exporter-base": "0.57.2", + "@opentelemetry/otlp-transformer": "0.57.2", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.57.2.tgz", + "integrity": "sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/otlp-exporter-base": "0.57.2", + "@opentelemetry/otlp-transformer": "0.57.2", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", + "integrity": "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.62.0.tgz", + "integrity": "sha512-Tvx+vgAZKEQxU3Rx+xWLiR0mLxHwmk69/8ya04+VsV9WYh8w6Lhx5hm5yAMvo1wy0KqWgFKBLwSeo3sHCwdOww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "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/instrumentation-express/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.214.0.tgz", + "integrity": "sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/instrumentation": "0.214.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "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/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz", + "integrity": "sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.60.0.tgz", + "integrity": "sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.60.0.tgz", + "integrity": "sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@opentelemetry/sql-common": "^0.41.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz", + "integrity": "sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.41.2", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "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/instrumentation-pg/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz", + "integrity": "sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.214.0", + "@opentelemetry/redis-common": "^0.38.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.57.2.tgz", + "integrity": "sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/otlp-transformer": "0.57.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.57.2.tgz", + "integrity": "sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/otlp-exporter-base": "0.57.2", + "@opentelemetry/otlp-transformer": "0.57.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.57.2.tgz", + "integrity": "sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.57.2", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/sdk-logs": "0.57.2", + "@opentelemetry/sdk-metrics": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/plugin-dns": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/@opentelemetry/plugin-dns/-/plugin-dns-0.15.0.tgz", @@ -10793,6 +11196,16 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", + "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, "node_modules/@opentelemetry/resources": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", @@ -10809,6 +11222,39 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.57.2.tgz", + "integrity": "sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.57.2", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz", + "integrity": "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", @@ -10855,6 +11301,48 @@ "node": ">=14" } }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", + "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@opentelemetry/sql-common/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "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/sql-common/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@otplib/core": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", @@ -11070,36 +11558,31 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -11109,36 +11592,31 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@scarf/scarf": { "version": "1.4.0", @@ -12480,6 +12958,10 @@ "resolved": "sandbox/oauth-example", "link": true }, + "node_modules/@sourceloop/observability": { + "resolved": "packages/observability", + "link": true + }, "node_modules/@sourceloop/oidc-basic-example": { "resolved": "sandbox/oidc-basic-example", "link": true @@ -12994,6 +13476,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -13028,6 +13511,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -13401,6 +13885,16 @@ "@types/node": "*" } }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "25.3.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", @@ -13622,6 +14116,16 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/pg-pool": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz", + "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -14075,6 +14579,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -14628,6 +15133,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -14657,6 +15163,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-import-phases": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", @@ -14770,6 +15286,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15379,6 +15896,7 @@ "deprecated": "The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -15435,6 +15953,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -16146,6 +16665,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -16987,6 +17507,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -19212,6 +19733,7 @@ "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -19724,6 +20246,7 @@ "resolved": "https://registry.npmjs.org/db-migrate-pg/-/db-migrate-pg-1.5.2.tgz", "integrity": "sha512-agbT9biJi43E7wld9JgnpMKadYgIobMlRXdtRO8JLRWHI1Jc7mObl9pM7iv4AQ4UTLDgjtkqUqtXlfeWtRuRbA==", "license": "MIT", + "peer": true, "dependencies": { "bluebird": "^3.1.1", "db-migrate-base": "^2.3.0", @@ -20913,6 +21436,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -20969,6 +21493,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -21495,6 +22020,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -22724,6 +23250,13 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "dev": true, + "license": "MIT" + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -23897,6 +24430,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -24814,6 +25348,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -25405,6 +25940,29 @@ "node": ">=4" } }, + "node_modules/import-in-the-middle": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", + "integrity": "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/import-in-the-middle/node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -25529,6 +26087,7 @@ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", "license": "MIT", + "peer": true, "dependencies": { "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", @@ -26536,7 +27095,6 @@ "integrity": "sha512-P7nLXRRlo7Sqinty6lNa7+4o9jBUYGpqtejqCOZKfgXlRoxY/QArflcB86YO500Ahj4pDJEG34JjMRbQgePLnQ==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "archy": "^1.0.0", "cross-spawn": "^7.0.3", @@ -26555,7 +27113,6 @@ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", @@ -26574,7 +27131,6 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -26585,7 +27141,6 @@ "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "aggregate-error": "^3.0.0" }, @@ -26599,7 +27154,6 @@ "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" @@ -26776,6 +27330,7 @@ "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^28.1.3", "@jest/types": "^28.1.3", @@ -29232,6 +29787,7 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -30760,7 +31316,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "devOptional": true, "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -31675,6 +32230,7 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz", "integrity": "sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 14" }, @@ -31762,6 +32318,7 @@ "integrity": "sha512-R5uJDWfKYeBtS48eO2ckE2Ro3/cJ8ROk3u6u2JQ3/bEQUdxbZfgIs9yEHFfdgumv/w3dEilJVr03idn0vptVgg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@loopback/context": "^7.0.14", "debug": "^4.4.0", @@ -31807,6 +32364,7 @@ "integrity": "sha512-jFQIjtqE0Uis/goXfztiNIKGQ1ITIqsIR8xe8q8j6PJbNoetVaA4A3wz4LaBhuFpyjCgQM8iJdwGKfNTJNzn4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@loopback/filter": "^5.0.13", "@types/debug": "^4.1.12", @@ -33107,6 +33665,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "license": "MIT", + "peer": true, "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -33169,6 +33728,7 @@ "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", @@ -33624,6 +34184,13 @@ "node": ">=0.10.0" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "dev": true, + "license": "MIT" + }, "node_modules/module-not-found-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", @@ -35121,6 +35688,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -35388,7 +35956,6 @@ "integrity": "sha512-G5UyHinFkB1BxqGTrmZdB6uIYH0+v7ZnVssuflUDi+J+RhKWyAhRT1RCehBSI6jLFLuUUgFDyLt49mUtdO1XeQ==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", @@ -35431,7 +35998,6 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -35442,7 +36008,6 @@ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -35455,7 +36020,6 @@ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -35466,7 +36030,6 @@ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -35481,7 +36044,6 @@ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", @@ -35500,7 +36062,6 @@ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -35518,7 +36079,6 @@ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -35532,7 +36092,6 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -35543,7 +36102,6 @@ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -35560,7 +36118,6 @@ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -35574,7 +36131,6 @@ "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "aggregate-error": "^3.0.0" }, @@ -35588,7 +36144,6 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -35599,7 +36154,6 @@ "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" @@ -35619,8 +36173,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/nyc/node_modules/test-exclude": { "version": "8.0.0", @@ -35628,7 +36181,6 @@ "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^13.0.6", @@ -35643,8 +36195,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/nyc/node_modules/yargs": { "version": "15.4.1", @@ -35652,7 +36203,6 @@ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -35676,7 +36226,6 @@ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -37517,6 +38066,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -37998,6 +38548,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -38136,6 +38687,7 @@ "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz", "integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tdigest": "^0.1.1" }, @@ -38271,7 +38823,6 @@ "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, "license": "BSD-3-Clause", - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -39955,7 +40506,8 @@ "version": "0.1.14", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -40111,6 +40663,20 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -40561,6 +41127,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -40747,6 +41314,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -41642,7 +42210,6 @@ "integrity": "sha512-z+s5vv4KzFPJVddGab0xX2n7kQPGMdNUX5l9T8EJqsXdKTWpcxmAqWHpsgHEXoC1taGBCc7b79bi62M5kdbrxQ==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "foreground-child": "^2.0.0", @@ -41662,7 +42229,6 @@ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^3.0.2" @@ -41677,7 +42243,6 @@ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", @@ -41695,8 +42260,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/spawn-wrap/node_modules/minipass": { "version": "7.1.3", @@ -41704,7 +42268,6 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -41715,7 +42278,6 @@ "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" @@ -41735,8 +42297,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/spawn-wrap/node_modules/which": { "version": "2.0.2", @@ -41744,7 +42305,6 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -44769,7 +45329,6 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -44783,7 +45342,6 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -44793,8 +45351,7 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tslint/node_modules/brace-expansion": { "version": "1.1.12", @@ -44802,7 +45359,6 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -44814,7 +45370,6 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -44830,7 +45385,6 @@ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-name": "1.1.3" } @@ -44840,16 +45394,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tslint/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tslint/node_modules/diff": { "version": "4.0.4", @@ -44857,7 +45409,6 @@ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.3.1" } @@ -44868,7 +45419,6 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.0" } @@ -44880,7 +45430,6 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -44902,7 +45451,6 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -44913,7 +45461,6 @@ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -44928,7 +45475,6 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -44942,7 +45488,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -44956,7 +45501,6 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -44969,8 +45513,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsscmp": { "version": "1.0.6", @@ -44987,6 +45530,7 @@ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^1.8.1" }, @@ -45344,6 +45888,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -46172,7 +46717,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -46191,7 +46735,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -46206,7 +46749,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -46217,7 +46759,6 @@ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -46227,8 +46768,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -46236,7 +46776,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -50508,6 +51047,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -51552,6 +52092,7 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -51957,6 +52498,104 @@ "dev": true, "license": "MIT" }, + "packages/observability": { + "name": "@sourceloop/observability", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@loopback/core": "^7.0.3", + "@loopback/rest": "^15.0.4", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.2", + "@opentelemetry/exporter-trace-otlp-http": "^0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/sdk-trace-node": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.28.0", + "tslib": "^2.6.2" + }, + "devDependencies": { + "@loopback/eslint-config": "16.0.1", + "@opentelemetry/instrumentation-express": "^0.62.0", + "@opentelemetry/instrumentation-http": "^0.214.0", + "@opentelemetry/instrumentation-kafkajs": "^0.23.0", + "@opentelemetry/instrumentation-mysql": "^0.60.0", + "@opentelemetry/instrumentation-mysql2": "^0.60.0", + "@opentelemetry/instrumentation-pg": "^0.66.0", + "@opentelemetry/instrumentation-redis": "^0.62.0", + "@types/mocha": "^10.0.10", + "@types/node": "20.19.37", + "eslint": "8.57.1", + "mocha": "11.7.5", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@opentelemetry/instrumentation-express": "^0.62.0", + "@opentelemetry/instrumentation-http": "^0.214.0", + "@opentelemetry/instrumentation-kafkajs": "^0.23.0", + "@opentelemetry/instrumentation-mysql": "^0.60.0", + "@opentelemetry/instrumentation-mysql2": "^0.60.0", + "@opentelemetry/instrumentation-pg": "^0.66.0", + "@opentelemetry/instrumentation-redis": "^0.62.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/instrumentation-express": { + "optional": true + }, + "@opentelemetry/instrumentation-http": { + "optional": true + }, + "@opentelemetry/instrumentation-kafkajs": { + "optional": true + }, + "@opentelemetry/instrumentation-mysql": { + "optional": true + }, + "@opentelemetry/instrumentation-mysql2": { + "optional": true + }, + "@opentelemetry/instrumentation-pg": { + "optional": true + }, + "@opentelemetry/instrumentation-redis": { + "optional": true + } + } + }, + "packages/observability/node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "packages/observability/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "packages/observability/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "sandbox/audit-ms-example": { "name": "@sourceloop/audit-ms-example", "version": "0.12.6", diff --git a/package.json b/package.json index 48de8fd75f..27fc619735 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "packages/cli/", "packages/cache/", "packages/feature-toggle/", + "packages/observability/", "packages/file-utils/", "packages/custom-sf-changelog/", "services/*", diff --git a/packages/observability/.eslintignore b/packages/observability/.eslintignore new file mode 100644 index 0000000000..d5983396a5 --- /dev/null +++ b/packages/observability/.eslintignore @@ -0,0 +1,2 @@ +test-enterprise-features.js +test-vendor-agnostic.js \ No newline at end of file diff --git a/packages/observability/.eslintrc.js b/packages/observability/.eslintrc.js new file mode 100644 index 0000000000..551aa3967a --- /dev/null +++ b/packages/observability/.eslintrc.js @@ -0,0 +1,26 @@ +module.exports = { + extends: '@loopback/eslint-config', + rules: { + 'no-extra-boolean-cast': 'off', + '@typescript-eslint/interface-name-prefix': 'off', + 'no-prototype-builtins': 'off', + // Explicitly enable the no-console rule as an error + 'no-console': 'error', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + // Allows promises to be used in void return contexts (e.g., event handlers) + // Set to true if you want stricter checks. + checksVoidReturn: false, + }, + ], + }, + parserOptions: { + // Point to your tsconfig.json for typed linting + project: './tsconfig.json', + // Define the root directory for the tsconfig.json path + tsconfigRootDir: __dirname, + }, + // Add '.eslintrc.js' to prevent ESLint from linting itself with typed rules + ignorePatterns: ['dist', '.eslintrc.js', 'templates'], +}; diff --git a/packages/observability/.gitignore b/packages/observability/.gitignore new file mode 100644 index 0000000000..c60866cad4 --- /dev/null +++ b/packages/observability/.gitignore @@ -0,0 +1,69 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# Transpiled JavaScript files from Typescript +/dist + +# Cache used by TypeScript's incremental build +*.tsbuildinfo +mochawesome-report + + +#Private key pem file +*.pem diff --git a/packages/observability/.prettierignore b/packages/observability/.prettierignore new file mode 100644 index 0000000000..a3e8b771a1 --- /dev/null +++ b/packages/observability/.prettierignore @@ -0,0 +1,4 @@ +dist +*.json +node_modules/ +mochawesome-report/ diff --git a/packages/observability/.prettierrc b/packages/observability/.prettierrc new file mode 100644 index 0000000000..2e48c76c31 --- /dev/null +++ b/packages/observability/.prettierrc @@ -0,0 +1,7 @@ +{ + "bracketSpacing": false, + "singleQuote": true, + "printWidth": 80, + "trailingComma": "all", + "arrowParens": "avoid" +} diff --git a/packages/observability/README.md b/packages/observability/README.md new file mode 100644 index 0000000000..fb451a814d --- /dev/null +++ b/packages/observability/README.md @@ -0,0 +1,249 @@ +# @sourceloop/observability + +OpenTelemetry-first observability bootstrap and optional LoopBack integration for Sourceloop services. + +## Design + +This package follows a hybrid model: + +1. **Bootstrap is the required startup path** + Use bootstrap before your application imports go too far, so tracing can start early. +2. **The LoopBack component is optional** + Use `ObservabilityComponent` only when you want DI-bound config, framework bindings, and profile extension-point integration. + +That means this package should be thought of as: + +- `bootstrap.ts`: core runtime startup +- `component.ts`: LoopBack adapter + +## What It Provides + +- Early env-led or override-led bootstrap for observability startup +- DI-bound config via `ObservabilityBindings.config` +- Built-in profiles for `default`, `newrelic`, `signoz`, and `datadog` +- LoopBack extension-point support for custom profiles +- Generic helpers for spans, exception recording, and propagation headers + +## Startup Model + +### Required: bootstrap + +Call bootstrap in your service entrypoint before application construction. + +```ts +import {bootstrapObservabilityFromEnv} from '@sourceloop/observability'; + +bootstrapObservabilityFromEnv(); +``` + +You can also pass explicit startup config for tests or advanced bootstrap flows: + +```ts +import {bootstrapObservability} from '@sourceloop/observability'; + +bootstrapObservability({ + enabled: true, + profile: 'default', + serviceName: 'audit-service', +}); +``` + +Startup-only instrumentation toggles are also configured here: + +```ts +bootstrapObservability({ + enabled: true, + profile: 'default', + serviceName: 'audit-service', + instrumentations: { + http: true, + express: true, + pg: false, + mysql: true, + redis: false, + kafka: false, + }, +}); +``` + +### Optional: LoopBack component + +Use the component only if you want framework-native bindings and DI config. + +```ts +import { + ObservabilityBindings, + ObservabilityComponent, +} from '@sourceloop/observability'; + +this.bind(ObservabilityBindings.config).to({ + serviceVersion: '1.0.0', + environment: process.env.NODE_ENV, + resourceAttributes: { + 'deployment.color': 'blue', + }, +}); + +this.component(ObservabilityComponent); +``` + +Important: + +- Bootstrap controls startup-critical config. +- DI config is meant for component-level enrichment, not for replacing early bootstrap. +- Instrumentation toggles and custom instrumentations are bootstrap concerns, not DI concerns. +- If you skip bootstrap and rely only on DI config, early instrumentation will not be enabled. + +## Config Resolution + +The package resolves config in two stages. + +### Bootstrap config + +Resolved by `resolveBootstrapConfig()`. + +Used for startup-critical fields such as: + +- `enabled` +- `profile` +- `serviceName` +- exporter endpoint/protocol/headers +- sampler +- instrumentation toggles + +Precedence: + +1. bootstrap overrides +2. environment variables +3. defaults + +Instrumentation toggles can be set either in `instrumentations` during bootstrap or through environment variables: + +- `OBSERVABILITY_INSTRUMENT_HTTP` +- `OBSERVABILITY_INSTRUMENT_EXPRESS` +- `OBSERVABILITY_INSTRUMENT_PG` +- `OBSERVABILITY_INSTRUMENT_MYSQL` +- `OBSERVABILITY_INSTRUMENT_REDIS` +- `OBSERVABILITY_INSTRUMENT_KAFKA` + +Set each one to `true` or `false` to enable or disable that instrumentation. `express` requires `http` to remain enabled. + +The same toggles can be set programmatically through `bootstrapObservability({...})` using the `instrumentations` object shown above. That is the preferred non-env path. + +You can also attach your own instrumentation instances during bootstrap: + +```ts +import { + bootstrapObservability, + ObservabilityInstrumentation, +} from '@sourceloop/observability'; + +const customInstrumentation: ObservabilityInstrumentation = { + enable() { + // initialize your instrumentation + }, + disable() { + // clean up your instrumentation + }, + setTracerProvider(tracerProvider) { + // optional: keep the tracer provider if your instrumentation needs it + }, +}; + +bootstrapObservability({ + enabled: true, + profile: 'default', + serviceName: 'audit-service', + customInstrumentations: [customInstrumentation], +}); +``` + +Use `customInstrumentations` when you need a package-specific or in-house instrumentation that is not built into `@sourceloop/observability`. + +### Component config + +Resolved by `resolveComponentConfig()`. + +Used for LoopBack/application enrichment such as: + +- `serviceVersion` +- `environment` +- extra `resourceAttributes` + +Precedence: + +1. DI config +2. bootstrap-resolved config + +## Profiles + +Built-in profiles: + +- `default` +- `newrelic` +- `signoz` +- `datadog` + +`default` means plain OTLP behavior with no vendor-specific profile layered on top. + +These profiles are implemented by profile classes inside the package. Custom profiles can be added using the LoopBack extension point exposed by the package. + +## Dependencies + +OTLP exporters are bundled as package dependencies because OTLP is the built-in transport path for all current profiles. + +There are currently no vendor-native SDK dependencies in the package. The built-in `newrelic`, `signoz`, and `datadog` profiles are OTLP-oriented presets, not native agent integrations. + +OpenTelemetry instrumentation libraries are optional peer dependencies so consumers can cherry-pick only the instrumentations they want to use: + +- `@opentelemetry/instrumentation-http` +- `@opentelemetry/instrumentation-express` +- `@opentelemetry/instrumentation-pg` +- `@opentelemetry/instrumentation-redis` +- `@opentelemetry/instrumentation-kafkajs` +- `@opentelemetry/instrumentation-mysql` +- `@opentelemetry/instrumentation-mysql2` + +If an instrumentation is enabled in config but its package is not installed, bootstrap will fail fast with a clear startup error. + +## Public API + +Bootstrap/runtime: + +- `bootstrapObservability()` + Starts observability using explicit startup overrides. Use this in tests or when startup config needs to be supplied programmatically before the app is constructed. +- `bootstrapObservabilityFromEnv()` + Starts observability from environment-driven configuration only. This is the standard entrypoint for service bootstrapping. +- `shutdownObservability()` + Shuts down the active tracer runtime and clears internal singleton state. Use this for graceful shutdown or test cleanup. +- `isObservabilityEnabled()` + Returns whether observability is currently active in the bootstrapped runtime. + +LoopBack: + +- `ObservabilityBindings` + Provides LoopBack binding keys for DI-bound observability config, runtime, and resolved config. +- `ObservabilityComponent` + Registers the optional LoopBack integration layer, including config bindings and built-in profile bindings. + +Tracing helpers: + +- `withSpan()` + Runs an async function inside a new active span and closes the span automatically when the function completes. +- `addSpanAttributes()` + Adds attributes to the current active span when one exists. +- `recordException()` + Records an error on the current span and marks the span status as failed. +- `createPropagationHeaders()` + Injects the current trace context into a plain header carrier for outbound calls. +- `getTraceContext()` + Returns the current trace identifiers and propagation headers for correlation or logging. + +Config helpers: + +- `resolveBootstrapConfig()` + Resolves startup-critical config from explicit overrides, environment variables, and defaults. +- `resolveComponentConfig()` + Resolves the LoopBack/component-level config by layering DI enrichment on top of the bootstrapped config. +- `validateObservabilityConfig()` + Validates that the resolved observability config is coherent before runtime startup. diff --git a/packages/observability/package.json b/packages/observability/package.json new file mode 100644 index 0000000000..572e265446 --- /dev/null +++ b/packages/observability/package.json @@ -0,0 +1,104 @@ +{ + "name": "@sourceloop/observability", + "version": "0.0.1", + "description": "OpenTelemetry-first observability bootstrap and LoopBack integration for Sourceloop services", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "engines": { + "node": ">=20" + }, + "files": [ + "README.md", + "dist", + "src", + "!*/__tests__" + ], + "scripts": { + "build": "lb-tsc", + "build:watch": "lb-tsc --watch", + "lint": "npm run eslint && npm run prettier:check", + "lint:fix": "npm run eslint:fix && npm run prettier:fix", + "prettier:cli": "prettier \"**/*.ts\" \"**/*.js\"", + "prettier:check": "npm run prettier:cli -- -l", + "prettier:fix": "npm run prettier:cli -- --write", + "eslint": "eslint --report-unused-disable-directives .", + "eslint:fix": "npm run eslint -- --fix", + "pretest": "npm run rebuild", + "test": "lb-mocha --allow-console-logs \"dist/__tests__\"", + "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js", + "clean": "lb-clean dist *.tsbuildinfo .eslintcache", + "rebuild": "npm run clean && npm run build", + "prune": "npm prune --production", + "coverage": "nyc npm run test" + }, + "repository": { + "type": "git", + "url": "https://github.com/sourcefuse/loopback4-microservice-catalog.git", + "directory": "packages/observability" + }, + "author": "Sourcefuse", + "license": "MIT", + "dependencies": { + "@loopback/core": "^7.0.3", + "@loopback/rest": "^15.0.4", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.2", + "@opentelemetry/exporter-trace-otlp-http": "^0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/sdk-trace-node": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.28.0", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@opentelemetry/instrumentation-express": "^0.62.0", + "@opentelemetry/instrumentation-http": "^0.214.0", + "@opentelemetry/instrumentation-kafkajs": "^0.23.0", + "@opentelemetry/instrumentation-mysql": "^0.60.0", + "@opentelemetry/instrumentation-mysql2": "^0.60.0", + "@opentelemetry/instrumentation-pg": "^0.66.0", + "@opentelemetry/instrumentation-redis": "^0.62.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/instrumentation-express": { + "optional": true + }, + "@opentelemetry/instrumentation-http": { + "optional": true + }, + "@opentelemetry/instrumentation-kafkajs": { + "optional": true + }, + "@opentelemetry/instrumentation-mysql": { + "optional": true + }, + "@opentelemetry/instrumentation-mysql2": { + "optional": true + }, + "@opentelemetry/instrumentation-pg": { + "optional": true + }, + "@opentelemetry/instrumentation-redis": { + "optional": true + } + }, + "devDependencies": { + "@loopback/eslint-config": "16.0.1", + "@opentelemetry/instrumentation-express": "^0.62.0", + "@opentelemetry/instrumentation-http": "^0.214.0", + "@opentelemetry/instrumentation-kafkajs": "^0.23.0", + "@opentelemetry/instrumentation-mysql": "^0.60.0", + "@opentelemetry/instrumentation-mysql2": "^0.60.0", + "@opentelemetry/instrumentation-pg": "^0.66.0", + "@opentelemetry/instrumentation-redis": "^0.62.0", + "@types/mocha": "^10.0.10", + "@types/node": "20.19.37", + "eslint": "8.57.1", + "mocha": "11.7.5", + "typescript": "5.9.3" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + } +} diff --git a/packages/observability/src/__tests__/acceptance/README.md b/packages/observability/src/__tests__/acceptance/README.md new file mode 100644 index 0000000000..5bb1778297 --- /dev/null +++ b/packages/observability/src/__tests__/acceptance/README.md @@ -0,0 +1 @@ +# Acceptance tests diff --git a/packages/observability/src/__tests__/integration/README.md b/packages/observability/src/__tests__/integration/README.md new file mode 100644 index 0000000000..0ca287e976 --- /dev/null +++ b/packages/observability/src/__tests__/integration/README.md @@ -0,0 +1 @@ +# Integration tests diff --git a/packages/observability/src/__tests__/integration/runtime.integration.ts b/packages/observability/src/__tests__/integration/runtime.integration.ts new file mode 100644 index 0000000000..a47ab54090 --- /dev/null +++ b/packages/observability/src/__tests__/integration/runtime.integration.ts @@ -0,0 +1,87 @@ +import {ok, strictEqual} from 'assert'; +import {InMemorySpanExporter} from '@opentelemetry/sdk-trace-base'; +import {DEFAULT_OBSERVABILITY_CONFIG} from '../../config/defaults'; +import { + addSpanAttributes, + createPropagationHeaders, + getTraceContext, + recordException, + withSpan, +} from '../../index'; +import {OtlpObservabilityProfile} from '../../profiles'; +import {ResolvedObservabilityConfig} from '../../types'; + +function buildConfig( + overrides?: Partial, +): ResolvedObservabilityConfig { + return { + ...DEFAULT_OBSERVABILITY_CONFIG, + enabled: true, + profile: 'default', + serviceName: 'runtime-test-service', + ...overrides, + instrumentations: { + ...DEFAULT_OBSERVABILITY_CONFIG.instrumentations, + ...overrides?.instrumentations, + }, + resourceAttributes: { + ...DEFAULT_OBSERVABILITY_CONFIG.resourceAttributes, + ...overrides?.resourceAttributes, + }, + otlpHeaders: { + ...DEFAULT_OBSERVABILITY_CONFIG.otlpHeaders, + ...overrides?.otlpHeaders, + }, + customInstrumentations: + overrides?.customInstrumentations ?? + DEFAULT_OBSERVABILITY_CONFIG.customInstrumentations, + }; +} + +describe('observability runtime', () => { + let profile: OtlpObservabilityProfile | undefined; + + afterEach(async () => { + await profile?.shutdown(); + profile = undefined; + }); + + it('initializes a tracer runtime and exposes active trace correlation', async () => { + profile = new OtlpObservabilityProfile(); + + const initResult = profile.initialize( + buildConfig({ + instrumentations: { + http: true, + express: true, + pg: false, + mysql: false, + redis: false, + kafka: false, + }, + }), + { + createExporter: () => new InMemorySpanExporter(), + }, + ); + + ok(initResult.tracerProvider); + + await withSpan('runtime-test-span', async () => { + addSpanAttributes({ + 'test.case': 'runtime', + }); + + recordException(new Error('runtime-test-error')); + + const traceContext = getTraceContext(); + ok(traceContext.traceId); + ok(traceContext.spanId); + + const headers = createPropagationHeaders(); + ok(headers.traceparent); + }); + + strictEqual(initResult.exporterName, 'loadOtlpExporter'); + }); +}); diff --git a/packages/observability/src/__tests__/unit/README.md b/packages/observability/src/__tests__/unit/README.md new file mode 100644 index 0000000000..a0291f0699 --- /dev/null +++ b/packages/observability/src/__tests__/unit/README.md @@ -0,0 +1 @@ +# Unit tests diff --git a/packages/observability/src/__tests__/unit/instrumentations.unit.ts b/packages/observability/src/__tests__/unit/instrumentations.unit.ts new file mode 100644 index 0000000000..61f0842613 --- /dev/null +++ b/packages/observability/src/__tests__/unit/instrumentations.unit.ts @@ -0,0 +1,114 @@ +import {deepStrictEqual, strictEqual} from 'assert'; +import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node'; +import {DEFAULT_OBSERVABILITY_CONFIG} from '../../config/defaults'; +import { + createAutoInstrumentations, + InstrumentationLoader, +} from '../../profiles/instrumentations'; +import {ResolvedObservabilityConfig} from '../../types'; + +function buildConfig( + overrides?: Partial, +): ResolvedObservabilityConfig { + return { + ...DEFAULT_OBSERVABILITY_CONFIG, + enabled: true, + profile: 'default', + serviceName: 'instrumentation-test-service', + ...overrides, + instrumentations: { + ...DEFAULT_OBSERVABILITY_CONFIG.instrumentations, + ...overrides?.instrumentations, + }, + resourceAttributes: { + ...DEFAULT_OBSERVABILITY_CONFIG.resourceAttributes, + ...overrides?.resourceAttributes, + }, + otlpHeaders: { + ...DEFAULT_OBSERVABILITY_CONFIG.otlpHeaders, + ...overrides?.otlpHeaders, + }, + customInstrumentations: + overrides?.customInstrumentations ?? + DEFAULT_OBSERVABILITY_CONFIG.customInstrumentations, + }; +} + +describe('createAutoInstrumentations', () => { + it('loads and enables only the configured instrumentations', () => { + const tracerProvider = new NodeTracerProvider(); + const loaded: string[] = []; + const enabled: string[] = []; + + const loader: InstrumentationLoader = name => { + loaded.push(name); + return [ + { + disable() {}, + enable() { + enabled.push(name); + }, + setTracerProvider(provider) { + strictEqual(provider, tracerProvider); + }, + }, + ]; + }; + + const instrumentations = createAutoInstrumentations( + buildConfig({ + instrumentations: { + http: true, + express: false, + pg: true, + mysql: true, + redis: false, + kafka: true, + }, + }), + tracerProvider, + loader, + ); + + strictEqual(instrumentations.length, 4); + deepStrictEqual(loaded, ['http', 'pg', 'mysql', 'kafka']); + deepStrictEqual(enabled, ['http', 'pg', 'mysql', 'kafka']); + }); + + it('enables custom instrumentations supplied through bootstrap config', () => { + const tracerProvider = new NodeTracerProvider(); + let enabled = false; + let disabled = false; + + const instrumentations = createAutoInstrumentations( + buildConfig({ + instrumentations: { + http: false, + express: false, + pg: false, + mysql: false, + redis: false, + kafka: false, + }, + customInstrumentations: [ + { + disable() { + disabled = true; + }, + enable() { + enabled = true; + }, + setTracerProvider(provider) { + strictEqual(provider, tracerProvider); + }, + }, + ], + }), + tracerProvider, + ); + + strictEqual(instrumentations.length, 1); + strictEqual(enabled, true); + strictEqual(disabled, false); + }); +}); diff --git a/packages/observability/src/__tests__/unit/profile-defaults.unit.ts b/packages/observability/src/__tests__/unit/profile-defaults.unit.ts new file mode 100644 index 0000000000..a52fe02ea6 --- /dev/null +++ b/packages/observability/src/__tests__/unit/profile-defaults.unit.ts @@ -0,0 +1,79 @@ +import {strictEqual} from 'assert'; +import {DEFAULT_OBSERVABILITY_CONFIG} from '../../config/defaults'; +import {DatadogObservabilityProfile} from '../../profiles/datadog.profile'; +import {NewRelicObservabilityProfile} from '../../profiles/newrelic.profile'; +import {SignozObservabilityProfile} from '../../profiles/signoz.profile'; +import {ResolvedObservabilityConfig} from '../../types'; + +function buildConfig( + overrides?: Partial, +): ResolvedObservabilityConfig { + return { + ...DEFAULT_OBSERVABILITY_CONFIG, + enabled: true, + profile: 'default', + serviceName: 'profile-test-service', + ...overrides, + instrumentations: { + ...DEFAULT_OBSERVABILITY_CONFIG.instrumentations, + ...overrides?.instrumentations, + }, + resourceAttributes: { + ...DEFAULT_OBSERVABILITY_CONFIG.resourceAttributes, + ...overrides?.resourceAttributes, + }, + otlpHeaders: { + ...DEFAULT_OBSERVABILITY_CONFIG.otlpHeaders, + ...overrides?.otlpHeaders, + }, + customInstrumentations: + overrides?.customInstrumentations ?? + DEFAULT_OBSERVABILITY_CONFIG.customInstrumentations, + }; +} + +describe('built-in profile defaults', () => { + afterEach(() => { + delete process.env.NEW_RELIC_LICENSE_KEY; + delete process.env.DD_API_KEY; + }); + + it('applies New Relic OTLP defaults', () => { + process.env.NEW_RELIC_LICENSE_KEY = 'new-relic-license'; + + const config = new NewRelicObservabilityProfile().applyDefaults( + buildConfig({ + profile: 'newrelic', + }), + ); + + strictEqual(config.otlpEndpoint, 'https://otlp.nr-data.net:4318/v1/traces'); + strictEqual(config.otlpHeaders['api-key'], 'new-relic-license'); + strictEqual(config.resourceAttributes['vendor.apm'], 'newrelic'); + }); + + it('applies SigNoz collector defaults', () => { + const config = new SignozObservabilityProfile().applyDefaults( + buildConfig({ + profile: 'signoz', + }), + ); + + strictEqual(config.otlpEndpoint, 'http://localhost:4318/v1/traces'); + strictEqual(config.resourceAttributes['vendor.apm'], 'signoz'); + }); + + it('applies Datadog OTLP defaults', () => { + process.env.DD_API_KEY = 'datadog-api-key'; + + const config = new DatadogObservabilityProfile().applyDefaults( + buildConfig({ + profile: 'datadog', + }), + ); + + strictEqual(config.otlpEndpoint, 'http://localhost:4318/v1/traces'); + strictEqual(config.otlpHeaders['dd-api-key'], 'datadog-api-key'); + strictEqual(config.resourceAttributes['vendor.apm'], 'datadog'); + }); +}); diff --git a/packages/observability/src/__tests__/unit/resolve-config.unit.ts b/packages/observability/src/__tests__/unit/resolve-config.unit.ts new file mode 100644 index 0000000000..51223436a3 --- /dev/null +++ b/packages/observability/src/__tests__/unit/resolve-config.unit.ts @@ -0,0 +1,62 @@ +import {deepStrictEqual, strictEqual} from 'assert'; +import { + resolveBootstrapConfig, + resolveComponentConfig, +} from '../../config/resolve-config'; +import {clearRuntimeState, updateRuntimeState} from '../../runtime'; + +describe('config resolution', () => { + afterEach(() => { + delete process.env.OBSERVABILITY_ENABLED; + delete process.env.OBSERVABILITY_PROFILE; + delete process.env.OBSERVABILITY_PROVIDER; + delete process.env.MS_NAME; + delete process.env.OTEL_SERVICE_NAME; + delete process.env.OTEL_RESOURCE_ATTRIBUTES; + clearRuntimeState(); + }); + + it('prefers bootstrap overrides for startup-critical fields', () => { + process.env.OBSERVABILITY_ENABLED = 'false'; + process.env.OBSERVABILITY_PROFILE = 'none'; + + const config = resolveBootstrapConfig({ + enabled: true, + profile: 'default', + serviceName: 'bootstrap-service', + }); + + strictEqual(config.enabled, true); + strictEqual(config.profile, 'default'); + strictEqual(config.serviceName, 'bootstrap-service'); + }); + + it('uses DI config only for component-level enrichment', () => { + process.env.MS_NAME = 'bootstrap-service'; + process.env.OTEL_RESOURCE_ATTRIBUTES = 'cloud.region=us-east-1'; + + const bootstrapConfig = resolveBootstrapConfig({ + enabled: true, + profile: 'default', + serviceVersion: '1.0.0', + }); + updateRuntimeState({config: bootstrapConfig}); + + const componentConfig = resolveComponentConfig({ + profile: 'signoz', + serviceVersion: '2.0.0', + environment: 'staging', + resourceAttributes: { + 'deployment.color': 'blue', + }, + }); + + strictEqual(componentConfig.profile, 'default'); + strictEqual(componentConfig.serviceVersion, '2.0.0'); + strictEqual(componentConfig.environment, 'staging'); + deepStrictEqual(componentConfig.resourceAttributes, { + 'cloud.region': 'us-east-1', + 'deployment.color': 'blue', + }); + }); +}); diff --git a/packages/observability/src/__tests__/unit/validate-config.unit.ts b/packages/observability/src/__tests__/unit/validate-config.unit.ts new file mode 100644 index 0000000000..529763b5f8 --- /dev/null +++ b/packages/observability/src/__tests__/unit/validate-config.unit.ts @@ -0,0 +1,52 @@ +import {throws} from 'assert'; +import {DEFAULT_OBSERVABILITY_CONFIG} from '../../config/defaults'; +import {validateObservabilityConfig} from '../../config/validate-config'; +import {ResolvedObservabilityConfig} from '../../types'; + +function buildConfig( + overrides?: Partial, +): ResolvedObservabilityConfig { + return { + ...DEFAULT_OBSERVABILITY_CONFIG, + enabled: true, + profile: 'default', + serviceName: 'validate-config-service', + ...overrides, + instrumentations: { + ...DEFAULT_OBSERVABILITY_CONFIG.instrumentations, + ...overrides?.instrumentations, + }, + resourceAttributes: { + ...DEFAULT_OBSERVABILITY_CONFIG.resourceAttributes, + ...overrides?.resourceAttributes, + }, + otlpHeaders: { + ...DEFAULT_OBSERVABILITY_CONFIG.otlpHeaders, + ...overrides?.otlpHeaders, + }, + customInstrumentations: + overrides?.customInstrumentations ?? + DEFAULT_OBSERVABILITY_CONFIG.customInstrumentations, + }; +} + +describe('validateObservabilityConfig', () => { + it('rejects express instrumentation when http is disabled', () => { + throws( + () => + validateObservabilityConfig( + buildConfig({ + instrumentations: { + http: false, + express: true, + pg: false, + mysql: false, + redis: false, + kafka: false, + }, + }), + ), + /express instrumentation requires http instrumentation/i, + ); + }); +}); diff --git a/packages/observability/src/bootstrap.ts b/packages/observability/src/bootstrap.ts new file mode 100644 index 0000000000..7f688febe6 --- /dev/null +++ b/packages/observability/src/bootstrap.ts @@ -0,0 +1,124 @@ +import { + ObservabilityConfig, + ObservabilityProfile, + ObservabilityRuntime, +} from './types'; +import {resolveBootstrapConfig} from './config/resolve-config'; +import {validateObservabilityConfig} from './config/validate-config'; +import { + DatadogObservabilityProfile, + NewRelicObservabilityProfile, + OtlpObservabilityProfile, + SignozObservabilityProfile, + createOtlpExporter, +} from './profiles'; +import { + clearRuntimeState, + getRuntimeState, + updateRuntimeState, +} from './runtime'; + +const BUILTIN_PROFILES: ObservabilityProfile[] = [ + new OtlpObservabilityProfile(), + new NewRelicObservabilityProfile(), + new SignozObservabilityProfile(), + new DatadogObservabilityProfile(), +]; + +function resolveProfile(profileName: string): ObservabilityProfile { + const profile = BUILTIN_PROFILES.find(candidate => + candidate.supports(profileName), + ); + + if (!profile) { + throw new Error( + `No observability profile registered for "${profileName}".`, + ); + } + + return profile; +} + +function buildDisabledRuntime(): ObservabilityRuntime { + const config = getRuntimeState().config; + return { + enabled: false, + profile: config?.profile ?? 'none', + config: + config ?? + resolveBootstrapConfig({ + enabled: false, + profile: 'none', + }), + async shutdown() { + clearRuntimeState(); + }, + }; +} + +export function bootstrapObservability( + overrides?: Partial, +): ObservabilityRuntime { + const existingRuntime = getRuntimeState().runtime; + if (existingRuntime) { + return existingRuntime; + } + + updateRuntimeState({bootstrapOverrides: overrides}); + const resolvedConfig = resolveBootstrapConfig(overrides); + updateRuntimeState({config: resolvedConfig}); + + if (!resolvedConfig.enabled || resolvedConfig.profile === 'none') { + const runtime = buildDisabledRuntime(); + updateRuntimeState({runtime}); + return runtime; + } + + const profile = resolveProfile(resolvedConfig.profile); + const profileConfig = profile.applyDefaults(resolvedConfig); + validateObservabilityConfig(profileConfig); + + const initResult = profile.initialize(profileConfig, { + createExporter: createOtlpExporter, + }); + + updateRuntimeState({ + profile, + config: profileConfig, + }); + + const runtime: ObservabilityRuntime = { + enabled: true, + profile: profile.name, + config: profileConfig, + tracerProvider: initResult.tracerProvider, + async shutdown() { + await profile.shutdown?.(); + clearRuntimeState(); + }, + }; + + updateRuntimeState({runtime}); + return runtime; +} + +export function bootstrapObservabilityFromEnv(): ObservabilityRuntime { + return bootstrapObservability(); +} + +export async function shutdownObservability(): Promise { + const runtime = getRuntimeState().runtime; + if (!runtime) { + return; + } + + await runtime.shutdown(); +} + +export function isObservabilityEnabled(): boolean { + return getRuntimeState().runtime?.enabled ?? false; +} + +export function getBootstrapProfile(): ObservabilityProfile | undefined { + return getRuntimeState().profile; +} diff --git a/packages/observability/src/component.ts b/packages/observability/src/component.ts new file mode 100644 index 0000000000..5a8cb9f721 --- /dev/null +++ b/packages/observability/src/component.ts @@ -0,0 +1,56 @@ +import { + Binding, + Component, + Constructor, + CoreBindings, + createBindingFromClass, + inject, +} from '@loopback/core'; +import {RestApplication} from '@loopback/rest'; +import {bootstrapObservability} from './bootstrap'; +import {resolveComponentConfig} from './config/resolve-config'; +import {ObservabilityBindings} from './keys'; +import {getRuntimeState} from './runtime'; +import {ObservabilityConfig, ObservabilityProfile} from './types'; +import { + DatadogObservabilityProfile, + NewRelicObservabilityProfile, + ObservabilityProfileRegistry, + SignozObservabilityProfile, + OtlpObservabilityProfile, + asObservabilityProfile, +} from './profiles'; + +const BUILTIN_PROFILE_CLASSES: Constructor[] = [ + OtlpObservabilityProfile, + NewRelicObservabilityProfile, + SignozObservabilityProfile, + DatadogObservabilityProfile, +]; + +export class ObservabilityComponent implements Component { + bindings: Binding[] = []; + + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + private readonly application: RestApplication, + @inject(ObservabilityBindings.config, {optional: true}) + private readonly config?: ObservabilityConfig, + ) { + const runtime = getRuntimeState().runtime ?? bootstrapObservability(); + const resolvedConfig = resolveComponentConfig(this.config); + + this.application.bind(ObservabilityBindings.runtime).to(runtime); + this.application + .bind(ObservabilityBindings.resolvedConfig) + .to(resolvedConfig); + + this.bindings.push(createBindingFromClass(ObservabilityProfileRegistry)); + + for (const profileClass of BUILTIN_PROFILE_CLASSES) { + const binding = createBindingFromClass(profileClass); + asObservabilityProfile(binding); + this.bindings.push(binding); + } + } +} diff --git a/packages/observability/src/config/defaults.ts b/packages/observability/src/config/defaults.ts new file mode 100644 index 0000000000..9e65173c12 --- /dev/null +++ b/packages/observability/src/config/defaults.ts @@ -0,0 +1,23 @@ +import {ResolvedObservabilityConfig} from '../types'; + +export const DEFAULT_INSTRUMENTATIONS = { + http: true, + express: true, + pg: false, + mysql: false, + redis: false, + kafka: false, +}; + +export const DEFAULT_OBSERVABILITY_CONFIG: ResolvedObservabilityConfig = { + enabled: false, + profile: 'none', + serviceName: 'application', + exporterProtocol: 'http/protobuf', + otlpHeaders: {}, + sampler: 'always_on', + samplerArg: 1, + instrumentations: DEFAULT_INSTRUMENTATIONS, + customInstrumentations: [], + resourceAttributes: {}, +}; diff --git a/packages/observability/src/config/resolve-config.ts b/packages/observability/src/config/resolve-config.ts new file mode 100644 index 0000000000..80f2642e45 --- /dev/null +++ b/packages/observability/src/config/resolve-config.ts @@ -0,0 +1,211 @@ +import { + ExporterProtocol, + InstrumentationToggles, + ObservabilityConfig, + ResolvedObservabilityConfig, + SamplerName, +} from '../types'; +import { + DEFAULT_INSTRUMENTATIONS, + DEFAULT_OBSERVABILITY_CONFIG, +} from './defaults'; +import {getRuntimeState} from '../runtime'; + +function parseBoolean(value: string | undefined): boolean | undefined { + if (value == null) { + return undefined; + } + + switch (value.trim().toLowerCase()) { + case '1': + case 'true': + case 'yes': + case 'on': + return true; + case '0': + case 'false': + case 'no': + case 'off': + return false; + default: + return undefined; + } +} + +function parseNumber(value: string | undefined): number | undefined { + if (value == null || value === '') { + return undefined; + } + + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function parseHeaders(value: string | undefined): Record { + if (!value) { + return {}; + } + + return value + .split(',') + .map(part => part.trim()) + .filter(Boolean) + .reduce>((headers, header) => { + const separatorIndex = header.indexOf('='); + if (separatorIndex < 1) { + return headers; + } + + const key = header.slice(0, separatorIndex).trim(); + const headerValue = header.slice(separatorIndex + 1).trim(); + if (key && headerValue) { + headers[key] = headerValue; + } + return headers; + }, {}); +} + +function parseResourceAttributes( + value: string | undefined, +): Record { + return parseHeaders(value); +} + +const INSTRUMENTATION_ENV_VARS: Record = { + http: 'OBSERVABILITY_INSTRUMENT_HTTP', + express: 'OBSERVABILITY_INSTRUMENT_EXPRESS', + pg: 'OBSERVABILITY_INSTRUMENT_PG', + mysql: 'OBSERVABILITY_INSTRUMENT_MYSQL', + redis: 'OBSERVABILITY_INSTRUMENT_REDIS', + kafka: 'OBSERVABILITY_INSTRUMENT_KAFKA', +}; + +function resolveInstrumentationToggle( + key: keyof InstrumentationToggles, + overrides?: Partial, +): boolean { + return ( + overrides?.[key] ?? + parseBoolean(process.env[INSTRUMENTATION_ENV_VARS[key]]) ?? + DEFAULT_INSTRUMENTATIONS[key] + ); +} + +function startupInstrumentationConfig( + overrides?: Partial, +): InstrumentationToggles { + const keys = Object.keys( + DEFAULT_INSTRUMENTATIONS, + ) as (keyof InstrumentationToggles)[]; + + return keys.reduce( + (result, key) => { + result[key] = resolveInstrumentationToggle(key, overrides); + return result; + }, + {} as InstrumentationToggles, + ); +} + +function resolveServiceInfo( + overrides: Partial | undefined, + env: NodeJS.ProcessEnv, + enabled: boolean, +) { + const profile = + overrides?.profile ?? + env.OBSERVABILITY_PROFILE ?? + env.OBSERVABILITY_PROVIDER ?? + (enabled ? 'default' : DEFAULT_OBSERVABILITY_CONFIG.profile); + + const serviceName = + overrides?.serviceName ?? + env.OTEL_SERVICE_NAME ?? + env.MS_NAME ?? + DEFAULT_OBSERVABILITY_CONFIG.serviceName; + + return { + profile, + serviceName, + serviceVersion: overrides?.serviceVersion ?? env.SERVICE_VERSION, + environment: overrides?.environment ?? env.NODE_ENV, + }; +} + +function resolveExporterConfig( + overrides: Partial | undefined, + env: NodeJS.ProcessEnv, +) { + const exporterProtocol = (overrides?.exporterProtocol ?? + (env.OTEL_EXPORTER_OTLP_PROTOCOL as ExporterProtocol | undefined) ?? + DEFAULT_OBSERVABILITY_CONFIG.exporterProtocol) as ExporterProtocol; + + const sampler = (overrides?.sampler ?? + (env.OTEL_TRACES_SAMPLER as SamplerName | undefined) ?? + DEFAULT_OBSERVABILITY_CONFIG.sampler) as SamplerName; + + const samplerArg = + overrides?.samplerArg ?? + parseNumber(env.OTEL_TRACES_SAMPLER_ARG) ?? + DEFAULT_OBSERVABILITY_CONFIG.samplerArg; + + const otlpEndpoint = + overrides?.otlpEndpoint ?? + env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? + env.OTEL_EXPORTER_OTLP_ENDPOINT; + + const otlpHeaders = { + ...parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS), + ...overrides?.otlpHeaders, + }; + + return {exporterProtocol, sampler, samplerArg, otlpEndpoint, otlpHeaders}; +} + +export function resolveBootstrapConfig( + bootstrapOverrides?: Partial, +): ResolvedObservabilityConfig { + const env = process.env; + + const enabled = + bootstrapOverrides?.enabled ?? + parseBoolean(env.OBSERVABILITY_ENABLED) ?? + DEFAULT_OBSERVABILITY_CONFIG.enabled; + + const serviceInfo = resolveServiceInfo(bootstrapOverrides, env, enabled); + const exporterConfig = resolveExporterConfig(bootstrapOverrides, env); + + const resourceAttributes = { + ...parseResourceAttributes(env.OTEL_RESOURCE_ATTRIBUTES), + ...bootstrapOverrides?.resourceAttributes, + }; + + return { + enabled, + ...serviceInfo, + ...exporterConfig, + instrumentations: startupInstrumentationConfig( + bootstrapOverrides?.instrumentations, + ), + customInstrumentations: bootstrapOverrides?.customInstrumentations ?? [], + resourceAttributes, + }; +} + +export function resolveComponentConfig( + diConfig?: ObservabilityConfig, +): ResolvedObservabilityConfig { + const resolvedBootstrapConfig = + getRuntimeState().config ?? resolveBootstrapConfig(); + + return { + ...resolvedBootstrapConfig, + serviceVersion: + diConfig?.serviceVersion ?? resolvedBootstrapConfig.serviceVersion, + environment: diConfig?.environment ?? resolvedBootstrapConfig.environment, + resourceAttributes: { + ...resolvedBootstrapConfig.resourceAttributes, + ...diConfig?.resourceAttributes, + }, + }; +} diff --git a/packages/observability/src/config/validate-config.ts b/packages/observability/src/config/validate-config.ts new file mode 100644 index 0000000000..866415da10 --- /dev/null +++ b/packages/observability/src/config/validate-config.ts @@ -0,0 +1,83 @@ +import {ResolvedObservabilityConfig} from '../types'; +import { + getEnabledInstrumentations, + InstrumentationModuleRequirement, + InstrumentationName, + INSTRUMENTATION_MODULE_REQUIREMENTS, +} from '../profiles/instrumentations'; + +function assertDependencyInstalled(moduleName: string, message: string): void { + try { + require.resolve(moduleName); + } catch { + throw new Error(message); + } +} + +function isDependencyInstalled(moduleName: string): boolean { + try { + require.resolve(moduleName); + return true; + } catch { + return false; + } +} + +export function validateObservabilityConfig( + config: ResolvedObservabilityConfig, +): void { + if (!config.serviceName.trim()) { + throw new Error('Observability serviceName must be a non-empty string.'); + } + + if ( + config.sampler === 'traceidratio' && + (config.samplerArg < 0 || config.samplerArg > 1) + ) { + throw new Error( + 'Observability samplerArg must be between 0 and 1 for traceidratio.', + ); + } + + if (!config.enabled || config.profile === 'none') { + return; + } + + if (config.instrumentations.express && !config.instrumentations.http) { + throw new Error( + 'Observability express instrumentation requires http instrumentation to be enabled.', + ); + } + + for (const instrumentation of getEnabledInstrumentations(config)) { + validateInstrumentationRequirements( + instrumentation, + INSTRUMENTATION_MODULE_REQUIREMENTS[instrumentation], + ); + } +} + +function validateInstrumentationRequirements( + instrumentation: InstrumentationName, + requirements: InstrumentationModuleRequirement[], +): void { + for (const requirement of requirements) { + if (requirement.type === 'all') { + for (const moduleName of requirement.modules) { + assertDependencyInstalled( + moduleName, + `Install the optional peer dependency "${moduleName}" or disable the "${instrumentation}" instrumentation.`, + ); + } + continue; + } + + if (!requirement.modules.some(isDependencyInstalled)) { + throw new Error( + `Install one of the optional peer dependencies "${requirement.modules.join( + '" or "', + )}" or disable the "${instrumentation}" instrumentation.`, + ); + } + } +} diff --git a/packages/observability/src/correlation.ts b/packages/observability/src/correlation.ts new file mode 100644 index 0000000000..b6b4a2d8e6 --- /dev/null +++ b/packages/observability/src/correlation.ts @@ -0,0 +1,26 @@ +import {context, propagation, trace} from '@opentelemetry/api'; + +export function createPropagationHeaders( + carrier: Record = {}, +): Record { + propagation.inject(context.active(), carrier); + return carrier; +} + +export function getTraceContext(): { + traceId?: string; + spanId?: string; + traceparent?: string; + tracestate?: string; +} { + const activeSpan = trace.getSpan(context.active()); + const spanContext = activeSpan?.spanContext(); + const carrier = createPropagationHeaders(); + + return { + traceId: spanContext?.traceId, + spanId: spanContext?.spanId, + traceparent: carrier.traceparent, + tracestate: carrier.tracestate, + }; +} diff --git a/packages/observability/src/errors.ts b/packages/observability/src/errors.ts new file mode 100644 index 0000000000..9d0b8a692a --- /dev/null +++ b/packages/observability/src/errors.ts @@ -0,0 +1,22 @@ +import {context, SpanStatusCode, trace} from '@opentelemetry/api'; +import {AttributeValue} from './types'; + +export function recordException( + error: Error, + attributes?: Record, +): void { + const span = trace.getSpan(context.active()); + if (!span) { + return; + } + + if (attributes) { + span.setAttributes(attributes); + } + + span.recordException(error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); +} diff --git a/packages/observability/src/index.ts b/packages/observability/src/index.ts new file mode 100644 index 0000000000..6bed6071e7 --- /dev/null +++ b/packages/observability/src/index.ts @@ -0,0 +1,20 @@ +export { + bootstrapObservability, + bootstrapObservabilityFromEnv, + getBootstrapProfile, + isObservabilityEnabled, + shutdownObservability, +} from './bootstrap'; +export {ObservabilityComponent} from './component'; +export {ObservabilityBindings} from './keys'; +export * from './profiles'; +export {getRuntimeState} from './runtime'; +export * from './types'; +export { + resolveBootstrapConfig, + resolveComponentConfig, +} from './config/resolve-config'; +export {validateObservabilityConfig} from './config/validate-config'; +export {withSpan, addSpanAttributes} from './tracing'; +export {recordException} from './errors'; +export {createPropagationHeaders, getTraceContext} from './correlation'; diff --git a/packages/observability/src/keys.ts b/packages/observability/src/keys.ts new file mode 100644 index 0000000000..dd20a2e426 --- /dev/null +++ b/packages/observability/src/keys.ts @@ -0,0 +1,20 @@ +import {BindingKey} from '@loopback/core'; +import { + ObservabilityConfig, + ObservabilityRuntime, + ResolvedObservabilityConfig, +} from './types'; + +export namespace ObservabilityBindings { + export const config = BindingKey.create( + 'sf.packages.observability.config', + ); + + export const runtime = BindingKey.create( + 'sf.packages.observability.runtime', + ); + + export const resolvedConfig = BindingKey.create( + 'sf.packages.observability.resolvedConfig', + ); +} diff --git a/packages/observability/src/profiles/datadog.profile.ts b/packages/observability/src/profiles/datadog.profile.ts new file mode 100644 index 0000000000..2d0f93bc03 --- /dev/null +++ b/packages/observability/src/profiles/datadog.profile.ts @@ -0,0 +1,50 @@ +import { + ObservabilityProfileName, + ObservabilityProfile, + ProfileBootstrapContext, + ProfileInitResult, + ResolvedObservabilityConfig, +} from '../types'; +import {BaseOtlpObservabilityProfile} from './otlp.profile'; + +export class DatadogObservabilityProfile + extends BaseOtlpObservabilityProfile + implements ObservabilityProfile +{ + name = 'datadog'; + + supports(profile: ObservabilityProfileName): boolean { + return profile === this.name; + } + + applyDefaults( + config: ResolvedObservabilityConfig, + ): ResolvedObservabilityConfig { + const apiKey = process.env.DD_API_KEY?.trim(); + const otlpEndpoint = + config.otlpEndpoint ?? + (config.exporterProtocol === 'grpc' + ? 'http://localhost:4317' + : 'http://localhost:4318/v1/traces'); + + return { + ...config, + otlpEndpoint, + otlpHeaders: { + ...(apiKey ? {'dd-api-key': apiKey} : {}), + ...config.otlpHeaders, + }, + resourceAttributes: { + 'vendor.apm': 'datadog', + ...config.resourceAttributes, + }, + }; + } + + initialize( + config: ResolvedObservabilityConfig, + context: ProfileBootstrapContext, + ): ProfileInitResult { + return super.initialize(this.applyDefaults(config), context); + } +} diff --git a/packages/observability/src/profiles/index.ts b/packages/observability/src/profiles/index.ts new file mode 100644 index 0000000000..84039b0711 --- /dev/null +++ b/packages/observability/src/profiles/index.ts @@ -0,0 +1,6 @@ +export * from './datadog.profile'; +export * from './keys'; +export * from './newrelic.profile'; +export * from './otlp.profile'; +export * from './registry.service'; +export * from './signoz.profile'; diff --git a/packages/observability/src/profiles/instrumentations.ts b/packages/observability/src/profiles/instrumentations.ts new file mode 100644 index 0000000000..9b36658767 --- /dev/null +++ b/packages/observability/src/profiles/instrumentations.ts @@ -0,0 +1,153 @@ +import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node'; +import { + ObservabilityInstrumentation, + ResolvedObservabilityConfig, +} from '../types'; + +export type InstrumentationName = + | 'http' + | 'express' + | 'pg' + | 'mysql' + | 'redis' + | 'kafka'; + +export type InstrumentationModuleRequirement = + | {type: 'all'; modules: string[]} + | {type: 'any'; modules: string[]}; + +export const INSTRUMENTATION_MODULE_REQUIREMENTS: Record< + InstrumentationName, + InstrumentationModuleRequirement[] +> = { + http: [{type: 'all', modules: ['@opentelemetry/instrumentation-http']}], + express: [{type: 'all', modules: ['@opentelemetry/instrumentation-express']}], + pg: [{type: 'all', modules: ['@opentelemetry/instrumentation-pg']}], + mysql: [ + { + type: 'any', + modules: [ + '@opentelemetry/instrumentation-mysql', + '@opentelemetry/instrumentation-mysql2', + ], + }, + ], + redis: [{type: 'all', modules: ['@opentelemetry/instrumentation-redis']}], + kafka: [{type: 'all', modules: ['@opentelemetry/instrumentation-kafkajs']}], +}; + +export type ManagedInstrumentation = ObservabilityInstrumentation; + +export type InstrumentationLoader = ( + name: InstrumentationName, +) => ManagedInstrumentation[]; + +function isModuleInstalled(moduleName: string): boolean { + try { + require.resolve(moduleName); + return true; + } catch { + return false; + } +} + +function loadInstrumentations( + name: InstrumentationName, +): ManagedInstrumentation[] { + switch (name) { + case 'http': { + const mod = require('@opentelemetry/instrumentation-http') as { + HttpInstrumentation: new () => ManagedInstrumentation; + }; + return [new mod.HttpInstrumentation()]; + } + case 'express': { + const mod = require('@opentelemetry/instrumentation-express') as { + ExpressInstrumentation: new () => ManagedInstrumentation; + }; + return [new mod.ExpressInstrumentation()]; + } + case 'pg': { + const mod = require('@opentelemetry/instrumentation-pg') as { + PgInstrumentation: new () => ManagedInstrumentation; + }; + return [new mod.PgInstrumentation()]; + } + case 'mysql': { + const instrumentations: ManagedInstrumentation[] = []; + + if (isModuleInstalled('@opentelemetry/instrumentation-mysql')) { + const mysql = require('@opentelemetry/instrumentation-mysql') as { + MySQLInstrumentation: new () => ManagedInstrumentation; + }; + instrumentations.push(new mysql.MySQLInstrumentation()); + } + + if (isModuleInstalled('@opentelemetry/instrumentation-mysql2')) { + const mysql2 = require('@opentelemetry/instrumentation-mysql2') as { + MySQL2Instrumentation: new () => ManagedInstrumentation; + }; + instrumentations.push(new mysql2.MySQL2Instrumentation()); + } + + return instrumentations; + } + case 'redis': { + const redis = require('@opentelemetry/instrumentation-redis') as { + RedisInstrumentation: new () => ManagedInstrumentation; + }; + return [new redis.RedisInstrumentation()]; + } + case 'kafka': { + const mod = require('@opentelemetry/instrumentation-kafkajs') as { + KafkaJsInstrumentation: new () => ManagedInstrumentation; + }; + return [new mod.KafkaJsInstrumentation()]; + } + } +} + +export function createAutoInstrumentations( + config: ResolvedObservabilityConfig, + tracerProvider: NodeTracerProvider, + loader: InstrumentationLoader = loadInstrumentations, +): ManagedInstrumentation[] { + const instrumentationNames = getEnabledInstrumentations(config); + const instrumentations = [ + ...instrumentationNames.flatMap(name => loader(name)), + ...config.customInstrumentations, + ]; + for (const instrumentation of instrumentations) { + instrumentation.setTracerProvider?.(tracerProvider); + instrumentation.enable(); + } + + return instrumentations; +} + +export function getEnabledInstrumentations( + config: ResolvedObservabilityConfig, +): InstrumentationName[] { + const instrumentationNames: InstrumentationName[] = []; + + if (config.instrumentations.http) { + instrumentationNames.push('http'); + } + if (config.instrumentations.express) { + instrumentationNames.push('express'); + } + if (config.instrumentations.pg) { + instrumentationNames.push('pg'); + } + if (config.instrumentations.mysql) { + instrumentationNames.push('mysql'); + } + if (config.instrumentations.redis) { + instrumentationNames.push('redis'); + } + if (config.instrumentations.kafka) { + instrumentationNames.push('kafka'); + } + + return instrumentationNames; +} diff --git a/packages/observability/src/profiles/keys.ts b/packages/observability/src/profiles/keys.ts new file mode 100644 index 0000000000..6e3d0fbf46 --- /dev/null +++ b/packages/observability/src/profiles/keys.ts @@ -0,0 +1,9 @@ +import {BindingTemplate, extensionFor} from '@loopback/core'; + +export const OBSERVABILITY_PROFILE_EXTENSION_POINT_NAME = + 'sf.packages.observability.profiles'; + +export const asObservabilityProfile: BindingTemplate = binding => { + extensionFor(OBSERVABILITY_PROFILE_EXTENSION_POINT_NAME)(binding); + binding.tag({namespace: OBSERVABILITY_PROFILE_EXTENSION_POINT_NAME}); +}; diff --git a/packages/observability/src/profiles/newrelic.profile.ts b/packages/observability/src/profiles/newrelic.profile.ts new file mode 100644 index 0000000000..df181aaf50 --- /dev/null +++ b/packages/observability/src/profiles/newrelic.profile.ts @@ -0,0 +1,50 @@ +import { + ObservabilityProfileName, + ObservabilityProfile, + ProfileBootstrapContext, + ProfileInitResult, + ResolvedObservabilityConfig, +} from '../types'; +import {BaseOtlpObservabilityProfile} from './otlp.profile'; + +export class NewRelicObservabilityProfile + extends BaseOtlpObservabilityProfile + implements ObservabilityProfile +{ + name = 'newrelic'; + + supports(profile: ObservabilityProfileName): boolean { + return profile === this.name; + } + + applyDefaults( + config: ResolvedObservabilityConfig, + ): ResolvedObservabilityConfig { + const licenseKey = process.env.NEW_RELIC_LICENSE_KEY?.trim(); + const otlpEndpoint = + config.otlpEndpoint ?? + (config.exporterProtocol === 'grpc' + ? 'https://otlp.nr-data.net:4317' + : 'https://otlp.nr-data.net:4318/v1/traces'); + + return { + ...config, + otlpEndpoint, + otlpHeaders: { + ...(licenseKey ? {'api-key': licenseKey} : {}), + ...config.otlpHeaders, + }, + resourceAttributes: { + 'vendor.apm': 'newrelic', + ...config.resourceAttributes, + }, + }; + } + + initialize( + config: ResolvedObservabilityConfig, + context: ProfileBootstrapContext, + ): ProfileInitResult { + return super.initialize(this.applyDefaults(config), context); + } +} diff --git a/packages/observability/src/profiles/otlp.profile.ts b/packages/observability/src/profiles/otlp.profile.ts new file mode 100644 index 0000000000..5caeeeb478 --- /dev/null +++ b/packages/observability/src/profiles/otlp.profile.ts @@ -0,0 +1,141 @@ +import {Resource, detectResourcesSync} from '@opentelemetry/resources'; +import { + AlwaysOffSampler, + AlwaysOnSampler, + BatchSpanProcessor, + ParentBasedSampler, + SpanExporter, + TraceIdRatioBasedSampler, +} from '@opentelemetry/sdk-trace-base'; +import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node'; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, +} from '@opentelemetry/semantic-conventions'; +import {ATTR_DEPLOYMENT_ENVIRONMENT} from '@opentelemetry/semantic-conventions/incubating'; +import { + ObservabilityProfileName, + ObservabilityProfile, + ProfileBootstrapContext, + ProfileInitResult, + ResolvedObservabilityConfig, +} from '../types'; +import { + createAutoInstrumentations, + ManagedInstrumentation, +} from './instrumentations'; + +function resolveSampler(config: ResolvedObservabilityConfig) { + switch (config.sampler) { + case 'always_off': + return new AlwaysOffSampler(); + case 'traceidratio': + return new ParentBasedSampler({ + root: new TraceIdRatioBasedSampler(config.samplerArg), + }); + case 'always_on': + default: + return new AlwaysOnSampler(); + } +} + +function buildResource(config: ResolvedObservabilityConfig): Resource { + const attributes: Record = { + ...config.resourceAttributes, + [ATTR_SERVICE_NAME]: config.serviceName, + }; + + if (config.serviceVersion) { + attributes[ATTR_SERVICE_VERSION] = config.serviceVersion; + } + + if (config.environment) { + attributes[ATTR_DEPLOYMENT_ENVIRONMENT] = config.environment; + } + + return detectResourcesSync().merge(new Resource(attributes)); +} + +function loadOtlpExporter(config: ResolvedObservabilityConfig): SpanExporter { + if (config.exporterProtocol === 'grpc') { + const otlpGrpc = require('@opentelemetry/exporter-trace-otlp-grpc') as { + OTLPTraceExporter: new (options?: object) => SpanExporter; + }; + + return new otlpGrpc.OTLPTraceExporter({ + url: config.otlpEndpoint, + metadata: Object.keys(config.otlpHeaders).length + ? config.otlpHeaders + : undefined, + }); + } + + const otlpHttp = require('@opentelemetry/exporter-trace-otlp-http') as { + OTLPTraceExporter: new (options?: object) => SpanExporter; + }; + + return new otlpHttp.OTLPTraceExporter({ + url: config.otlpEndpoint, + headers: Object.keys(config.otlpHeaders).length + ? config.otlpHeaders + : undefined, + }); +} + +export abstract class BaseOtlpObservabilityProfile implements ObservabilityProfile { + abstract name: ObservabilityProfileName; + + private instrumentations: ManagedInstrumentation[] = []; + private tracerProvider?: NodeTracerProvider; + + supports(profile: ObservabilityProfileName): boolean { + return profile === this.name; + } + + applyDefaults( + config: ResolvedObservabilityConfig, + ): ResolvedObservabilityConfig { + return config; + } + + initialize( + config: ResolvedObservabilityConfig, + context: ProfileBootstrapContext, + ): ProfileInitResult { + const exporter = context.createExporter(config); + const tracerProvider = new NodeTracerProvider({ + resource: buildResource(config), + sampler: resolveSampler(config), + spanProcessors: [new BatchSpanProcessor(exporter)], + }); + + tracerProvider.register(); + this.instrumentations = createAutoInstrumentations(config, tracerProvider); + this.tracerProvider = tracerProvider; + + return { + exporterName: loadOtlpExporter.name, + tracerProvider, + }; + } + + async shutdown(): Promise { + for (const instrumentation of this.instrumentations) { + instrumentation.disable(); + } + this.instrumentations = []; + + await (this.tracerProvider?.shutdown() ?? Promise.resolve()); + this.tracerProvider = undefined; + } +} + +export class OtlpObservabilityProfile extends BaseOtlpObservabilityProfile { + name: ObservabilityProfileName = 'default'; +} + +export function createOtlpExporter( + config: ResolvedObservabilityConfig, +): SpanExporter { + return loadOtlpExporter(config); +} diff --git a/packages/observability/src/profiles/registry.service.ts b/packages/observability/src/profiles/registry.service.ts new file mode 100644 index 0000000000..9209568246 --- /dev/null +++ b/packages/observability/src/profiles/registry.service.ts @@ -0,0 +1,18 @@ +import {Getter, extensionPoint, extensions} from '@loopback/core'; +import {ObservabilityProfile, ObservabilityProfileName} from '../types'; +import {OBSERVABILITY_PROFILE_EXTENSION_POINT_NAME} from './keys'; + +@extensionPoint(OBSERVABILITY_PROFILE_EXTENSION_POINT_NAME) +export class ObservabilityProfileRegistry { + constructor( + @extensions() + private readonly getProfiles: Getter, + ) {} + + async getProfile( + profileName: ObservabilityProfileName, + ): Promise { + const profiles = await this.getProfiles(); + return profiles.find(profile => profile.supports(profileName)); + } +} diff --git a/packages/observability/src/profiles/signoz.profile.ts b/packages/observability/src/profiles/signoz.profile.ts new file mode 100644 index 0000000000..3ac7ba0e26 --- /dev/null +++ b/packages/observability/src/profiles/signoz.profile.ts @@ -0,0 +1,45 @@ +import { + ObservabilityProfileName, + ObservabilityProfile, + ProfileBootstrapContext, + ProfileInitResult, + ResolvedObservabilityConfig, +} from '../types'; +import {BaseOtlpObservabilityProfile} from './otlp.profile'; + +export class SignozObservabilityProfile + extends BaseOtlpObservabilityProfile + implements ObservabilityProfile +{ + name = 'signoz'; + + supports(profile: ObservabilityProfileName): boolean { + return profile === this.name; + } + + applyDefaults( + config: ResolvedObservabilityConfig, + ): ResolvedObservabilityConfig { + const otlpEndpoint = + config.otlpEndpoint ?? + (config.exporterProtocol === 'grpc' + ? 'http://localhost:4317' + : 'http://localhost:4318/v1/traces'); + + return { + ...config, + otlpEndpoint, + resourceAttributes: { + 'vendor.apm': 'signoz', + ...config.resourceAttributes, + }, + }; + } + + initialize( + config: ResolvedObservabilityConfig, + context: ProfileBootstrapContext, + ): ProfileInitResult { + return super.initialize(this.applyDefaults(config), context); + } +} diff --git a/packages/observability/src/runtime.ts b/packages/observability/src/runtime.ts new file mode 100644 index 0000000000..dacd589d85 --- /dev/null +++ b/packages/observability/src/runtime.ts @@ -0,0 +1,33 @@ +import { + ObservabilityConfig, + ObservabilityProfile, + ObservabilityRuntime, + ResolvedObservabilityConfig, +} from './types'; + +export type RuntimeState = { + bootstrapOverrides?: Partial; + config?: ResolvedObservabilityConfig; + profile?: ObservabilityProfile; + runtime?: ObservabilityRuntime; +}; + +const state: RuntimeState = {}; + +export function getRuntimeState(): RuntimeState { + return state; +} + +export function updateRuntimeState( + partial: Partial, +): RuntimeState { + Object.assign(state, partial); + return state; +} + +export function clearRuntimeState(): void { + state.bootstrapOverrides = undefined; + state.config = undefined; + state.profile = undefined; + state.runtime = undefined; +} diff --git a/packages/observability/src/tracing.ts b/packages/observability/src/tracing.ts new file mode 100644 index 0000000000..9342a502c9 --- /dev/null +++ b/packages/observability/src/tracing.ts @@ -0,0 +1,38 @@ +import {context, trace} from '@opentelemetry/api'; +import {AttributeValue} from './types'; + +function getTracer() { + return trace.getTracer('@sourceloop/observability'); +} + +export async function withSpan( + name: string, + fn: () => Promise, + attributes?: Record, +): Promise { + return getTracer().startActiveSpan(name, async span => { + if (attributes) { + span.setAttributes(attributes); + } + + try { + return await fn(); + } catch (error) { + span.recordException(error as Error); + throw error; + } finally { + span.end(); + } + }); +} + +export function addSpanAttributes( + attributes: Record, +): void { + const span = trace.getSpan(context.active()); + if (!span) { + return; + } + + span.setAttributes(attributes); +} diff --git a/packages/observability/src/types.ts b/packages/observability/src/types.ts new file mode 100644 index 0000000000..5d8c3881ea --- /dev/null +++ b/packages/observability/src/types.ts @@ -0,0 +1,100 @@ +import {BindingTemplate, Constructor} from '@loopback/core'; +import {SpanExporter} from '@opentelemetry/sdk-trace-base'; +import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node'; + +export type AttributeValue = string | number | boolean; + +export type ObservabilityProfileName = + | 'none' + | 'default' + | 'newrelic' + | 'signoz' + | 'datadog' + | (string & {}); + +export type ExporterProtocol = 'grpc' | 'http/protobuf'; + +export type SamplerName = 'always_on' | 'always_off' | 'traceidratio'; + +export interface ObservabilityInstrumentation { + disable(): void; + enable(): void; + setTracerProvider?(tracerProvider: NodeTracerProvider): void; +} + +export interface InstrumentationToggles { + http: boolean; + express: boolean; + pg: boolean; + mysql: boolean; + redis: boolean; + kafka: boolean; +} + +export interface ObservabilityConfig { + enabled?: boolean; + profile?: ObservabilityProfileName; + serviceName?: string; + serviceVersion?: string; + environment?: string; + otlpEndpoint?: string; + otlpHeaders?: Record; + exporterProtocol?: ExporterProtocol; + sampler?: SamplerName; + samplerArg?: number; + instrumentations?: Partial; + customInstrumentations?: ObservabilityInstrumentation[]; + resourceAttributes?: Record; +} + +export interface ResolvedObservabilityConfig { + enabled: boolean; + profile: ObservabilityProfileName; + serviceName: string; + serviceVersion?: string; + environment?: string; + otlpEndpoint?: string; + otlpHeaders: Record; + exporterProtocol: ExporterProtocol; + sampler: SamplerName; + samplerArg: number; + instrumentations: InstrumentationToggles; + customInstrumentations: ObservabilityInstrumentation[]; + resourceAttributes: Record; +} + +export interface ProfileInitResult { + exporterName: string; + tracerProvider: NodeTracerProvider; +} + +export interface ObservabilityRuntime { + enabled: boolean; + profile: ObservabilityProfileName; + config: ResolvedObservabilityConfig; + tracerProvider?: NodeTracerProvider; + shutdown(): Promise; +} + +export interface ProfileBootstrapContext { + createExporter(config: ResolvedObservabilityConfig): SpanExporter; +} + +export interface ObservabilityProfile { + name: ObservabilityProfileName; + supports(profile: ObservabilityProfileName): boolean; + applyDefaults( + config: ResolvedObservabilityConfig, + ): ResolvedObservabilityConfig; + initialize( + config: ResolvedObservabilityConfig, + context: ProfileBootstrapContext, + ): ProfileInitResult; + shutdown?(): Promise; +} + +export interface ProfileRegistration { + key: string; + profileClass: Constructor; + template: BindingTemplate; +} diff --git a/packages/observability/tsconfig.json b/packages/observability/tsconfig.json new file mode 100644 index 0000000000..e29ea9d525 --- /dev/null +++ b/packages/observability/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "CommonJS", + "moduleResolution": "node", + "declaration": true, + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node", "mocha"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +}