diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000..a92d04a --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,20 @@ +{ + "rules": { + "type-enum": [ + 2, + "always", + [ + "feat", + "fix", + "doc", + "perf", + "refactor", + "style", + "test", + "chore", + "security", + "revert" + ] + ] + } +} diff --git a/.env.example b/.env.example index eb01de8..9b1e182 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,23 @@ # App +APP_NAME="nodejs-template" APP_PORT=3000 -LOG_LEVEL="info" + +# LOG +LOG_LEVEL=info # trace is the lowest, fatal the highest +LOG_SAMPLING_RATE=0.05 # Database +DATABASE_PORT=5432 +DATABASE_HOST=localhost DATABASE_USER="postgres" DATABASE_PASSWORD="postgres" DATABASE_NAME="postgres" -DATABASE_HOST=localhost -DATABASE_PORT=5432 DATABASE_MAX=10 + +# OTEL +OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4318" + +# OTHER +NPM_PACKAGE_VERSION="1.0.0" + +# TODO: ratelimiting, json payload sizes, http logger diff --git a/README.md b/README.md index 3f17710..6cf7ebe 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,14 @@ A Node.js template for building RESTful APIs. Includes: - Docker - PostgreSQL - An example RESTful API +- Wide events based logging +- Otel tracing support +- Metrics support for Prometheus The project structures follows the [Tao of Node](https://alexkondov.com/tao-of-node/), which I've found very useful during my career. +Logging follows the philosophy from [this](https://loggingsucks.com/) blog. + ## Quickstart You need to have diff --git a/package.json b/package.json index 6342a0b..05c6cf4 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dev": "NODE_ENV=development tsx watch src/index.ts", "typecheck": "tsc", "test": "vitest run && vitest run --config vitest.integration.config.ts", + "test:debug": "vitest run --printConsoleTrace=true --silent=false && vitest run --config vitest.integration.config.ts --printConsoleTrace=true --silent=false", "test:watch": "vitest", "test:unit": "vitest run", "test:integration": "vitest run --config vitest.integration.config.ts", @@ -51,14 +52,23 @@ "vitest": "^4.1.7" }, "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/auto-instrumentations-node": "^0.76.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", + "@opentelemetry/resources": "^2.7.1", + "@opentelemetry/sdk-node": "^0.218.0", + "@opentelemetry/sdk-trace-node": "^2.7.1", + "@opentelemetry/semantic-conventions": "^1.41.1", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^8.5.2", "helmet": "^8.1.0", "http-status-codes": "^2.3.0", "kysely": "^0.29.2", "pg": "^8.21.0", "pino": "^10.3.1", + "prom-client": "^15.1.3", "zod": "^4.4.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5cb6eba..668f640 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,27 @@ importers: .: dependencies: + '@opentelemetry/api': + specifier: ^1.9.1 + version: 1.9.1 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.76.0 + version: 0.76.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.218.0 + version: 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ^2.7.1 + version: 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': + specifier: ^0.218.0 + version: 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': + specifier: ^2.7.1 + version: 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': + specifier: ^1.41.1 + version: 1.41.1 cors: specifier: ^2.8.6 version: 2.8.6 @@ -17,6 +38,9 @@ importers: express: specifier: ^5.2.1 version: 5.2.1 + express-rate-limit: + specifier: ^8.5.2 + version: 8.5.2(express@5.2.1) helmet: specifier: ^8.1.0 version: 8.2.0 @@ -32,6 +56,9 @@ importers: pino: specifier: ^10.3.1 version: 10.3.1 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 zod: specifier: ^4.4.3 version: 4.4.3 @@ -95,7 +122,7 @@ importers: version: 8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) vitest: specifier: ^4.1.7 - version: 4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages: @@ -379,6 +406,461 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@opentelemetry/api-logs@0.218.0': + resolution: {integrity: sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/auto-instrumentations-node@0.76.0': + resolution: {integrity: sha512-44KWgqsMuqfV4UhOcwwnDeK8CpB5LT1MmpZj6sKXFXu2q6rjKo622pWgOgn5Ntp5Qal9q1uBX2VS8mvTpsMeyw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + '@opentelemetry/core': ^2.0.0 + + '@opentelemetry/configuration@0.218.0': + resolution: {integrity: sha512-W8wIz7H2R1pufR5jfjb3gU2XkMpm2x/7b1RJcsuzvd70Il/rWWE+g5/Od7hQKrxRTSrTrOWlru101PWXz5I1EQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.7.1': + resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.7.1': + resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.218.0': + resolution: {integrity: sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.218.0': + resolution: {integrity: sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.218.0': + resolution: {integrity: sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.218.0': + resolution: {integrity: sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.218.0': + resolution: {integrity: sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.218.0': + resolution: {integrity: sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.218.0': + resolution: {integrity: sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.218.0': + resolution: {integrity: sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.218.0': + resolution: {integrity: sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.218.0': + resolution: {integrity: sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.7.1': + resolution: {integrity: sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.65.0': + resolution: {integrity: sha512-fF7fNHA59n3y23ROfst2EbSxmP+L3E+snZO6aMU4w4xD84mfejAivspIAsqa9arX5HZlBK6dslHz5dWGNp5D0A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.70.0': + resolution: {integrity: sha512-HT74cQxi/iiVEz5dRdNdfGCFzPFbkxSiwHfFPHDwkRcr1JKQqI6hm8qeXEvEiJ+36xIU1KkQMDfeThJ1ifnUiA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.73.0': + resolution: {integrity: sha512-0INPkHbR6o4J3psE+ncwWaE7qtDpb2p+i+qfV82cfwYLCXavYCGosBZ/S4pOErDVJYIyQVIsNAHhaUgaL313SQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.63.0': + resolution: {integrity: sha512-z0xPSZ62d3I7sG2sUTyQ5/ES1RdESP2eOETiMLY9gPSp+HZwbsAyj7T/2sdZKYD+O2ajRHZEil+DBoUolf1ocQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.63.0': + resolution: {integrity: sha512-jnVTOr3h/46UDalEwJ4ITux8UWwHmnsOik5WFs3JB/UrUj8Wad5eI+KpOEBuOUeOfPB9sce11qgVw3WXU2r+hg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.61.0': + resolution: {integrity: sha512-ZTQ0W3Lb7GJsOd+72cG8FJQKA5DqYfELJGLmChrJIezRSLfJIfofwKEGLX5rMtFJmwckpichQkBZWjid5dvnVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.34.0': + resolution: {integrity: sha512-VK63Cm8osAdsSZpULPk+qnNktQUJzmnIOv2wuh79fV41WuTM38uOFC3s978/24pDkSljhN4EYCbPRLrAhXfKSA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.35.0': + resolution: {integrity: sha512-6x6UPP0tLzrdj15PIEN3qgp/WCcESCavHJfkIKoyLmy4UjGLF1KgEUMyD74xhbKGo426uvMbhvCgZC0ye8nO/A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.61.0': + resolution: {integrity: sha512-5D8xFaw9GXq9ZIOAvG7NPDivFfZWFAekLGFn1B7ppyhuAYBVHGybFpx4Q9BV1Uup3yzCdiD78KhyH7c3dKOYSw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.66.0': + resolution: {integrity: sha512-G1xTh5M5shklMgIyUXWDjU2BakulKtcISaM4U5TyanvO7R4xbB3iC7YQ8QKegLXaOs81Ku8RlcIcbYRrz/82wQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.37.0': + resolution: {integrity: sha512-5mxhFuwAK0FFvisUdvuywaZ9ySMZ15HfbN6IpLn0gwRh9s1/QBcpLznQ/A15cZs1QFtBJ+JXIHdwY7WOD0c4Eg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.61.0': + resolution: {integrity: sha512-tvp5PWnGRPHY/kz9Kg1IRLBL0qUAxMSNG623f+ZGEsvnCVEjr3tFyw1JGQzM+B3eZKkO+Dp/LYrtOSfb69D5lA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.66.0': + resolution: {integrity: sha512-D4PN1tStj6rnOdofnt2xINJjtT1k2ockzaODrn76VEBZeqJ3QsEvKFfunB0EFAohO4xswVp14VAVmKNnGzA1Dw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.218.0': + resolution: {integrity: sha512-kcDCNrC7IWNXEKQriGrwuh5jjbMFU5exOQzU9ufEY9UkACNcgYIdOd7XpX3IqZ3UPSnZyZtlwgfsbC5SNlEDbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.64.0': + resolution: {integrity: sha512-PCHgCICCDz7p9BgCU9gQz2smbqu4V4P8QtWJ7DLjL3bmzSdrgy6EGvecDg1YuhjBsoN08SR+y36hgdHkqCgrzQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.218.0': + resolution: {integrity: sha512-x9djaqdzpT8WAboep1H9nCAQ1E+MMsm08TNfA02TqM3bNNddZeiim+E3KMWVQFaX6JpUy7V0nm/wfN/K2Em+Zw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.66.0': + resolution: {integrity: sha512-UfTAcaBKCzLUZ9opvfOLV4bH46XiNFqUsKykfPCIefDIxJ1iUYtMOucNaiZ+/kjQdPy5i6Ef5tk2IAjxol4X1w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.27.0': + resolution: {integrity: sha512-kl/C2AU4KZGHlMZD12nMFXcMjxSHvu5Q0UPSQ6IJeBfCadYuWgW+sWIa2JZVK/A0qRYm2cncekJyeBHQDyfUUg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.62.0': + resolution: {integrity: sha512-XgfhCAWwSqA0YnwaEKdpvQMavc90D3R65frhLCO9JNl867EulNps9tm6pjGIg+GiYuewn00gEzW4HQ5btgYxGQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.66.0': + resolution: {integrity: sha512-04x/z21WTMEfy3lUSr4aTj8WsTN3OZF901hJ+ciOwdwf7AK8UJTpZCXw6KQ3G4Vag56q1HoMihCONeWZLeld1g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.62.0': + resolution: {integrity: sha512-AlGKIdk6ZT7WmIozfUb2LjOcI3AhQrvAXKX0zi1cVcnw2QlRbVYyV5GTa2Th9ebuczVfWPaoPrmZw61zCp/czw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.61.0': + resolution: {integrity: sha512-qiCR9Wovf5AHzn6g+LXhvwMmv2I6zhHz2I2tEHZMmBuD8c18bkJzGFxHoSBlxdApRT+SW13r9472dDMm4BRjgQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.71.0': + resolution: {integrity: sha512-6rwfVjAUY69CKkyGqzL+F5X7Nzw0+Ke9pOxk9xUPJpy8vracZxuQYF7rWu02sV1xOgi4u52449SuVhD+zaSiIA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.64.0': + resolution: {integrity: sha512-iCIqeUaERN8Uc5Rrtg4zvQ6d7z5JQ5iUmbnr/JHYPxAidDowmRc8/wDMJeMKRfLPTj336Zu0ec7rH/ak/4N9vw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.64.0': + resolution: {integrity: sha512-yTu0mYh/qJPSE86VmNLQww5uugDyvCS2KJIPfPtIk2ufoEUoHPsV6Iynnvmz588Moq04aBLxfTa/EtE4A2ykWA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.64.0': + resolution: {integrity: sha512-W1w76AJkP7i0uzzAe7nsCMWq4+EMSA550f1lAmxDPdQC5FnreNbRIm/tod2OS9gVrYvRrQXNkFmZJKGo4kzCnw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.64.0': + resolution: {integrity: sha512-PW1ArxryMwF8/IXq1nzlQs7tmr/fWd1tf71AHevZT3Fm0hW7jRX9JEfYgIAcKDvmbqcJEr5K1224NEimrRPbuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.62.0': + resolution: {integrity: sha512-Gt2kzpACpmIad+q3LQqe8UNHuoVvdLuFpB6SN/A6xLPKNllb+ksPUYQhj1kXdZOpcFZNGKDXHyN+TUCVCk1TRw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-openai@0.16.0': + resolution: {integrity: sha512-I0KKybyqqFOxSBgYKQNdR/EF3LvzSaAUT7Y75xkjbgscY+V8UWDpUbY68POLhUC3SKMlGvZmrTSxcQ+Y0vRhNw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-oracledb@0.43.0': + resolution: {integrity: sha512-7Z4kOOdnrHX4S5gCeWhnnpWQwEd7weRjDhJA1nSrwTYtAcVWNjk5wsMKHBCTDCN0uJtA9T6PouZ+AKRYiS1Rrg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.70.0': + resolution: {integrity: sha512-g8WXwwOUXfjiEmATwjB/33QKE2AkIpNe4KIuJJh4djtXgCL0Wne+AzAfjuDIAspGvO1txQp8ibKsLd3SBmcvJA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.64.0': + resolution: {integrity: sha512-+vDL7tZMZjkp8BpYMx/cL2/HWGsNUqKcRmAIIEaQu/6F44oM6xGDMCSqMKHdKCsH1+WW52EYdHbWkVGTF0KVsQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.66.0': + resolution: {integrity: sha512-bVShkag6vP2VQO0cpA8CHjOohWbKNYLyjiwGkOnSAwou1TPc6pf9DssFUxwqN2XF1J4oqP0LVSvN9kZUzMecfA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.63.0': + resolution: {integrity: sha512-Z73YxZpt0Y56uRu2pRWOjO5wXHvZqF46K4czoKRTGlUifzzFmUZxyOeAAECACuMRSLZmZ394WJin0MDgU9iW9w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.62.0': + resolution: {integrity: sha512-0w8ok7GbXtYvX7TtLp72qQJKNyI7lD72Fy2NsNKIcQAv6TqGox5javFyXrIrCAtZHCONePxeAwAYj1Qd9si9OQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-runtime-node@0.31.0': + resolution: {integrity: sha512-HkLsuEfUDahFiL/xFtEqJDMp7sp8ynOtA045bJi9nAH8CrPvljPW5SgJQb2mQqEYJQopbWYZ2lPqQEfj7bYgJg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.65.0': + resolution: {integrity: sha512-dNvIbD40h0z69stQ9cIeAWRyy5WyQM1a1XnFthekc/oi/ipX4E6oYJBM4X2xKBxjZMTjdV5VshLoNeYMSBsnjw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.37.0': + resolution: {integrity: sha512-cGLF46UsgeI1334atJxLO36yQlV7WXKg35Mp+e2NXo2vOTfIZTVqoKOzExVOTOwT4AQjfGVEDxyq5wXybUYXIA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.28.0': + resolution: {integrity: sha512-7nh4Gw7PhYtQm82FIJtWUhx6iZQJj0bdkKe2RQb3XNIyxu0o9rM1J5Xt083SsG2tCbQZpX9/mlDxhTrK1Z/lVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.62.0': + resolution: {integrity: sha512-pr1U9ZV4RRy23qMVrRzebfxwDWjp44xA7sC0PAdeW9v4HDcfOr0ejdTJmIsBGvhkNHPBajfieaIF9b6/9wjErA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.218.0': + resolution: {integrity: sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.218.0': + resolution: {integrity: sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.218.0': + resolution: {integrity: sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.218.0': + resolution: {integrity: sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.7.1': + resolution: {integrity: sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.7.1': + resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.38.3': + resolution: {integrity: sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/resource-detector-alibaba-cloud@0.33.8': + resolution: {integrity: sha512-RnSB/uxkElny0/WBFEtIG2HRG0cpSNTRdE+YSB7Poa+uljK+ddCacEZYz/PMgZh+cs586XstJQxdyjz0jtcAug==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@2.18.0': + resolution: {integrity: sha512-wyMM4UoRuHvI2KjqnTzvyW8Yv7MKRGA+I78Xti6gTEw7hBhqXU1SRo+f9KrsQfeeiOn+TkDuvxavuaAQbD3i6g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.26.0': + resolution: {integrity: sha512-7KxF7mlwI2nKja/iEdwPqOaS0QAJbhT9ye4DeYZnXdOS/4phfonk5nSmyGDBYhBL7J30MPL91oZNuGYRKXZAXA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.8.9': + resolution: {integrity: sha512-Xd2C4HjW9hl75iqZT7tQNy2yRBUqNucq2O9+e0FJRNkbiItInYVMzc0S0KDXcx/vZBwNmlrKS3R0uLCU9ULsGA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.53.0': + resolution: {integrity: sha512-RCV31v23ZwZfYR3LPkuORHTHIOvfm3hZBT7hAzSO0+oAIrG/Dm0ld5tV4lYNO05GjI7sHQdRcbSqzEYAvQcQuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@2.7.1': + resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.218.0': + resolution: {integrity: sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.7.1': + resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.218.0': + resolution: {integrity: sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.7.1': + resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.7.1': + resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@oxc-project/types@0.132.0': resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} @@ -529,9 +1011,15 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/aws-lambda@8.10.161': + resolution: {integrity: sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/bunyan@1.8.11': + resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -583,18 +1071,33 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} '@types/node@25.9.1': resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/oracledb@6.5.2': + resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} + + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} + + '@types/pg@8.15.6': + resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -625,6 +1128,9 @@ packages: '@types/supertest@7.2.0': resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -724,6 +1230,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -734,6 +1245,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -846,6 +1361,12 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -918,6 +1439,9 @@ packages: citty@0.2.2: resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1004,6 +1528,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -1198,6 +1726,12 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -1205,6 +1739,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-copy@4.0.3: resolution: {integrity: sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==} @@ -1235,6 +1772,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1266,10 +1807,17 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + formidable@3.5.4: resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} engines: {node: '>=14.0.0'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1289,6 +1837,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1325,6 +1881,10 @@ packages: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1358,6 +1918,10 @@ packages: http-status-codes@2.3.0: resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1373,6 +1937,10 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + import-in-the-middle@3.0.1: + resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} + engines: {node: '>=18'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1380,6 +1948,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1420,6 +1992,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1757,6 +2332,9 @@ packages: engines: {node: '>=10'} hasBin: true + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1775,9 +2353,18 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -1952,6 +2539,10 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -2021,6 +2612,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -2181,6 +2776,9 @@ packages: tar-stream@3.2.0: resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -2380,6 +2978,10 @@ packages: jsdom: optional: true + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2665,6 +3267,682 @@ snapshots: '@noble/hashes@1.8.0': {} + '@opentelemetry/api-logs@0.218.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/auto-instrumentations-node@0.76.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.65.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-aws-lambda': 0.70.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-aws-sdk': 0.73.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-bunyan': 0.63.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-cassandra-driver': 0.63.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-cucumber': 0.34.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.35.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dns': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-express': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.37.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-grpc': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.64.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-ioredis': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.27.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-memcached': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.71.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.64.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.64.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.64.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-nestjs-core': 0.64.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-net': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-openai': 0.16.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-oracledb': 0.43.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.70.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pino': 0.64.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-restify': 0.63.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-router': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-runtime-node': 0.31.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-socket.io': 0.65.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.37.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-undici': 0.28.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-winston': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-alibaba-cloud': 0.33.8(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-aws': 2.18.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-azure': 0.26.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-container': 0.8.9(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp': 0.53.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/configuration@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + yaml: 2.9.0 + + '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/exporter-logs-otlp-grpc@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.218.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-logs-otlp-http@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.218.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-logs-otlp-proto@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-metrics-otlp-http@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-metrics-otlp-proto@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-prometheus@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/exporter-trace-otlp-grpc@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-trace-otlp-http@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-trace-otlp-proto@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-zipkin@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/instrumentation-amqplib@0.65.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.70.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/aws-lambda': 8.10.161 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.73.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.63.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.63.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.61.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.34.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.35.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.61.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.37.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.61.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.64.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.3 + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.27.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.61.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.71.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.64.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.64.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.64.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.64.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-openai@0.16.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-oracledb@0.43.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/oracledb': 6.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.70.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + '@types/pg': 8.15.6 + '@types/pg-pool': 2.0.7 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.64.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.3 + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.63.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-runtime-node@0.31.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.65.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.37.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.28.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-grpc-exporter-base@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/propagator-b3@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/propagator-jaeger@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/redis-common@0.38.3': {} + + '@opentelemetry/resource-detector-alibaba-cloud@0.33.8(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/resource-detector-aws@2.18.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/resource-detector-azure@0.26.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/resource-detector-container@0.8.9(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/resource-detector-gcp@0.53.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + gcp-metadata: 8.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-logs@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-node@0.218.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.218.0 + '@opentelemetry/configuration': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-proto': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-proto': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-prometheus': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-proto': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-zipkin': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.218.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/semantic-conventions@1.41.1': {} + + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@oxc-project/types@0.132.0': {} '@paralleldrive/cuid2@2.3.1': @@ -2765,11 +4043,17 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aws-lambda@8.10.161': {} + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 '@types/node': 25.9.1 + '@types/bunyan@1.8.11': + dependencies: + '@types/node': 25.9.1 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2833,10 +4117,18 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/memcached@2.2.10': + dependencies: + '@types/node': 25.9.1 + '@types/methods@1.1.4': {} '@types/ms@2.1.0': {} + '@types/mysql@2.15.27': + dependencies: + '@types/node': 25.9.1 + '@types/node@18.19.130': dependencies: undici-types: 5.26.5 @@ -2845,6 +4137,20 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/oracledb@6.5.2': + dependencies: + '@types/node': 25.9.1 + + '@types/pg-pool@2.0.7': + dependencies: + '@types/pg': 8.20.0 + + '@types/pg@8.15.6': + dependencies: + '@types/node': 25.9.1 + pg-protocol: 1.14.0 + pg-types: 2.2.0 + '@types/pg@8.20.0': dependencies: '@types/node': 25.9.1 @@ -2889,6 +4195,10 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.10 + '@types/tedious@4.0.14': + dependencies: + '@types/node': 25.9.1 + '@types/unist@3.0.3': {} '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': @@ -3032,12 +4342,18 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} + agent-base@7.1.4: {} + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -3139,6 +4455,10 @@ snapshots: dependencies: tweetnacl: 0.14.5 + bignumber.js@9.3.1: {} + + bintrees@1.0.2: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -3225,6 +4545,8 @@ snapshots: citty@0.2.2: {} + cjs-module-lexer@2.2.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -3299,6 +4621,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + data-uri-to-buffer@4.0.1: {} + dateformat@4.6.3: {} debug@4.4.3: @@ -3520,6 +4844,11 @@ snapshots: expect-type@1.3.0: {} + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + express@5.2.1: dependencies: accepts: 2.0.0 @@ -3555,6 +4884,8 @@ snapshots: exsolve@1.0.8: {} + extend@3.0.2: {} + fast-copy@4.0.3: {} fast-deep-equal@3.1.3: {} @@ -3575,6 +4906,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -3617,12 +4953,18 @@ snapshots: format@0.2.2: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + formidable@3.5.4: dependencies: '@paralleldrive/cuid2': 2.3.1 dezalgo: 1.0.4 once: 1.4.0 + forwarded-parse@2.1.2: {} + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -3634,6 +4976,22 @@ snapshots: function-bind@1.1.2: {} + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + get-caller-file@2.0.5: {} get-intrinsic@1.3.0: @@ -3675,6 +5033,8 @@ snapshots: globals@17.6.0: {} + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -3703,6 +5063,13 @@ snapshots: http-status-codes@2.3.0: {} + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -3713,10 +5080,19 @@ snapshots: ignore@7.0.5: {} + import-in-the-middle@3.0.1: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} inherits@2.0.4: {} + ip-address@10.2.0: {} + ipaddr.js@1.9.1: {} is-extglob@2.1.1: {} @@ -3745,6 +5121,10 @@ snapshots: joycon@3.1.1: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -4230,6 +5610,8 @@ snapshots: mkdirp@3.0.1: {} + module-details-from-path@1.0.4: {} + ms@2.1.3: {} nan@2.27.0: @@ -4241,8 +5623,16 @@ snapshots: negotiator@1.0.0: {} + node-domexception@1.0.0: {} + node-fetch-native@1.6.7: {} + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + normalize-path@3.0.0: {} nypm@0.6.6: @@ -4418,6 +5808,11 @@ snapshots: process@0.11.10: {} + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.1 + tdigest: 0.1.2 + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -4514,6 +5909,13 @@ snapshots: require-directory@2.1.1: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + retry@0.12.0: {} rolldown@1.0.2: @@ -4754,6 +6156,10 @@ snapshots: - bare-buffer - react-native-b4a + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + teex@1.0.1: dependencies: streamx: 2.26.0 @@ -4904,7 +6310,7 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vitest@4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.7 '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -4927,10 +6333,13 @@ snapshots: vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 25.9.1 transitivePeerDependencies: - msw + web-streams-polyfill@3.3.3: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f442032..d9221d2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,7 +2,7 @@ # Source: https://github.com/lirantal/npm-security-best-practices # SECURITY: block packages newer than 1 day (1440 minutes, pnpm 11 default) -# See the recent axios case to see why this is a good idea. +# See the recent supply chain attacks to see why this is a good idea. minimumReleaseAge: 1440 # SECURITY: reject a version whose publishing trust signals regressed diff --git a/src/__tests__/event.integration.test.ts b/src/__tests__/event.integration.test.ts index 75feda3..5030091 100644 --- a/src/__tests__/event.integration.test.ts +++ b/src/__tests__/event.integration.test.ts @@ -185,7 +185,7 @@ describe("POST /api/v1/event/:id/vote", () => { expect(res.body.votes).toHaveLength(2); }); - it("returns 500 when voting on a non-existent event", async () => { + it("returns 422 when voting on a non-existent event", async () => { const res = await request(app) .post(`${API}/99999/vote`) .send({ name: "Alice", votes: ["2024-06-01"] }); diff --git a/src/app.ts b/src/app.ts index bec6dc9..3d8bd33 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,13 @@ import express, { type Express } from "express"; import helmet from "helmet"; import cors from "cors"; import newEventRouter from "./event/event-handlers.ts"; -import { errorHandler } from "./middleware.ts"; +import newHealthRouter from "./health/health-handlers.ts"; +import { errorHandler } from "./middleware/error-handler.ts"; +import { rateLimiter } from "./middleware/rate-limiter.ts"; +import { wideEventHandler } from "./middleware/wide-event.ts"; +import { traceHandler } from "./middleware/trace-handler.ts"; +import { metricsCollectorHandler } from "./middleware/metrics-handler.ts"; +import { metricsHandler } from "./metrics.ts"; /** * Creates the Express app with all the necessary middleware and routes. @@ -16,10 +22,23 @@ const createApp = (): Express => { app.use(helmet()); // Setup cors here if you need it in the future. app.use(cors()); - app.use(express.json()); + // Limit the size of incoming JSON and URL-encoded bodies to 10mb. + // Defaults are a bit smaller (100kb), which is fine too. + // This is purely an example. + app.use(express.json({ limit: "10mb" })); + app.use(express.urlencoded({ extended: true, limit: "10mb" })); + // Apply rate limiting to all incoming requests. + app.use(rateLimiter); + // Centralized logging based on wide events philosophy. + // Must be registered before routes so req.event is initialized for every request. + app.use(wideEventHandler); + // Add tracing to all incoming requests. + app.use(traceHandler); + app.use(metricsCollectorHandler); + app.get(`${prefix}/metrics`, metricsHandler); + app.use(`${prefix}/health`, newHealthRouter()); app.use(`${prefix}/event`, newEventRouter()); - // Centralized error handling after routes. // This will catch any errors thrown by route handlers. // Except inside async callbacks. diff --git a/src/config.ts b/src/config.ts index 795f9bb..6c65f2b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,18 +1,89 @@ import dotenv from "dotenv"; dotenv.config(); -const config = { - environment: process.env["NODE_ENV"], - port: process.env["APP_PORT"] || 3000, - logLevel: process.env["LOG_LEVEL"] || "info", +import { z } from "zod"; + +const portSchema = z.coerce + .number() + .int() + .min(0) + .max(65535, "Port should be >= 0 and <= 65535"); + +const configSchema = z.object({ + app: z.object({ + name: z.string().default("app"), + environment: z + .enum(["test", "development", "production"]) + .default("development"), + port: portSchema.default(3000), + }), + + log: z.object({ + level: z + .enum(["fatal", "error", "warn", "info", "debug", "trace"]) + .default("info"), + samplingRate: z.coerce.number().min(0).max(1).default(0.05), + }), + + npmPackageVersion: z.string().default("0.0.0"), + + database: z.object({ + port: portSchema.default(5432), + host: z.string().min(1).default("localhost"), + user: z.string().min(1).default("postgres"), + password: z.string().default("postgres"), + name: z.string().min(1).default("postgres"), + max: z.coerce.number().int().positive().default(10), + }), + + otel: z.object({ + serviceName: z.string().default("app"), + otlpEndpoint: z.url().optional(), + }), +}); + +export type Config = z.infer; + +const loadedConfig = { + app: { + name: process.env["APP_NAME"], + environment: process.env["NODE_ENV"], + port: process.env["APP_PORT"], + }, + log: { + level: process.env["LOG_LEVEL"], + samplingRate: process.env["LOG_SAMPLING_RATE"], + }, + npmPackageVersion: process.env["NPM_PACKAGE_VERSION"], database: { - port: Number(process.env["DATABASE_PORT"]) || 5432, - host: process.env["DATABASE_HOST"] || "localhost", - user: process.env["DATABASE_USER"] || "postgres", - password: process.env["DATABASE_PASSWORD"] || "postgres", - name: process.env["DATABASE_NAME"] || "postgres", - max: Number(process.env["DATABASE_MAX"]) || 10, + port: process.env["DATABASE_PORT"], + host: process.env["DATABASE_HOST"], + user: process.env["DATABASE_USER"], + password: process.env["DATABASE_PASSWORD"], + name: process.env["DATABASE_NAME"], + max: process.env["DATABASE_MAX"], + }, + otel: { + serviceName: process.env["APP_NAME"], + otlpEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], }, }; +const result = configSchema.safeParse(loadedConfig); + +if (!result.success) { + console.error("\nEnvironment variables are not set correctly."); + console.error( + "Did you remember to `cp .env.example .env` and fill in the values?\n", + ); + + for (const issue of result.error.issues) { + console.error(`- ${issue.path.join(".")}: ${issue.message}`); + } + + process.exit(1); +} + +const config = result.data; + export default config; diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..2cff9d6 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,84 @@ +import { StatusCodes, getReasonPhrase } from "http-status-codes"; + +type ApiErrorOptions = { + message?: string; + params?: Record; + log?: { + level?: "error" | "warn" | "debug" | "fatal"; + enabled?: boolean; + message?: string; + }; +}; + +type ApiErrorConstructorOptions = ApiErrorOptions & { + statusCode: StatusCodes; +}; + +export class ApiError extends Error { + readonly statusCode: StatusCodes; + readonly log: { + level: "error" | "warn" | "debug" | "fatal"; + enabled: boolean; + message: string; + }; + readonly params?: Record | undefined; + + private constructor(options: ApiErrorConstructorOptions) { + const { + statusCode, + message = getReasonPhrase(statusCode), + params, + log, + } = options; + + super(message); + + this.name = "ApiError"; + this.statusCode = statusCode; + this.params = params; + this.log = { + level: log?.level ?? "error", + enabled: log?.enabled ?? true, + message: log?.message ?? message, + }; + } + + private static create( + statusCode: StatusCodes, + options: ApiErrorOptions = {}, + ): ApiError { + return new ApiError({ statusCode, ...options }); + } + + static badGateway(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.BAD_GATEWAY, options); + } + + static badRequest(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.BAD_REQUEST, options); + } + + static conflict(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.CONFLICT, options); + } + + static forbidden(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.FORBIDDEN, options); + } + + static internalServer(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.INTERNAL_SERVER_ERROR, options); + } + + static notFound(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.NOT_FOUND, options); + } + + static unauthorized(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.UNAUTHORIZED, options); + } + + static unprocessableEntity(options?: ApiErrorOptions): ApiError { + return ApiError.create(StatusCodes.UNPROCESSABLE_ENTITY, options); + } +} diff --git a/src/event/event-handlers.ts b/src/event/event-handlers.ts index aa53430..08bda97 100644 --- a/src/event/event-handlers.ts +++ b/src/event/event-handlers.ts @@ -1,6 +1,5 @@ import type { RequestHandler, Router } from "express"; import express from "express"; -import { validateData } from "../middleware.ts"; import { eventCreationSchema, voteCreationSchema } from "./event-schema.ts"; import type { CreateEvent, @@ -20,6 +19,9 @@ import { getEventResults, } from "./event-service.ts"; import { StatusCodes } from "http-status-codes"; +import { validateData } from "../middleware/validate-data.ts"; +import { ApiError } from "../errors.ts"; +import { SpanStatusCode, trace } from "@opentelemetry/api"; import { logger } from "../logger.ts"; /** @@ -60,18 +62,24 @@ export const eventGetHandler: RequestHandler< { id: string }, ShowEventResponse | ErrorResponse > = async (req, res) => { + const span = trace.getActiveSpan(); const { id } = req.params; + span?.setAttribute("event.id", id); + const event = await getEventById(Number(id)); if (!event) { - logger.warn(`Event ${id} not found`); - - return res - .status(StatusCodes.NOT_FOUND) - .json({ message: `Event ${id} not found` }); + throw ApiError.notFound({ + message: "Event not found.", + log: { level: "warn" }, + params: { id }, + }); } + logger.debug({ id }, "Event retrieved successfully"); + + span?.setStatus({ code: SpanStatusCode.OK }); return res.status(StatusCodes.OK).json(event); }; @@ -87,8 +95,6 @@ export const eventCreateHandler: RequestHandler< const created = await createEvent(newEvent); - logger.info(created, "Event created"); - return res.status(StatusCodes.CREATED).json(created); }; @@ -102,11 +108,9 @@ export const eventCreateVotesHandler: RequestHandler< > = async (req, res) => { const { id } = req.params; - const newVotes = await voteEvent(Number(id), req.body); + await voteEvent(Number(id), req.body); const updatedEvent = await getEventById(Number(id)); - logger.info(newVotes, "Votes created"); - return res.status(StatusCodes.OK).json(updatedEvent); }; @@ -122,11 +126,11 @@ export const eventGetResultsHandler: RequestHandler< const results = await getEventResults(Number(id)); if (!results) { - logger.warn(`Event ${id} not found`); - - return res - .status(StatusCodes.NOT_FOUND) - .json({ message: `Event ${id} not found` }); + throw ApiError.notFound({ + message: "Event results not found.", + log: { level: "warn" }, + params: { id }, + }); } return res.status(StatusCodes.OK).json(results); diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..b3a41d6 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,9 @@ +import type { WideEvent } from "./types.ts"; + +declare global { + namespace Express { + interface Request { + event: WideEvent; + } + } +} diff --git a/src/health/health-handlers.ts b/src/health/health-handlers.ts new file mode 100644 index 0000000..b54a6de --- /dev/null +++ b/src/health/health-handlers.ts @@ -0,0 +1,19 @@ +import type { RequestHandler, Router } from "express"; +import express from "express"; +import { StatusCodes } from "http-status-codes"; +import { logger } from "../logger.ts"; + +const newHealthRouter = () => { + const healthRouter: Router = express.Router(); + + healthRouter.post("/", healthHandler); + + return healthRouter; +}; + +export const healthHandler: RequestHandler = (_req, res): void => { + logger.debug("Server is healthy"); + res.sendStatus(StatusCodes.OK); +}; + +export default newHealthRouter; diff --git a/src/index.ts b/src/index.ts index 1940553..da46539 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,27 @@ import config from "./config.ts"; +import "./utils/tracing.ts"; import { logger } from "./logger.ts"; import createApp from "./app.ts"; +import { shutdown } from "./shutdown.ts"; +import { getIPv4Addresses } from "./utils/network.ts"; +import { SHUTDOWN_SIGNALS } from "./utils/constants.ts"; const app = createApp(); -app.listen(config.port, () => { - logger.info(`Server listening on port ${config.port}`); +const server = app.listen(config.app.port, () => { + for (const ipv4 of getIPv4Addresses()) { + logger.info(`Server listening at: http://${ipv4}:${config.app.port}`); + } +}); + +SHUTDOWN_SIGNALS.forEach((evt) => process.on(evt, () => void shutdown(server))); + +process.on("uncaughtException", (error) => { + logger.error({ error }, "Uncaught exception"); + void shutdown(server, 1); +}); + +process.on("unhandledRejection", (reason) => { + logger.error({ reason }, "Unhandled rejection"); + void shutdown(server, 1); }); diff --git a/src/logger.ts b/src/logger.ts index 09359d9..860f589 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,15 +1,68 @@ /** - * You can expand the logger capablities here - * if you want so in the future. + * Structured JSON logger backed by pino. + * Every log line is a JSON object — ideal for ingestion by Loki / Grafana. */ import pino from "pino"; +import { trace, context } from "@opentelemetry/api"; import config from "./config.ts"; +// --------------------------------------------------------------------------- +// Base logger +// --------------------------------------------------------------------------- + export const logger = pino({ - level: config.logLevel, + level: config.log.level ?? "info", + // Always emit JSON — Loki can parse this directly. ...(process.env["NODE_ENV"] === "development" && { transport: { target: "pino-pretty", options: { colorize: true } }, }), + formatters: { + level(label) { + return { level: label }; + }, + bindings(bindings) { + return { + pid: bindings["pid"] as string, + host: bindings["hostname"] as string, + service: config.app.name, + }; + }, + }, timestamp: pino.stdTimeFunctions.isoTime, }); + +// --------------------------------------------------------------------------- +// Helpers to extract the current OTel trace/span IDs +// --------------------------------------------------------------------------- + +function getTraceContext(): { trace_id: string; span_id: string } { + const span = trace.getSpan(context.active()); + if (!span) return { trace_id: "", span_id: "" }; + const ctx = span.spanContext(); + return { trace_id: ctx.traceId, span_id: ctx.spanId }; +} + +// --------------------------------------------------------------------------- +// Convenience: log an error with full details + trace context +// --------------------------------------------------------------------------- + +export function logError( + msg: string, + err: unknown, + extra?: Record, +): void { + const traceCtx = getTraceContext(); + if (err instanceof Error) { + logger.error( + { + ...traceCtx, + err: { message: err.message, stack: err.stack, name: err.name }, + ...extra, + }, + msg, + ); + } else { + logger.error({ ...traceCtx, err, ...extra }, msg); + } +} diff --git a/src/metrics.ts b/src/metrics.ts new file mode 100644 index 0000000..e3289d1 --- /dev/null +++ b/src/metrics.ts @@ -0,0 +1,88 @@ +/** + * Prometheus metrics definitions. + * + * All metrics are created here and exported for use in route handlers and + * middleware. The default prom-client registry is used so that the built-in + * Node.js / process metrics are collected automatically. + */ + +import type { RequestHandler } from "express"; +import { + Registry, + collectDefaultMetrics, + Counter, + Histogram, + Gauge, +} from "prom-client"; + +// --------------------------------------------------------------------------- +// Registry +// --------------------------------------------------------------------------- + +export const register = new Registry(); + +// Collect default Node.js metrics (event loop lag, GC, memory, etc.) +collectDefaultMetrics({ register }); + +// --------------------------------------------------------------------------- +// HTTP metrics +// --------------------------------------------------------------------------- + +/** + * Total number of HTTP requests completed, labelled by method, route, and + * status code. Use the normalised route pattern (e.g. /users) not the raw + * URL so high-cardinality IDs don't explode the metric. + */ +export const httpRequestsTotal = new Counter({ + name: "http_requests_total", + help: "Total number of HTTP requests", + labelNames: ["method", "route", "status_code"] as const, + registers: [register], +}); + +/** + * Histogram of HTTP request durations in seconds. + * Buckets cover the range expected for this demo app (1 ms → 5 s). + */ +export const httpRequestDurationSeconds = new Histogram({ + name: "http_request_duration_seconds", + help: "HTTP request duration in seconds", + labelNames: ["method", "route"] as const, + buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5], + registers: [register], +}); + +/** + * Number of HTTP requests currently being processed (in-flight gauge). + */ +export const activeRequests = new Gauge({ + name: "active_requests", + help: "Number of HTTP requests currently being processed", + registers: [register], +}); + +// --------------------------------------------------------------------------- +// Business metrics +// --------------------------------------------------------------------------- + +// EXAMPLE: Custom business metric: total checkout attempts, labelled by outcome. +// Labels: status = "success" | "failure" +// export const checkoutsTotal = new Counter({ +// name: "checkouts_total", +// help: "Total number of checkout attempts", +// labelNames: ["status"] as const, +// registers: [register], +// }); + +// --------------------------------------------------------------------------- +// Handler for metrics +// --------------------------------------------------------------------------- + +/** + * Prometheus can collect metrics from this endpoint. + */ +export const metricsHandler: RequestHandler = async (_req, res) => { + res.set("Content-Type", register.contentType); + const metrics = await register.metrics(); + res.end(metrics); +}; diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index 8726740..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { - Request, - Response, - NextFunction, - ErrorRequestHandler, -} from "express"; -import { z, ZodError } from "zod"; -import { StatusCodes } from "http-status-codes"; -import { DatabaseError } from "pg"; -import { logger } from "./logger.ts"; - -// PostgreSQL error codes -// https://www.postgresql.org/docs/current/errcodes-appendix.html -const PG_ERROR_CODES = { - NOT_NULL_VIOLATION: "23502", - FOREIGN_KEY_VIOLATION: "23503", - UNIQUE_VIOLATION: "23505", - CHECK_VIOLATION: "23514", -} as const; - -/** - * Generic middleware to validate request data against a Zod schema. - */ -export function validateData(schema: z.ZodType) { - return (req: Request, _res: Response, next: NextFunction) => { - schema.parse(req.body); - next(); - }; -} - -/** - * Centralized error handler. - * - * Express 5 automatically forwards thrown errors (except in async callbacks) - * and rejected Promises to the next function. - */ -export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => { - if (error instanceof DatabaseError) { - switch (error.code) { - case PG_ERROR_CODES.UNIQUE_VIOLATION: - return res.status(StatusCodes.CONFLICT).json({ - error: "Conflict", - message: "A record with the given data already exists.", - }); - - case PG_ERROR_CODES.FOREIGN_KEY_VIOLATION: - return res.status(StatusCodes.UNPROCESSABLE_ENTITY).json({ - error: "Unprocessable Entity", - message: "Referenced record does not exist.", - }); - - case PG_ERROR_CODES.NOT_NULL_VIOLATION: - return res.status(StatusCodes.BAD_REQUEST).json({ - error: "Bad Request", - message: "A required field is missing.", - }); - - case PG_ERROR_CODES.CHECK_VIOLATION: - return res.status(StatusCodes.BAD_REQUEST).json({ - error: "Bad Request", - message: "A field value failed a database constraint check.", - }); - - default: - logger.error(error); - return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: "Internal Server Error", - message: "An unexpected database error occurred.", - }); - } - } - - if (error instanceof ZodError) { - const details = error.issues.map((issue) => ({ - message: `${issue.path.join(".")} is ${issue.message}`, - })); - - return res - .status(StatusCodes.BAD_REQUEST) - .json({ error: "Bad Request", details }); - } - - logger.error(error); - - return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: "Internal Server Error", - message: "Internal server error", - }); -}; diff --git a/src/middleware/error-handler.ts b/src/middleware/error-handler.ts new file mode 100644 index 0000000..12cbdba --- /dev/null +++ b/src/middleware/error-handler.ts @@ -0,0 +1,102 @@ +import type { ErrorRequestHandler } from "express"; +import { ZodError } from "zod"; +import { getReasonPhrase, StatusCodes } from "http-status-codes"; +import { DatabaseError } from "pg"; +import { PG_ERROR_CODES } from "../utils/constants.ts"; +import { ApiError } from "../errors.ts"; +import type { WideEventError } from "../types.ts"; + +/** + * Centralized error handler. + * + * Express 5 automatically forwards thrown errors (except in async callbacks) + * and rejected Promises to the next function. + */ +export const errorHandler: ErrorRequestHandler = (error, req, res, _next) => { + if (error instanceof Error) { + req.event.error = { + ...error, + level: "warn", + }; + + if (error instanceof ApiError) { + const handledError = handleApiError(error); + if (handledError) req.event.error = handledError; + + return res.status(error.statusCode).json({ + error: getReasonPhrase(error.statusCode), + message: error.message, + }); + } + + if (error instanceof DatabaseError) { + switch (error.code) { + case PG_ERROR_CODES.UNIQUE_VIOLATION: + return res.status(StatusCodes.CONFLICT).json({ + error: "Conflict", + message: "A record with the given data already exists.", + }); + + case PG_ERROR_CODES.FOREIGN_KEY_VIOLATION: + return res.status(StatusCodes.UNPROCESSABLE_ENTITY).json({ + error: "Unprocessable Entity", + message: "Referenced record does not exist.", + }); + + case PG_ERROR_CODES.NOT_NULL_VIOLATION: + return res.status(StatusCodes.BAD_REQUEST).json({ + error: "Bad Request", + message: "A required field is missing.", + }); + + case PG_ERROR_CODES.CHECK_VIOLATION: + return res.status(StatusCodes.BAD_REQUEST).json({ + error: "Bad Request", + message: "A field value failed a database constraint check.", + }); + + default: + req.event.error.level = "error"; + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: "Internal Server Error", + message: "Internal server error.", + }); + } + } + + if (error instanceof ZodError) { + const details = error.issues.map((issue) => ({ + path: issue.path.join("."), + message: issue.message, + })); + + return res + .status(StatusCodes.BAD_REQUEST) + .json({ error: "Bad Request", details }); + } + } + + req.event.error = { + ...req.event.error, + name: "Unknown Error", + message: String(error), + level: "error", + }; + + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: "Internal Server Error", + message: "Internal server error.", + }); +}; + +const handleApiError = (error: ApiError): WideEventError | undefined => { + if (error.log.enabled) { + return { + ...error, + message: error.log.message, + level: error.log.level, + }; + } + + return undefined; +}; diff --git a/src/middleware/metrics-handler.ts b/src/middleware/metrics-handler.ts new file mode 100644 index 0000000..f3c8839 --- /dev/null +++ b/src/middleware/metrics-handler.ts @@ -0,0 +1,40 @@ +import type { RequestHandler } from "express"; +import { + activeRequests, + httpRequestDurationSeconds, + httpRequestsTotal, +} from "../metrics.ts"; +import type { ExpressRoute } from "../types.ts"; + +export const metricsCollectorHandler: RequestHandler = (req, res, next) => { + if (req.path === "/metrics") return next(); + + activeRequests.inc(); + + const started = process.hrtime.bigint(); + + res.on("finish", () => { + activeRequests.dec(); + + // Hrtime might be more precise (nanoseconds) than performance.now() + const durationSeconds = + Number(process.hrtime.bigint() - started) / 1_000_000_000; + + const routeRecord = req.route as ExpressRoute; + const route = + typeof routeRecord?.path === "string" ? routeRecord.path : "unknown"; + + httpRequestDurationSeconds.observe( + { method: req.method, route }, + durationSeconds, + ); + + httpRequestsTotal.inc({ + method: req.method, + route, + status_code: String(res.statusCode), + }); + }); + + next(); +}; diff --git a/src/middleware/rate-limiter.ts b/src/middleware/rate-limiter.ts new file mode 100644 index 0000000..8556e86 --- /dev/null +++ b/src/middleware/rate-limiter.ts @@ -0,0 +1,15 @@ +import type { Options, RateLimitRequestHandler } from "express-rate-limit"; + +import { rateLimit } from "express-rate-limit"; + +const options: Partial = { + windowMs: 1 * 60 * 1000, + limit: 100, + message: { + message: "Too many requests from this IP, please try again after 1 minute.", + }, + standardHeaders: "draft-8", + legacyHeaders: false, +}; + +export const rateLimiter: RateLimitRequestHandler = rateLimit(options); diff --git a/src/middleware/trace-handler.ts b/src/middleware/trace-handler.ts new file mode 100644 index 0000000..bd21a52 --- /dev/null +++ b/src/middleware/trace-handler.ts @@ -0,0 +1,54 @@ +import type { RequestHandler } from "express"; +import { context, SpanStatusCode, trace } from "@opentelemetry/api"; +import config from "../config.ts"; +import type { ExpressRoute } from "../types.ts"; + +const tracer = trace.getTracer(config.app.name); + +/** + * Middleware that sets up traces for incoming requests using OpenTelemetry. + * Use after the wideEvent middleware. + * + * Set attributes you want to include in every trace here. + * + * To access and modify the trace context later down the chain: + * + * ```ts + * import { trace } from "@opentelemetry/api"; + * + * const span = trace.getActiveSpan(); + * + * span?.setAttribute("event.id", id); + * ``` + */ +export const traceHandler: RequestHandler = (req, res, next) => { + const span = tracer.startSpan(`${req.method} ${req.path}`); + const ctx = trace.setSpan(context.active(), span); + + const spanContext = span.spanContext(); + + span.setAttribute("req.id", req.event.request_id); + span.setAttribute("req.method", req.method); + + const routePath = (req.route as ExpressRoute | undefined)?.path; + if (typeof routePath === "string") { + span.setAttribute("http.route", routePath); + } + + req.event.trace_id = spanContext.traceId; + req.event.span_id = spanContext.spanId; + + res.on("finish", () => { + span.setAttribute("http.status_code", res.statusCode); + + if (res.statusCode >= 500) { + span.setStatus({ code: SpanStatusCode.ERROR }); + } else { + span.setStatus({ code: SpanStatusCode.OK }); + } + + span.end(); + }); + + context.with(ctx, next); +}; diff --git a/src/middleware/validate-data.ts b/src/middleware/validate-data.ts new file mode 100644 index 0000000..8dc4fcb --- /dev/null +++ b/src/middleware/validate-data.ts @@ -0,0 +1,12 @@ +import type { Request, Response, NextFunction } from "express"; +import { z } from "zod"; + +/** + * Generic middleware to validate request data against a Zod schema. + */ +export const validateData = (schema: z.ZodType) => { + return (req: Request, _res: Response, next: NextFunction) => { + schema.parse(req.body); + next(); + }; +}; diff --git a/src/middleware/wide-event.ts b/src/middleware/wide-event.ts new file mode 100644 index 0000000..bc7df9b --- /dev/null +++ b/src/middleware/wide-event.ts @@ -0,0 +1,99 @@ +import type { NextFunction, Request, Response } from "express"; +import type { ExpressRoute, WideEvent } from "../types.ts"; +import { logger } from "../logger.ts"; +import config from "../config.ts"; + +const SAMPLED_ERROR_LEVELS = ["error", "fatal"]; + +/** + * Wide events only log a single big object at the end of each request. + * Each wide event contains all the desired information about the request. + * Good for keeping the request context together, reducing log noise. + * + * When you want to log something, you should manually add relevant context to the event object, as + * requests travel through the middleware chain. + * + * ```ts + * req.event.somethingToLog = aVariable; + * ``` + * + * Read more here https://loggingsucks.com/ + */ +export const wideEventHandler = ( + req: Request, + res: Response, + next: NextFunction, +) => { + const started = performance.now(); + + // Initialize the wide event with request context + req.event = { + event: "http_request", + request_id: crypto.randomUUID(), + method: req.method, + url: req.originalUrl, + }; + + // We only log at the end of each request (wide-event philosophy). + res.on("finish", () => { + req.event.status_code = res.statusCode; + req.event.duration_ms = Math.round(performance.now() - started); + + const routePath = (req.route as ExpressRoute)?.path; + if (typeof routePath === "string") { + req.event.route = routePath; + } + + if (shouldSample(req.event)) { + if (req.event.error) { + switch (req.event.error.level) { + case "fatal": + logger.fatal(req.event); + break; + case "error": + logger.error(req.event); + break; + case "warn": + logger.warn(req.event); + break; + case "debug": + logger.debug(req.event); + break; + } + } else { + logger.info(req.event); + } + } + }); + + next(); +}; + +/** + * Tail sampling decision function. Configure this + * to achieve the level of cardinality you are looking for + * (the more unique - higher cardinality - the log is, the better most often). + */ +const shouldSample = (event: WideEvent): boolean => { + // Always keep 500 errors + if (event.status_code && event.status_code >= 500) return true; + + if (SAMPLED_ERROR_LEVELS.some((level) => event.error?.level === level)) { + return true; + } + + if (event.duration_ms !== undefined && event.duration_ms > 2000) { + // Always keep slow requests (above p99) + return true; + } + + // EXAMPLES: + // Always keep VIP users + //if (event["user"]?.["subscription"] === "enterprise") return true; + + // Always keep requests with specific feature flags (debugging rollouts) + //if (event["feature_flags"]?.["new_vote_flow"]) return true; + + // Random sample the rest as defined in config (default 5%) + return Math.random() < config.log.samplingRate; +}; diff --git a/src/shutdown.ts b/src/shutdown.ts new file mode 100644 index 0000000..eed7892 --- /dev/null +++ b/src/shutdown.ts @@ -0,0 +1,86 @@ +import type { Server } from "node:http"; +import { logger } from "./logger.ts"; +import sdk from "./utils/tracing.ts"; +import { getDb } from "./database/postgres.ts"; +import timeout from "./utils/timeout.ts"; + +// Tracks whether the shutdown process is currently in progress. +// Prevents multiple shutdown attempts from being initiated simultaneously. +let shuttingDown = false; + +/** + * Initiates a graceful shutdown of the server, flushing logs, closing database connections, and shutting down tracing. + * + * @param server - The HTTP server to shut down. + * @param exitCode - The exit code to use when shutting down. See https://nodejs.org/api/process.html#exit-codes + */ +export const shutdown = async (server: Server, exitCode = 0) => { + if (shuttingDown) return; + shuttingDown = true; + + logger.info("Shutdown signal received"); + + // Graceful shutdown sequence. + // You should flush your logs and close db connections here. + try { + logger.info("Shutting down HTTP server..."); + // Wait for the server to close, or timeout after 60 seconds. + await Promise.race([ + closeServer(server), + timeout(60_000, "Server close timeout"), + ]); + + logger.info("Destroying db connection instance..."); + // DB might log things during shutdown + // so better to destroy it before shutting down sdk. + await Promise.race([ + getDb().destroy(), + timeout(60_000, "DB destroy timeout"), + ]); + + logger.info("Shutting down OTEL SDK..."); + await Promise.race([ + sdk.shutdown(), + timeout(60_000, "OTEL SDK shutdown timeout"), + ]); + + logger.info("Shutdown complete"); + + logger.flush(); + + // Let Node handle exit during normal shutdown. + process.exitCode = exitCode; + + // Force exit if node hangs (for 5 seconds). + // Notice unref() - the timer will not keep the process alive. + setTimeout(() => { + process.exit(exitCode); + }, 5000).unref(); + } catch (error) { + logger.fatal({ error }, "Error during shutdown"); + + logger.flush(); + + process.exitCode = 1; + + setTimeout(() => { + process.exit(1); + }, 5000).unref(); + } +}; + +/** + * Closes the server and returns a promise that resolves when it's closed. + */ +const closeServer = (server: Server): Promise => { + return new Promise((resolve, reject) => { + server.close((err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..4227052 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,32 @@ +export type WideEventError = { + name: string; + message: string; + level: "error" | "warn" | "debug" | "fatal"; + params?: Record | undefined; + stack?: string | undefined; +}; + +/** + * WideEvent object. + * + * Snake case is used since logs are consumed outside nodejs. + */ +export type WideEvent = { + event: string; + request_id: string; + method: string; + url: string; + trace_id?: string; + span_id?: string; + route?: string; + status_code?: number; + duration_ms?: number; + user_id?: string; + error?: WideEventError; +}; + +export type ExpressRoute = + | { + path?: string; + } + | undefined; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..a378237 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,14 @@ +// PostgreSQL error codes +// https://www.postgresql.org/docs/current/errcodes-appendix.html +export const PG_ERROR_CODES = { + NOT_NULL_VIOLATION: "23502", + FOREIGN_KEY_VIOLATION: "23503", + UNIQUE_VIOLATION: "23505", + CHECK_VIOLATION: "23514", +} as const; + +/** Only include signals that warrant graceful shutdown. + * SIGSEGV, SIGILL, SIGBUS, SIGABRT are examples of signals + * that benefit from loud a shutdown. + */ +export const SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"] as const; diff --git a/src/utils/network.ts b/src/utils/network.ts new file mode 100644 index 0000000..e5dacc4 --- /dev/null +++ b/src/utils/network.ts @@ -0,0 +1,10 @@ +import os from "node:os"; + +export const getIPv4Addresses = (): Array => { + const interfaces = os.networkInterfaces(); + + return Object.values(interfaces) + .flat() + .filter((net) => net?.family === "IPv4") + .map(({ address }) => address); +}; diff --git a/src/utils/timeout.ts b/src/utils/timeout.ts new file mode 100644 index 0000000..21e3c9e --- /dev/null +++ b/src/utils/timeout.ts @@ -0,0 +1,15 @@ +/** + * Wait for a specified amount of time, before rejecting with an error. + * + * **Does not block process.exit.** + * + * @param ms The number of milliseconds to wait before rejecting. + * @param message The error message to reject with. + * @returns A promise that rejects after the specified time. + */ +const timeout = (ms: number, message: string) => + new Promise((_, reject) => + setTimeout(() => reject(new Error(message)), ms).unref(), + ); + +export default timeout; diff --git a/src/utils/tracing.ts b/src/utils/tracing.ts new file mode 100644 index 0000000..e78d548 --- /dev/null +++ b/src/utils/tracing.ts @@ -0,0 +1,47 @@ +/** + * OpenTelemetry tracing setup. + * This file MUST be imported at the very top of index.ts before any other imports + * so that auto-instrumentation patches are applied before libraries are loaded. + */ + +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, +} from "@opentelemetry/semantic-conventions"; +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; +import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node"; +import config from "../config.ts"; + +const OTLP_ENDPOINT = config.otel.otlpEndpoint; + +const exporter = new OTLPTraceExporter({ + url: `${OTLP_ENDPOINT}/v1/traces`, + headers: {}, +}); + +const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: config.otel.serviceName, + [ATTR_SERVICE_VERSION]: config.npmPackageVersion, + "deployment.environment": config.app.environment, +}); + +const sdk = new NodeSDK({ + resource, + spanProcessors: [new BatchSpanProcessor(exporter)], + instrumentations: [ + getNodeAutoInstrumentations({ + // Reduce noise from fs polling; keep HTTP, Express, pg + "@opentelemetry/instrumentation-fs": { enabled: false }, + "@opentelemetry/instrumentation-http": { enabled: true }, + "@opentelemetry/instrumentation-express": { enabled: true }, + "@opentelemetry/instrumentation-pg": { enabled: true }, + }), + ], +}); + +sdk.start(); + +export default sdk; diff --git a/tsconfig.app.json b/tsconfig.app.json index 79a6d5b..9b1b29a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -11,7 +11,7 @@ "target": "esnext", // For nodejs: "lib": ["esnext"], - "types": ["node"], + "noEmit": true, "composite": true, "allowImportingTsExtensions": true, @@ -41,5 +41,5 @@ "moduleDetection": "force", "skipLibCheck": true }, - "include": ["src"] + "include": ["src/**/*"] } diff --git a/wiki/OBSERVABILITY.md b/wiki/OBSERVABILITY.md new file mode 100644 index 0000000..4f795c0 --- /dev/null +++ b/wiki/OBSERVABILITY.md @@ -0,0 +1,10 @@ +# Mental model for all the data collected + +Traces: +OpenTelemetry SDK → OTLP exporter → Tempo / Grafana Cloud Traces + +Logs: +Pino wide events → stdout → Alloy/Promtail/Fluent Bit → Loki / Grafana Cloud Logs + +Metrics: +prom-client / OTel metrics → Prometheus / Grafana Cloud Metrics