mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat(server): add OpenTelemetry tracing and metrics support
Adds comprehensive OpenTelemetry instrumentation for better observability: Tracing: - HTTP requests via @opentelemetry/instrumentation-http - NestJS controllers via @opentelemetry/instrumentation-nestjs-core - Redis operations via @opentelemetry/instrumentation-ioredis - BullMQ job queues via bullmq-otel - Database queries via Kysely log callback with accurate timing - File operations (send, stream, read) - Media processing (thumbnails, thumbhash, EXIF operations) - Metadata extraction Metrics: - Database connection pool usage (used/idle connections) The SDK is initialized in telemetry-preload.ts which is preloaded via --require before the main app starts. This ensures http instrumentation hooks are in place before any http module is imported. Enable tracing by setting OTEL_EXPORTER_OTLP_ENDPOINT environment variable. Also adds TraceContext to IBaseJob for future distributed trace propagation across BullMQ job boundaries.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Generated
+423
-56
@@ -373,9 +373,15 @@ importers:
|
|||||||
'@opentelemetry/context-async-hooks':
|
'@opentelemetry/context-async-hooks':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.4.0(@opentelemetry/api@1.9.0)
|
version: 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/exporter-metrics-otlp-http':
|
||||||
|
specifier: ^0.211.0
|
||||||
|
version: 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/exporter-prometheus':
|
'@opentelemetry/exporter-prometheus':
|
||||||
specifier: ^0.210.0
|
specifier: ^0.210.0
|
||||||
version: 0.210.0(@opentelemetry/api@1.9.0)
|
version: 0.210.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/exporter-trace-otlp-http':
|
||||||
|
specifier: ^0.211.0
|
||||||
|
version: 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/instrumentation-http':
|
'@opentelemetry/instrumentation-http':
|
||||||
specifier: ^0.210.0
|
specifier: ^0.210.0
|
||||||
version: 0.210.0(@opentelemetry/api@1.9.0)
|
version: 0.210.0(@opentelemetry/api@1.9.0)
|
||||||
@@ -385,9 +391,6 @@ importers:
|
|||||||
'@opentelemetry/instrumentation-nestjs-core':
|
'@opentelemetry/instrumentation-nestjs-core':
|
||||||
specifier: ^0.56.0
|
specifier: ^0.56.0
|
||||||
version: 0.56.0(@opentelemetry/api@1.9.0)
|
version: 0.56.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/instrumentation-pg':
|
|
||||||
specifier: ^0.62.0
|
|
||||||
version: 0.62.0(@opentelemetry/api@1.9.0)
|
|
||||||
'@opentelemetry/resources':
|
'@opentelemetry/resources':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.4.0(@opentelemetry/api@1.9.0)
|
version: 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
@@ -397,6 +400,9 @@ importers:
|
|||||||
'@opentelemetry/sdk-node':
|
'@opentelemetry/sdk-node':
|
||||||
specifier: ^0.210.0
|
specifier: ^0.210.0
|
||||||
version: 0.210.0(@opentelemetry/api@1.9.0)
|
version: 0.210.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/sdk-trace-base':
|
||||||
|
specifier: ^2.5.0
|
||||||
|
version: 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/semantic-conventions':
|
'@opentelemetry/semantic-conventions':
|
||||||
specifier: ^1.34.0
|
specifier: ^1.34.0
|
||||||
version: 1.38.0
|
version: 1.38.0
|
||||||
@@ -427,6 +433,9 @@ importers:
|
|||||||
bullmq:
|
bullmq:
|
||||||
specifier: ^5.51.0
|
specifier: ^5.51.0
|
||||||
version: 5.66.5
|
version: 5.66.5
|
||||||
|
bullmq-otel:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^4.0.3
|
specifier: ^4.0.3
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
@@ -670,19 +679,19 @@ importers:
|
|||||||
version: 13.15.10
|
version: 13.15.10
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.14.0
|
specifier: ^9.14.0
|
||||||
version: 9.39.2(jiti@2.6.1)
|
version: 9.39.2(jiti@1.21.7)
|
||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
specifier: ^10.1.8
|
specifier: ^10.1.8
|
||||||
version: 10.1.8(eslint@9.39.2(jiti@2.6.1))
|
version: 10.1.8(eslint@9.39.2(jiti@1.21.7))
|
||||||
eslint-plugin-prettier:
|
eslint-plugin-prettier:
|
||||||
specifier: ^5.1.3
|
specifier: ^5.1.3
|
||||||
version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0)
|
version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(prettier@3.8.0)
|
||||||
eslint-plugin-unicorn:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^62.0.0
|
specifier: ^62.0.0
|
||||||
version: 62.0.0(eslint@9.39.2(jiti@2.6.1))
|
version: 62.0.0(eslint@9.39.2(jiti@1.21.7))
|
||||||
globals:
|
globals:
|
||||||
specifier: ^16.0.0
|
specifier: ^16.0.0
|
||||||
version: 16.5.0
|
version: 16.5.0
|
||||||
@@ -718,16 +727,16 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.28.0
|
specifier: ^8.28.0
|
||||||
version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
unplugin-swc:
|
unplugin-swc:
|
||||||
specifier: ^1.4.5
|
specifier: ^1.4.5
|
||||||
version: 1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.55.1)
|
version: 1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.55.1)
|
||||||
vite-tsconfig-paths:
|
vite-tsconfig-paths:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
web:
|
web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3839,6 +3848,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==}
|
resolution: {integrity: sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
'@opentelemetry/api-logs@0.211.0':
|
||||||
|
resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0':
|
'@opentelemetry/api@1.9.0':
|
||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -3861,6 +3874,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||||
|
|
||||||
|
'@opentelemetry/core@2.5.0':
|
||||||
|
resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||||
|
|
||||||
'@opentelemetry/exporter-logs-otlp-grpc@0.210.0':
|
'@opentelemetry/exporter-logs-otlp-grpc@0.210.0':
|
||||||
resolution: {integrity: sha512-+BolenqOO6ow65go7uWRYPvvs/BBIWp1mtRn93VvGduqvMVH/IY8nXrt80a4L9hZ7lHi2Tq2/NcC3H2QzcWKag==}
|
resolution: {integrity: sha512-+BolenqOO6ow65go7uWRYPvvs/BBIWp1mtRn93VvGduqvMVH/IY8nXrt80a4L9hZ7lHi2Tq2/NcC3H2QzcWKag==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -3891,6 +3910,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
|
'@opentelemetry/exporter-metrics-otlp-http@0.211.0':
|
||||||
|
resolution: {integrity: sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
'@opentelemetry/exporter-metrics-otlp-proto@0.210.0':
|
'@opentelemetry/exporter-metrics-otlp-proto@0.210.0':
|
||||||
resolution: {integrity: sha512-CFa7SOinYOVWIWJuQL7XFeyedzmFGIpHpSMNFE8Xefb6iGB4m+MukQecdssvPcJKYlfF5FpovEOLXwafAzsXWQ==}
|
resolution: {integrity: sha512-CFa7SOinYOVWIWJuQL7XFeyedzmFGIpHpSMNFE8Xefb6iGB4m+MukQecdssvPcJKYlfF5FpovEOLXwafAzsXWQ==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -3915,6 +3940,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
|
'@opentelemetry/exporter-trace-otlp-http@0.211.0':
|
||||||
|
resolution: {integrity: sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
'@opentelemetry/exporter-trace-otlp-proto@0.210.0':
|
'@opentelemetry/exporter-trace-otlp-proto@0.210.0':
|
||||||
resolution: {integrity: sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg==}
|
resolution: {integrity: sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -3951,12 +3982,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
'@opentelemetry/instrumentation-pg@0.62.0':
|
|
||||||
resolution: {integrity: sha512-/ZSMRCyFRMjQVx7Wf+BIAOMEdN/XWBbAGTNLKfQgGYs1GlmdiIFkUy8Z8XGkToMpKrgZju0drlTQpqt4Ul7R6w==}
|
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
|
||||||
peerDependencies:
|
|
||||||
'@opentelemetry/api': ^1.3.0
|
|
||||||
|
|
||||||
'@opentelemetry/instrumentation@0.210.0':
|
'@opentelemetry/instrumentation@0.210.0':
|
||||||
resolution: {integrity: sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g==}
|
resolution: {integrity: sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -3969,6 +3994,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
|
'@opentelemetry/otlp-exporter-base@0.211.0':
|
||||||
|
resolution: {integrity: sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
'@opentelemetry/otlp-grpc-exporter-base@0.210.0':
|
'@opentelemetry/otlp-grpc-exporter-base@0.210.0':
|
||||||
resolution: {integrity: sha512-fEJs8UhkFMrdXMOCLXyKd2uc6N209tIi8IBNqSTi83ri+MlMFrBKnOtklmv9/zzxovoN5zD1waRt6XBFGPfmIw==}
|
resolution: {integrity: sha512-fEJs8UhkFMrdXMOCLXyKd2uc6N209tIi8IBNqSTi83ri+MlMFrBKnOtklmv9/zzxovoN5zD1waRt6XBFGPfmIw==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -3981,6 +4012,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': ^1.3.0
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
|
'@opentelemetry/otlp-transformer@0.211.0':
|
||||||
|
resolution: {integrity: sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
|
||||||
'@opentelemetry/propagator-b3@2.4.0':
|
'@opentelemetry/propagator-b3@2.4.0':
|
||||||
resolution: {integrity: sha512-6VPsFiMUkJBre/86F0d+PZMaUCcuLA9DtZuC46KH8EeVEKZPEM2WlX35M/qmde8UpzoQL9qzdz54YjUYABt8Uw==}
|
resolution: {integrity: sha512-6VPsFiMUkJBre/86F0d+PZMaUCcuLA9DtZuC46KH8EeVEKZPEM2WlX35M/qmde8UpzoQL9qzdz54YjUYABt8Uw==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -4003,18 +4040,36 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
|
||||||
|
'@opentelemetry/resources@2.5.0':
|
||||||
|
resolution: {integrity: sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
|
||||||
'@opentelemetry/sdk-logs@0.210.0':
|
'@opentelemetry/sdk-logs@0.210.0':
|
||||||
resolution: {integrity: sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ==}
|
resolution: {integrity: sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
||||||
|
|
||||||
|
'@opentelemetry/sdk-logs@0.211.0':
|
||||||
|
resolution: {integrity: sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
||||||
|
|
||||||
'@opentelemetry/sdk-metrics@2.4.0':
|
'@opentelemetry/sdk-metrics@2.4.0':
|
||||||
resolution: {integrity: sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==}
|
resolution: {integrity: sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||||
|
|
||||||
|
'@opentelemetry/sdk-metrics@2.5.0':
|
||||||
|
resolution: {integrity: sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||||
|
|
||||||
'@opentelemetry/sdk-node@0.210.0':
|
'@opentelemetry/sdk-node@0.210.0':
|
||||||
resolution: {integrity: sha512-KymqUtYvfpblDNgGxBXYqCcDjYXwjOF7Muc6ocs0rMlG/66Hcs9KiJ7hg4zLOv63JubF/vxi5WXaLrQrPKyaZQ==}
|
resolution: {integrity: sha512-KymqUtYvfpblDNgGxBXYqCcDjYXwjOF7Muc6ocs0rMlG/66Hcs9KiJ7hg4zLOv63JubF/vxi5WXaLrQrPKyaZQ==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -4027,6 +4082,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
|
||||||
|
'@opentelemetry/sdk-trace-base@2.5.0':
|
||||||
|
resolution: {integrity: sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
|
||||||
'@opentelemetry/sdk-trace-node@2.4.0':
|
'@opentelemetry/sdk-trace-node@2.4.0':
|
||||||
resolution: {integrity: sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==}
|
resolution: {integrity: sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
@@ -4037,12 +4098,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==}
|
resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==}
|
||||||
engines: {node: '>=14'}
|
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
|
|
||||||
|
|
||||||
'@paralleldrive/cuid2@2.3.1':
|
'@paralleldrive/cuid2@2.3.1':
|
||||||
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
||||||
|
|
||||||
@@ -5403,12 +5458,6 @@ packages:
|
|||||||
'@types/parse5@5.0.3':
|
'@types/parse5@5.0.3':
|
||||||
resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==}
|
resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==}
|
||||||
|
|
||||||
'@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.16.0':
|
'@types/pg@8.16.0':
|
||||||
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
||||||
|
|
||||||
@@ -6130,6 +6179,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==}
|
resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==}
|
||||||
engines: {node: '>=18.20'}
|
engines: {node: '>=18.20'}
|
||||||
|
|
||||||
|
bullmq-otel@1.1.0:
|
||||||
|
resolution: {integrity: sha512-8tHl/NkZghp+YLSVvqBtB1Gox/hilNFVG4Or2NmBJQilxbC+nhfgl4+lG5QB0C3bEfLwAvBQjyDNAnGe0i5vHA==}
|
||||||
|
|
||||||
bullmq@5.66.5:
|
bullmq@5.66.5:
|
||||||
resolution: {integrity: sha512-DC1E7P03L+TfNHv+2SGxwNYvtb0oJPODWSKkWdfis0heU5zFW16vjM7fCjwlxMdGWw2w28EI3mTRfYLEHeQQSw==}
|
resolution: {integrity: sha512-DC1E7P03L+TfNHv+2SGxwNYvtb0oJPODWSKkWdfis0heU5zFW16vjM7fCjwlxMdGWw2w28EI3mTRfYLEHeQQSw==}
|
||||||
|
|
||||||
@@ -15463,6 +15515,11 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.2':
|
'@esbuild/win32-x64@0.27.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@1.21.7))':
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))':
|
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
@@ -16529,6 +16586,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
|
|
||||||
|
'@opentelemetry/api-logs@0.211.0':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
'@opentelemetry/configuration@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/configuration@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
@@ -16546,6 +16607,11 @@ snapshots:
|
|||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/semantic-conventions': 1.38.0
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
|
|
||||||
|
'@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
|
|
||||||
'@opentelemetry/exporter-logs-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/exporter-logs-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@grpc/grpc-js': 1.14.3
|
'@grpc/grpc-js': 1.14.3
|
||||||
@@ -16597,6 +16663,15 @@ snapshots:
|
|||||||
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
|
'@opentelemetry/exporter-metrics-otlp-http@0.211.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
'@opentelemetry/exporter-metrics-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/exporter-metrics-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
@@ -16634,6 +16709,15 @@ snapshots:
|
|||||||
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
|
'@opentelemetry/exporter-trace-otlp-http@0.211.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
'@opentelemetry/exporter-trace-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/exporter-trace-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
@@ -16683,18 +16767,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@opentelemetry/instrumentation-pg@0.62.0(@opentelemetry/api@1.9.0)':
|
|
||||||
dependencies:
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
|
||||||
'@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0)
|
|
||||||
'@opentelemetry/semantic-conventions': 1.38.0
|
|
||||||
'@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0)
|
|
||||||
'@types/pg': 8.15.6
|
|
||||||
'@types/pg-pool': 2.0.7
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
@@ -16710,6 +16782,12 @@ snapshots:
|
|||||||
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
|
'@opentelemetry/otlp-exporter-base@0.211.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
'@opentelemetry/otlp-grpc-exporter-base@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/otlp-grpc-exporter-base@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@grpc/grpc-js': 1.14.3
|
'@grpc/grpc-js': 1.14.3
|
||||||
@@ -16729,6 +16807,17 @@ snapshots:
|
|||||||
'@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
protobufjs: 8.0.0
|
protobufjs: 8.0.0
|
||||||
|
|
||||||
|
'@opentelemetry/otlp-transformer@0.211.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/api-logs': 0.211.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
protobufjs: 8.0.0
|
||||||
|
|
||||||
'@opentelemetry/propagator-b3@2.4.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/propagator-b3@2.4.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
@@ -16747,6 +16836,12 @@ snapshots:
|
|||||||
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/semantic-conventions': 1.38.0
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
|
|
||||||
|
'@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
|
|
||||||
'@opentelemetry/sdk-logs@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/sdk-logs@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
@@ -16754,12 +16849,25 @@ snapshots:
|
|||||||
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
|
'@opentelemetry/sdk-logs@0.211.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/api-logs': 0.211.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
'@opentelemetry/sdk-metrics@2.4.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/sdk-metrics@2.4.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
|
'@opentelemetry/sdk-metrics@2.5.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
|
||||||
'@opentelemetry/sdk-node@0.210.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/sdk-node@0.210.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
@@ -16797,6 +16905,13 @@ snapshots:
|
|||||||
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
'@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0)
|
||||||
'@opentelemetry/semantic-conventions': 1.38.0
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
|
|
||||||
|
'@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0)':
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
|
|
||||||
'@opentelemetry/sdk-trace-node@2.4.0(@opentelemetry/api@1.9.0)':
|
'@opentelemetry/sdk-trace-node@2.4.0(@opentelemetry/api@1.9.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
@@ -16806,11 +16921,6 @@ snapshots:
|
|||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.38.0': {}
|
'@opentelemetry/semantic-conventions@1.38.0': {}
|
||||||
|
|
||||||
'@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)':
|
|
||||||
dependencies:
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
'@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0)
|
|
||||||
|
|
||||||
'@paralleldrive/cuid2@2.3.1':
|
'@paralleldrive/cuid2@2.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
@@ -18256,16 +18366,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/parse5@5.0.3': {}
|
'@types/parse5@5.0.3': {}
|
||||||
|
|
||||||
'@types/pg-pool@2.0.7':
|
|
||||||
dependencies:
|
|
||||||
'@types/pg': 8.16.0
|
|
||||||
|
|
||||||
'@types/pg@8.15.6':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 24.10.9
|
|
||||||
pg-protocol: 1.11.0
|
|
||||||
pg-types: 2.2.0
|
|
||||||
|
|
||||||
'@types/pg@8.16.0':
|
'@types/pg@8.16.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.9
|
'@types/node': 24.10.9
|
||||||
@@ -18409,6 +18509,22 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/yargs-parser': 21.0.3
|
'@types/yargs-parser': 21.0.3
|
||||||
|
|
||||||
|
'@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/regexpp': 4.12.2
|
||||||
|
'@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/scope-manager': 8.53.0
|
||||||
|
'@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/visitor-keys': 8.53.0
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
ignore: 7.0.5
|
||||||
|
natural-compare: 1.4.0
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@@ -18425,6 +18541,18 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/scope-manager': 8.53.0
|
||||||
|
'@typescript-eslint/types': 8.53.0
|
||||||
|
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/visitor-keys': 8.53.0
|
||||||
|
debug: 4.4.3
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.53.0
|
'@typescript-eslint/scope-manager': 8.53.0
|
||||||
@@ -18455,6 +18583,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/types': 8.53.0
|
||||||
|
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
|
debug: 4.4.3
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.53.0
|
'@typescript-eslint/types': 8.53.0
|
||||||
@@ -18484,6 +18624,17 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||||
|
'@typescript-eslint/scope-manager': 8.53.0
|
||||||
|
'@typescript-eslint/types': 8.53.0
|
||||||
|
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3)
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
@@ -18504,6 +18655,25 @@ snapshots:
|
|||||||
|
|
||||||
'@vercel/oidc@3.0.5': {}
|
'@vercel/oidc@3.0.5': {}
|
||||||
|
|
||||||
|
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
|
ast-v8-to-istanbul: 0.3.8
|
||||||
|
debug: 4.4.3
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
istanbul-lib-source-maps: 5.0.6
|
||||||
|
istanbul-reports: 3.2.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
magicast: 0.3.5
|
||||||
|
std-env: 3.10.0
|
||||||
|
test-exclude: 7.0.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
@@ -18550,6 +18720,14 @@ snapshots:
|
|||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
|
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.4
|
||||||
@@ -19163,6 +19341,13 @@ snapshots:
|
|||||||
|
|
||||||
builtin-modules@5.0.0: {}
|
builtin-modules@5.0.0: {}
|
||||||
|
|
||||||
|
bullmq-otel@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.0
|
||||||
|
bullmq: 5.66.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
bullmq@5.66.5:
|
bullmq@5.66.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
cron-parser: 4.9.0
|
cron-parser: 4.9.0
|
||||||
@@ -20552,6 +20737,10 @@ snapshots:
|
|||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)):
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)):
|
eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
@@ -20568,6 +20757,16 @@ snapshots:
|
|||||||
lodash.memoize: 4.1.2
|
lodash.memoize: 4.1.2
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
|
|
||||||
|
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(prettier@3.8.0):
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
prettier: 3.8.0
|
||||||
|
prettier-linter-helpers: 1.0.1
|
||||||
|
synckit: 0.11.12
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/eslint': 9.6.1
|
||||||
|
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@1.21.7))
|
||||||
|
|
||||||
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0):
|
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
@@ -20596,6 +20795,28 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- ts-node
|
- ts-node
|
||||||
|
|
||||||
|
eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@1.21.7)):
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||||
|
'@eslint/plugin-kit': 0.4.1
|
||||||
|
change-case: 5.4.4
|
||||||
|
ci-info: 4.3.1
|
||||||
|
clean-regexp: 1.0.0
|
||||||
|
core-js-compat: 3.47.0
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
esquery: 1.6.0
|
||||||
|
find-up-simple: 1.0.1
|
||||||
|
globals: 16.5.0
|
||||||
|
indent-string: 5.0.0
|
||||||
|
is-builtin-module: 5.0.0
|
||||||
|
jsesc: 3.1.0
|
||||||
|
pluralize: 8.0.0
|
||||||
|
regexp-tree: 0.1.27
|
||||||
|
regjsparser: 0.13.0
|
||||||
|
semver: 7.7.3
|
||||||
|
strip-indent: 4.1.1
|
||||||
|
|
||||||
eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@2.6.1)):
|
eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
@@ -20632,6 +20853,47 @@ snapshots:
|
|||||||
|
|
||||||
eslint-visitor-keys@4.2.1: {}
|
eslint-visitor-keys@4.2.1: {}
|
||||||
|
|
||||||
|
eslint@9.39.2(jiti@1.21.7):
|
||||||
|
dependencies:
|
||||||
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
|
||||||
|
'@eslint-community/regexpp': 4.12.2
|
||||||
|
'@eslint/config-array': 0.21.1
|
||||||
|
'@eslint/config-helpers': 0.4.2
|
||||||
|
'@eslint/core': 0.17.0
|
||||||
|
'@eslint/eslintrc': 3.3.3
|
||||||
|
'@eslint/js': 9.39.2
|
||||||
|
'@eslint/plugin-kit': 0.4.1
|
||||||
|
'@humanfs/node': 0.16.7
|
||||||
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
|
'@humanwhocodes/retry': 0.4.3
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
ajv: 6.12.6
|
||||||
|
chalk: 4.1.2
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
debug: 4.4.3
|
||||||
|
escape-string-regexp: 4.0.0
|
||||||
|
eslint-scope: 8.4.0
|
||||||
|
eslint-visitor-keys: 4.2.1
|
||||||
|
espree: 10.4.0
|
||||||
|
esquery: 1.6.0
|
||||||
|
esutils: 2.0.3
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
file-entry-cache: 8.0.0
|
||||||
|
find-up: 5.0.0
|
||||||
|
glob-parent: 6.0.2
|
||||||
|
ignore: 5.3.2
|
||||||
|
imurmurhash: 0.1.4
|
||||||
|
is-glob: 4.0.3
|
||||||
|
json-stable-stringify-without-jsonify: 1.0.1
|
||||||
|
lodash.merge: 4.6.2
|
||||||
|
minimatch: 3.1.2
|
||||||
|
natural-compare: 1.4.0
|
||||||
|
optionator: 0.9.4
|
||||||
|
optionalDependencies:
|
||||||
|
jiti: 1.21.7
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
eslint@9.39.2(jiti@2.6.1):
|
eslint@9.39.2(jiti@2.6.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
|
||||||
@@ -26178,6 +26440,17 @@ snapshots:
|
|||||||
|
|
||||||
typedarray@0.0.6: {}
|
typedarray@0.0.6: {}
|
||||||
|
|
||||||
|
typescript-eslint@8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3)
|
||||||
|
'@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
|
eslint: 9.39.2(jiti@1.21.7)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||||
@@ -26463,6 +26736,27 @@ snapshots:
|
|||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
vite-node@3.2.4(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
|
dependencies:
|
||||||
|
cac: 6.7.14
|
||||||
|
debug: 4.4.3
|
||||||
|
es-module-lexer: 1.7.0
|
||||||
|
pathe: 2.0.3
|
||||||
|
vite: 7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- jiti
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
- tsx
|
||||||
|
- yaml
|
||||||
|
|
||||||
vite-node@3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
vite-node@3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
@@ -26505,6 +26799,17 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
globrex: 0.1.2
|
||||||
|
tsconfck: 3.1.6(typescript@5.9.3)
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
- typescript
|
||||||
|
|
||||||
vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -26516,6 +26821,24 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.27.2
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
picomatch: 4.0.3
|
||||||
|
postcss: 8.5.6
|
||||||
|
rollup: 4.55.1
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 24.10.9
|
||||||
|
fsevents: 2.3.3
|
||||||
|
jiti: 1.21.7
|
||||||
|
lightningcss: 1.30.2
|
||||||
|
sass: 1.97.1
|
||||||
|
terser: 5.44.1
|
||||||
|
tsx: 4.21.0
|
||||||
|
yaml: 2.8.2
|
||||||
|
|
||||||
vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.2
|
esbuild: 0.27.2
|
||||||
@@ -26560,6 +26883,50 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
|
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
|
dependencies:
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/expect': 3.2.4
|
||||||
|
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
|
'@vitest/pretty-format': 3.2.4
|
||||||
|
'@vitest/runner': 3.2.4
|
||||||
|
'@vitest/snapshot': 3.2.4
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
'@vitest/utils': 3.2.4
|
||||||
|
chai: 5.3.3
|
||||||
|
debug: 4.4.3
|
||||||
|
expect-type: 1.3.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.3
|
||||||
|
std-env: 3.10.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 0.3.2
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
tinypool: 1.1.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
vite: 7.3.1(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
vite-node: 3.2.4(@types/node@24.10.9)(jiti@1.21.7)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/debug': 4.1.12
|
||||||
|
'@types/node': 24.10.9
|
||||||
|
happy-dom: 20.3.0
|
||||||
|
jsdom: 26.1.0(canvas@2.11.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- jiti
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- msw
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
- tsx
|
||||||
|
- yaml
|
||||||
|
|
||||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
|
|||||||
@@ -6,4 +6,17 @@ if [[ "$IMMICH_ENV" == "production" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cd /usr/src/app || exit
|
cd /usr/src/app || exit
|
||||||
|
|
||||||
|
# Compile telemetry-preload so it can be required before the app starts.
|
||||||
|
# This is necessary for OpenTelemetry http instrumentation to work correctly.
|
||||||
|
mkdir -p dist
|
||||||
|
pnpm --filter immich exec tsc src/telemetry-preload.ts \
|
||||||
|
--outDir dist \
|
||||||
|
--esModuleInterop \
|
||||||
|
--module node16 \
|
||||||
|
--moduleResolution node16 \
|
||||||
|
--target ES2022 \
|
||||||
|
--skipLibCheck
|
||||||
|
|
||||||
|
export NODE_OPTIONS="${NODE_OPTIONS} --require ./dist/telemetry-preload.js"
|
||||||
pnpm --filter immich exec nest start --debug "0.0.0.0:9230" --watch -- "$@"
|
pnpm --filter immich exec nest start --debug "0.0.0.0:9230" --watch -- "$@"
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${SERVER_HOME}/dist/main.js" ]; then
|
if [ -f "${SERVER_HOME}/dist/main.js" ]; then
|
||||||
exec node "${SERVER_HOME}/dist/main.js" "$@"
|
exec node --require "${SERVER_HOME}/dist/telemetry-preload.js" "${SERVER_HOME}/dist/main.js" "$@"
|
||||||
else
|
else
|
||||||
echo "Error: ${SERVER_HOME}/dist/main.js not found"
|
echo "Error: ${SERVER_HOME}/dist/main.js not found"
|
||||||
if [ "$IMMICH_ENV" = "development" ]; then
|
if [ "$IMMICH_ENV" = "development" ]; then
|
||||||
|
|||||||
+4
-1
@@ -45,14 +45,16 @@
|
|||||||
"@nestjs/websockets": "^11.0.4",
|
"@nestjs/websockets": "^11.0.4",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||||
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.211.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.210.0",
|
"@opentelemetry/exporter-prometheus": "^0.210.0",
|
||||||
|
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
|
||||||
"@opentelemetry/instrumentation-http": "^0.210.0",
|
"@opentelemetry/instrumentation-http": "^0.210.0",
|
||||||
"@opentelemetry/instrumentation-ioredis": "^0.58.0",
|
"@opentelemetry/instrumentation-ioredis": "^0.58.0",
|
||||||
"@opentelemetry/instrumentation-nestjs-core": "^0.56.0",
|
"@opentelemetry/instrumentation-nestjs-core": "^0.56.0",
|
||||||
"@opentelemetry/instrumentation-pg": "^0.62.0",
|
|
||||||
"@opentelemetry/resources": "^2.0.1",
|
"@opentelemetry/resources": "^2.0.1",
|
||||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||||
"@opentelemetry/sdk-node": "^0.210.0",
|
"@opentelemetry/sdk-node": "^0.210.0",
|
||||||
|
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
||||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||||
"@react-email/components": "^0.5.0",
|
"@react-email/components": "^0.5.0",
|
||||||
"@react-email/render": "^1.1.2",
|
"@react-email/render": "^1.1.2",
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"bullmq": "^5.51.0",
|
"bullmq": "^5.51.0",
|
||||||
|
"bullmq-otel": "^1.1.0",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ export enum AssetFileType {
|
|||||||
Sidecar = 'sidecar',
|
Sidecar = 'sidecar',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum GeneratedImageType {
|
||||||
|
Thumbnail = 'thumbnail',
|
||||||
|
Preview = 'preview',
|
||||||
|
Fullsize = 'fullsize',
|
||||||
|
Person = 'person',
|
||||||
|
}
|
||||||
|
|
||||||
export enum AlbumUserRole {
|
export enum AlbumUserRole {
|
||||||
Editor = 'editor',
|
Editor = 'editor',
|
||||||
Viewer = 'viewer',
|
Viewer = 'viewer',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { RedisOptions } from 'ioredis';
|
|||||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
import { bullMQTelemetry } from 'src/telemetry-preload';
|
||||||
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
||||||
import { Telemetry } from 'src/decorators';
|
import { Telemetry } from 'src/decorators';
|
||||||
import { EnvDto } from 'src/dtos/env.dto';
|
import { EnvDto } from 'src/dtos/env.dto';
|
||||||
@@ -262,6 +263,7 @@ const getEnv = (): EnvData => {
|
|||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: false,
|
removeOnFail: false,
|
||||||
},
|
},
|
||||||
|
telemetry: bullMQTelemetry,
|
||||||
},
|
},
|
||||||
queues: Object.values(QueueName).map((name) => ({ name })),
|
queues: Object.values(QueueName).map((name) => ({ name })),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { context, Span, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { MachineLearningConfig } from 'src/config';
|
import { MachineLearningConfig } from 'src/config';
|
||||||
import { CLIPConfig } from 'src/dtos/model-config.dto';
|
import { CLIPConfig } from 'src/dtos/model-config.dto';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { Traced } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
|
const tracer = trace.getTracer('immich');
|
||||||
|
|
||||||
export interface BoundingBox {
|
export interface BoundingBox {
|
||||||
x1: number;
|
x1: number;
|
||||||
@@ -162,17 +166,31 @@ export class MachineLearningRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async predict<T>(payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
|
private async predict<T>(payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
|
||||||
const formData = await this.getFormData(payload, config);
|
const taskType = Object.keys(config)[0] as ModelTask;
|
||||||
|
|
||||||
for (const url of [
|
return tracer.startActiveSpan('ml.predict', { kind: SpanKind.CLIENT }, context.active(), async (span: Span) => {
|
||||||
// try healthy servers first
|
span.setAttribute('ml.task', taskType);
|
||||||
|
|
||||||
|
const formData = await this.getFormData(payload, config);
|
||||||
|
let attemptCount = 0;
|
||||||
|
let successUrl: string | undefined;
|
||||||
|
|
||||||
|
const urls = [
|
||||||
...this.config.urls.filter((url) => this.isHealthy(url)),
|
...this.config.urls.filter((url) => this.isHealthy(url)),
|
||||||
...this.config.urls.filter((url) => !this.isHealthy(url)),
|
...this.config.urls.filter((url) => !this.isHealthy(url)),
|
||||||
]) {
|
];
|
||||||
|
|
||||||
|
for (const url of urls) {
|
||||||
|
attemptCount++;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData });
|
const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
this.setHealthy(url, true);
|
this.setHealthy(url, true);
|
||||||
|
successUrl = url;
|
||||||
|
span.setAttribute('ml.url', url);
|
||||||
|
span.setAttribute('ml.attempts', attemptCount);
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
span.end();
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +206,20 @@ export class MachineLearningRepository {
|
|||||||
this.setHealthy(url, false);
|
this.setHealthy(url, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`);
|
span.setAttribute('ml.attempts', attemptCount);
|
||||||
|
const errorMessage = `Machine learning request '${JSON.stringify(config)}' failed for all URLs`;
|
||||||
|
span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });
|
||||||
|
span.end();
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced({ name: 'ml.detectFaces', kind: SpanKind.CLIENT })
|
||||||
async detectFaces(imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
|
async detectFaces(imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('ml.modelName', modelName);
|
||||||
|
span?.setAttribute('ml.minScore', minScore);
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
[ModelTask.FACIAL_RECOGNITION]: {
|
[ModelTask.FACIAL_RECOGNITION]: {
|
||||||
[ModelType.DETECTION]: { modelName, options: { minScore } },
|
[ModelType.DETECTION]: { modelName, options: { minScore } },
|
||||||
@@ -206,19 +234,36 @@ export class MachineLearningRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced({ name: 'ml.encodeImage', kind: SpanKind.CLIENT })
|
||||||
async encodeImage(imagePath: string, { modelName }: CLIPConfig) {
|
async encodeImage(imagePath: string, { modelName }: CLIPConfig) {
|
||||||
|
trace.getActiveSpan()?.setAttribute('ml.modelName', modelName);
|
||||||
|
|
||||||
const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } };
|
const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } };
|
||||||
const response = await this.predict<ClipVisualResponse>({ imagePath }, request);
|
const response = await this.predict<ClipVisualResponse>({ imagePath }, request);
|
||||||
return response[ModelTask.SEARCH];
|
return response[ModelTask.SEARCH];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced({ name: 'ml.encodeText', kind: SpanKind.CLIENT })
|
||||||
async encodeText(text: string, { language, modelName }: TextEncodingOptions) {
|
async encodeText(text: string, { language, modelName }: TextEncodingOptions) {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('ml.modelName', modelName);
|
||||||
|
if (language) {
|
||||||
|
span?.setAttribute('ml.language', language);
|
||||||
|
}
|
||||||
|
|
||||||
const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName, options: { language } } } };
|
const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName, options: { language } } } };
|
||||||
const response = await this.predict<ClipTextualResponse>({ text }, request);
|
const response = await this.predict<ClipTextualResponse>({ text }, request);
|
||||||
return response[ModelTask.SEARCH];
|
return response[ModelTask.SEARCH];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced({ name: 'ml.ocr', kind: SpanKind.CLIENT })
|
||||||
async ocr(imagePath: string, { modelName, minDetectionScore, minRecognitionScore, maxResolution }: OcrOptions) {
|
async ocr(imagePath: string, { modelName, minDetectionScore, minRecognitionScore, maxResolution }: OcrOptions) {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('ml.modelName', modelName);
|
||||||
|
span?.setAttribute('ml.minDetectionScore', minDetectionScore);
|
||||||
|
span?.setAttribute('ml.minRecognitionScore', minRecognitionScore);
|
||||||
|
span?.setAttribute('ml.maxResolution', maxResolution);
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
[ModelTask.OCR]: {
|
[ModelTask.OCR]: {
|
||||||
[ModelType.DETECTION]: { modelName, options: { minScore: minDetectionScore, maxResolution } },
|
[ModelType.DETECTION]: { modelName, options: { minScore: minDetectionScore, maxResolution } },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { context, Span, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
|
||||||
import { ExifDateTime, exiftool, WriteTags } from 'exiftool-vendored';
|
import { ExifDateTime, exiftool, WriteTags } from 'exiftool-vendored';
|
||||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
TranscodeCommand,
|
TranscodeCommand,
|
||||||
VideoInfo,
|
VideoInfo,
|
||||||
} from 'src/types';
|
} from 'src/types';
|
||||||
|
import { Traced } from 'src/utils/instrumentation';
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
import { createAffineMatrix } from 'src/utils/transform';
|
import { createAffineMatrix } from 'src/utils/transform';
|
||||||
|
|
||||||
@@ -26,9 +28,17 @@ const probe = (input: string, options: string[]): Promise<FfprobeData> =>
|
|||||||
new Promise((resolve, reject) =>
|
new Promise((resolve, reject) =>
|
||||||
ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
|
ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
|
||||||
);
|
);
|
||||||
|
const tracer = trace.getTracer('immich');
|
||||||
sharp.concurrency(0);
|
sharp.concurrency(0);
|
||||||
sharp.cache({ files: 0 });
|
sharp.cache({ files: 0 });
|
||||||
|
|
||||||
|
const EXTRACTION_METHODS = [
|
||||||
|
{ tag: 'JpgFromRaw2', format: RawExtractedFormat.Jpeg },
|
||||||
|
{ tag: 'JpgFromRaw', format: RawExtractedFormat.Jpeg },
|
||||||
|
{ tag: 'PreviewJXL', format: RawExtractedFormat.Jxl },
|
||||||
|
{ tag: 'PreviewImage', format: RawExtractedFormat.Jpeg },
|
||||||
|
] as const;
|
||||||
|
|
||||||
type ProgressEvent = {
|
type ProgressEvent = {
|
||||||
frames: number;
|
frames: number;
|
||||||
currentFps: number;
|
currentFps: number;
|
||||||
@@ -49,44 +59,23 @@ export class MediaRepository {
|
|||||||
this.logger.setContext(MediaRepository.name);
|
this.logger.setContext(MediaRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Traced('media.exiftool.extract')
|
||||||
*
|
|
||||||
* @param input file path to the input image
|
|
||||||
* @returns ExtractResult if succeeded, or null if failed
|
|
||||||
*/
|
|
||||||
async extract(input: string): Promise<ExtractResult | null> {
|
async extract(input: string): Promise<ExtractResult | null> {
|
||||||
|
for (const { tag, format } of EXTRACTION_METHODS) {
|
||||||
try {
|
try {
|
||||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
|
const buffer = await exiftool.extractBinaryTagToBuffer(tag, input);
|
||||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
trace.getActiveSpan()?.setAttribute('exiftool.extractedTag', tag);
|
||||||
} catch (error: any) {
|
return { buffer, format };
|
||||||
this.logger.debug(`Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next: ${error}`);
|
} catch {
|
||||||
|
this.logger.debug(`Could not extract ${tag} from image, trying next method`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
|
|
||||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.debug(`Could not extract JPEG buffer from image, trying PreviewJXL next: ${error}`);
|
|
||||||
}
|
}
|
||||||
|
trace.getActiveSpan()?.setAttribute('exiftool.extractedTag', 'none');
|
||||||
try {
|
|
||||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
|
|
||||||
return { buffer, format: RawExtractedFormat.Jxl };
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.debug(`Could not extract PreviewJXL buffer from image, trying PreviewImage next: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
|
|
||||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.debug(`Could not extract preview buffer from image: ${error}`);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@Traced('media.exiftool.writeExif')
|
||||||
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
||||||
try {
|
|
||||||
const tagsToWrite: WriteTags = {
|
const tagsToWrite: WriteTags = {
|
||||||
ExifImageWidth: tags.exifImageWidth,
|
ExifImageWidth: tags.exifImageWidth,
|
||||||
ExifImageHeight: tags.exifImageHeight,
|
ExifImageHeight: tags.exifImageHeight,
|
||||||
@@ -108,22 +97,28 @@ export class MediaRepository {
|
|||||||
ProfileDescription: tags.profileDescription,
|
ProfileDescription: tags.profileDescription,
|
||||||
ColorSpace: tags.colorspace,
|
ColorSpace: tags.colorspace,
|
||||||
Rating: tags.rating,
|
Rating: tags.rating,
|
||||||
// specially convert Orientation to numeric Orientation# for exiftool
|
|
||||||
'Orientation#': tags.orientation ? Number(tags.orientation) : undefined,
|
'Orientation#': tags.orientation ? Number(tags.orientation) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
await exiftool.write(output, tagsToWrite, {
|
await exiftool.write(output, tagsToWrite, {
|
||||||
ignoreMinorErrors: true,
|
ignoreMinorErrors: true,
|
||||||
writeArgs: ['-overwrite_original'],
|
writeArgs: ['-overwrite_original'],
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn(`Could not write exif data to image: ${error.message}`);
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
||||||
|
span?.recordException(error as Error);
|
||||||
|
this.logger.warn(`Could not write exif data to image: ${error}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('media.exiftool.copyTagGroup')
|
||||||
async copyTagGroup(tagGroup: string, source: string, target: string): Promise<boolean> {
|
async copyTagGroup(tagGroup: string, source: string, target: string): Promise<boolean> {
|
||||||
|
trace.getActiveSpan()?.setAttribute('exiftool.tagGroup', tagGroup);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await exiftool.write(
|
await exiftool.write(
|
||||||
target,
|
target,
|
||||||
@@ -134,15 +129,28 @@ export class MediaRepository {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn(`Could not copy tag data to image: ${error.message}`);
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
||||||
|
span?.recordException(error as Error);
|
||||||
|
this.logger.warn(`Could not copy tag data to image: ${error}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('media.decodeImage')
|
||||||
async decodeImage(input: string | Buffer, options: DecodeToBufferOptions) {
|
async decodeImage(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('media.colorspace', options.colorspace);
|
||||||
|
if (options.size !== undefined) {
|
||||||
|
span?.setAttribute('media.size', options.size);
|
||||||
|
}
|
||||||
|
|
||||||
const pipeline = await this.getImageDecodingPipeline(input, options);
|
const pipeline = await this.getImageDecodingPipeline(input, options);
|
||||||
return pipeline.raw().toBuffer({ resolveWithObject: true });
|
const result = await pipeline.raw().toBuffer({ resolveWithObject: true });
|
||||||
|
span?.setAttribute('media.output.width', result.info.width);
|
||||||
|
span?.setAttribute('media.output.height', result.info.height);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyEdits(pipeline: sharp.Sharp, edits: AssetEditActionItem[]): Promise<sharp.Sharp> {
|
private async applyEdits(pipeline: sharp.Sharp, edits: AssetEditActionItem[]): Promise<sharp.Sharp> {
|
||||||
@@ -170,11 +178,21 @@ export class MediaRepository {
|
|||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('media.generateThumbnail')
|
||||||
async generateThumbnail(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise<void> {
|
async generateThumbnail(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise<void> {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('media.imageType', options.imageType ?? 'unknown');
|
||||||
|
span?.setAttribute('media.format', options.format);
|
||||||
|
span?.setAttribute('media.quality', options.quality);
|
||||||
|
span?.setAttribute('media.size', options.size ?? 0);
|
||||||
|
span?.setAttribute('media.colorspace', options.colorspace);
|
||||||
|
if (options.progressive !== undefined) {
|
||||||
|
span?.setAttribute('media.progressive', options.progressive);
|
||||||
|
}
|
||||||
|
|
||||||
const pipeline = await this.getImageDecodingPipeline(input, options);
|
const pipeline = await this.getImageDecodingPipeline(input, options);
|
||||||
const decoded = pipeline.toFormat(options.format, {
|
const decoded = pipeline.toFormat(options.format, {
|
||||||
quality: options.quality,
|
quality: options.quality,
|
||||||
// this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
|
|
||||||
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
|
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
|
||||||
progressive: options.progressive,
|
progressive: options.progressive,
|
||||||
});
|
});
|
||||||
@@ -215,7 +233,11 @@ export class MediaRepository {
|
|||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('media.generateThumbhash')
|
||||||
async generateThumbhash(input: string | Buffer, options: GenerateThumbhashOptions): Promise<Buffer> {
|
async generateThumbhash(input: string | Buffer, options: GenerateThumbhashOptions): Promise<Buffer> {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('media.colorspace', options.colorspace);
|
||||||
|
|
||||||
const [{ rgbaToThumbHash }, decodingPipeline] = await Promise.all([
|
const [{ rgbaToThumbHash }, decodingPipeline] = await Promise.all([
|
||||||
import('thumbhash'),
|
import('thumbhash'),
|
||||||
this.getImageDecodingPipeline(input, {
|
this.getImageDecodingPipeline(input, {
|
||||||
@@ -229,12 +251,23 @@ export class MediaRepository {
|
|||||||
const pipeline = decodingPipeline.resize(100, 100, { fit: 'inside', withoutEnlargement: true }).raw().ensureAlpha();
|
const pipeline = decodingPipeline.resize(100, 100, { fit: 'inside', withoutEnlargement: true }).raw().ensureAlpha();
|
||||||
|
|
||||||
const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
||||||
|
span?.setAttribute('media.thumbhash.width', info.width);
|
||||||
|
span?.setAttribute('media.thumbhash.height', info.height);
|
||||||
|
|
||||||
return Buffer.from(rgbaToThumbHash(info.width, info.height, data));
|
return Buffer.from(rgbaToThumbHash(info.width, info.height, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('ffmpeg.probe')
|
||||||
async probe(input: string, options?: ProbeOptions): Promise<VideoInfo> {
|
async probe(input: string, options?: ProbeOptions): Promise<VideoInfo> {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('ffmpeg.countFrames', options?.countFrames ?? false);
|
||||||
|
|
||||||
const results = await probe(input, options?.countFrames ? ['-count_packets'] : []); // gets frame count quickly: https://stackoverflow.com/a/28376817
|
const results = await probe(input, options?.countFrames ? ['-count_packets'] : []); // gets frame count quickly: https://stackoverflow.com/a/28376817
|
||||||
|
|
||||||
|
span?.setAttribute('ffmpeg.duration', this.parseFloat(results.format.duration));
|
||||||
|
span?.setAttribute('ffmpeg.videoStreams', results.streams.filter((s) => s.codec_type === 'video').length);
|
||||||
|
span?.setAttribute('ffmpeg.audioStreams', results.streams.filter((s) => s.codec_type === 'audio').length);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
format: {
|
format: {
|
||||||
formatName: results.format.format_name,
|
formatName: results.format.format_name,
|
||||||
@@ -272,17 +305,39 @@ export class MediaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise<void> {
|
transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise<void> {
|
||||||
|
const span = tracer.startSpan('ffmpeg.transcode', { kind: SpanKind.INTERNAL }, context.active());
|
||||||
|
span.setAttribute('ffmpeg.twoPass', options.twoPass ?? false);
|
||||||
|
span.setAttribute('ffmpeg.frameCount', options.progress.frameCount);
|
||||||
|
|
||||||
|
const endSpan = (error?: Error) => {
|
||||||
|
if (error) {
|
||||||
|
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||||
|
span.recordException(error);
|
||||||
|
} else {
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
}
|
||||||
|
span.end();
|
||||||
|
};
|
||||||
|
|
||||||
if (!options.twoPass) {
|
if (!options.twoPass) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.configureFfmpegCall(input, output, options)
|
this.configureFfmpegCall(input, output, options)
|
||||||
.on('error', reject)
|
.on('error', (error) => {
|
||||||
.on('end', () => resolve())
|
endSpan(error);
|
||||||
|
reject(error);
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
endSpan();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
.run();
|
.run();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof output !== 'string') {
|
if (typeof output !== 'string') {
|
||||||
throw new TypeError('Two-pass transcoding does not support writing to a stream');
|
const error = new TypeError('Two-pass transcoding does not support writing to a stream');
|
||||||
|
endSpan(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// two-pass allows for precise control of bitrate at the cost of running twice
|
// two-pass allows for precise control of bitrate at the cost of running twice
|
||||||
@@ -293,24 +348,37 @@ export class MediaRepository {
|
|||||||
.addOptions('-pass', '1')
|
.addOptions('-pass', '1')
|
||||||
.addOptions('-passlogfile', output)
|
.addOptions('-passlogfile', output)
|
||||||
.addOptions('-f null')
|
.addOptions('-f null')
|
||||||
.on('error', reject)
|
.on('error', (error) => {
|
||||||
|
endSpan(error);
|
||||||
|
reject(error);
|
||||||
|
})
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
// second pass
|
// second pass
|
||||||
this.configureFfmpegCall(input, output, options)
|
this.configureFfmpegCall(input, output, options)
|
||||||
.addOptions('-pass', '2')
|
.addOptions('-pass', '2')
|
||||||
.addOptions('-passlogfile', output)
|
.addOptions('-passlogfile', output)
|
||||||
.on('error', reject)
|
.on('error', (error) => {
|
||||||
|
endSpan(error);
|
||||||
|
reject(error);
|
||||||
|
})
|
||||||
.on('end', () => handlePromiseError(fs.unlink(`${output}-0.log`), this.logger))
|
.on('end', () => handlePromiseError(fs.unlink(`${output}-0.log`), this.logger))
|
||||||
.on('end', () => handlePromiseError(fs.rm(`${output}-0.log.mbtree`, { force: true }), this.logger))
|
.on('end', () => handlePromiseError(fs.rm(`${output}-0.log.mbtree`, { force: true }), this.logger))
|
||||||
.on('end', () => resolve())
|
.on('end', () => {
|
||||||
|
endSpan();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
.run();
|
.run();
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('media.getImageDimensions')
|
||||||
async getImageDimensions(input: string | Buffer): Promise<ImageDimensions> {
|
async getImageDimensions(input: string | Buffer): Promise<ImageDimensions> {
|
||||||
const { width = 0, height = 0 } = await sharp(input).metadata();
|
const { width = 0, height = 0 } = await sharp(input).metadata();
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('media.width', width);
|
||||||
|
span?.setAttribute('media.height', height);
|
||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { SpanStatusCode, trace } from '@opentelemetry/api';
|
||||||
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
||||||
import geotz from 'geo-tz';
|
import geotz from 'geo-tz';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { Traced } from 'src/utils/instrumentation';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
|
||||||
interface ExifDuration {
|
interface ExifDuration {
|
||||||
@@ -104,22 +106,34 @@ export class MetadataRepository {
|
|||||||
await this.exiftool.end();
|
await this.exiftool.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
readTags(path: string): Promise<ImmichTags> {
|
@Traced('metadata.exiftool.readTags')
|
||||||
|
async readTags(path: string): Promise<ImmichTags> {
|
||||||
|
try {
|
||||||
const args = mimeTypes.isVideo(path) ? ['-ee'] : [];
|
const args = mimeTypes.isVideo(path) ? ['-ee'] : [];
|
||||||
return this.exiftool.read(path, args).catch((error) => {
|
return (await this.exiftool.read(path, args)) as ImmichTags;
|
||||||
this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`);
|
} catch (error: unknown) {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
||||||
|
span?.recordException(error as Error);
|
||||||
|
this.logger.warn(`Error reading exif data (${path}): ${error}`);
|
||||||
return {};
|
return {};
|
||||||
}) as Promise<ImmichTags>;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
|
@Traced('metadata.exiftool.extractBinaryTag')
|
||||||
|
async extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
|
||||||
|
trace.getActiveSpan()?.setAttribute('exiftool.tagName', tagName);
|
||||||
return this.exiftool.extractBinaryTagToBuffer(tagName, path);
|
return this.exiftool.extractBinaryTagToBuffer(tagName, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('metadata.exiftool.writeTags')
|
||||||
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.exiftool.write(path, tags);
|
await this.exiftool.write(path, tags);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
||||||
|
span?.recordException(error as Error);
|
||||||
this.logger.warn(`Error writing exif data (${path}): ${error}`);
|
this.logger.warn(`Error writing exif data (${path}): ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { trace } from '@opentelemetry/api';
|
||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
import chokidar, { ChokidarOptions } from 'chokidar';
|
import chokidar, { ChokidarOptions } from 'chokidar';
|
||||||
import { escapePath, glob, globStream } from 'fast-glob';
|
import { escapePath, glob, globStream } from 'fast-glob';
|
||||||
@@ -9,6 +10,7 @@ import { PassThrough, Readable, Writable } from 'node:stream';
|
|||||||
import { createGunzip, createGzip } from 'node:zlib';
|
import { createGunzip, createGzip } from 'node:zlib';
|
||||||
import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto';
|
import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { Traced } from 'src/utils/instrumentation';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
|
||||||
export interface WatchEvents {
|
export interface WatchEvents {
|
||||||
@@ -106,9 +108,18 @@ export class StorageRepository {
|
|||||||
return createReadStream(filepath);
|
return createReadStream(filepath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('storage.createReadStream')
|
||||||
async createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream> {
|
async createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream> {
|
||||||
const { size } = await fs.stat(filepath);
|
const { size } = await fs.stat(filepath);
|
||||||
await fs.access(filepath, constants.R_OK);
|
await fs.access(filepath, constants.R_OK);
|
||||||
|
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('file.path', filepath);
|
||||||
|
span?.setAttribute('file.size', size);
|
||||||
|
if (mimeType) {
|
||||||
|
span?.setAttribute('file.content_type', mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stream: createReadStream(filepath),
|
stream: createReadStream(filepath),
|
||||||
length: size,
|
length: size,
|
||||||
@@ -116,10 +127,15 @@ export class StorageRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Traced('storage.readFile')
|
||||||
async readFile(filepath: string, options?: ReadOptionsWithBuffer<Buffer>): Promise<Buffer> {
|
async readFile(filepath: string, options?: ReadOptionsWithBuffer<Buffer>): Promise<Buffer> {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
span?.setAttribute('file.path', filepath);
|
||||||
|
|
||||||
const file = await fs.open(filepath);
|
const file = await fs.open(filepath);
|
||||||
try {
|
try {
|
||||||
const { buffer } = await file.read(options);
|
const { buffer, bytesRead } = await file.read(options);
|
||||||
|
span?.setAttribute('file.bytes_read', bytesRead);
|
||||||
return buffer as Buffer;
|
return buffer as Buffer;
|
||||||
} finally {
|
} finally {
|
||||||
await file.close();
|
await file.close();
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { MetricOptions } from '@opentelemetry/api';
|
import { MetricOptions } from '@opentelemetry/api';
|
||||||
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
|
import { contextBase } from '@opentelemetry/sdk-node';
|
||||||
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
|
|
||||||
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
||||||
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
|
|
||||||
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
|
|
||||||
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
|
|
||||||
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
||||||
import { AggregationType } from '@opentelemetry/sdk-metrics';
|
|
||||||
import { NodeSDK, contextBase } from '@opentelemetry/sdk-node';
|
|
||||||
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
||||||
import { ClassConstructor } from 'class-transformer';
|
import { ClassConstructor } from 'class-transformer';
|
||||||
import { snakeCase, startCase } from 'lodash';
|
import { snakeCase, startCase } from 'lodash';
|
||||||
import { MetricService } from 'nestjs-otel';
|
import { MetricService } from 'nestjs-otel';
|
||||||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
|
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
|
||||||
import { serverVersion } from 'src/constants';
|
|
||||||
import { ImmichTelemetry, MetadataKey } from 'src/enum';
|
import { ImmichTelemetry, MetadataKey } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
@@ -51,49 +41,16 @@ export class MetricGroupRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const aggregationBoundaries = [
|
// OpenTelemetry SDK is now initialized in telemetry-preload.ts
|
||||||
0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10_000,
|
// which runs before any modules are imported. This ensures the
|
||||||
];
|
// global meter provider is available for nestjs-otel.
|
||||||
|
|
||||||
let instance: NodeSDK | undefined;
|
export const bootstrapTelemetry = (_port: number) => {
|
||||||
|
// No-op: SDK is initialized in telemetry-preload.ts
|
||||||
export const bootstrapTelemetry = (port: number) => {
|
|
||||||
if (instance) {
|
|
||||||
throw new Error('OpenTelemetry SDK already started');
|
|
||||||
}
|
|
||||||
instance = new NodeSDK({
|
|
||||||
resource: resourceFromAttributes({
|
|
||||||
[ATTR_SERVICE_NAME]: `immich`,
|
|
||||||
[ATTR_SERVICE_VERSION]: serverVersion.toString(),
|
|
||||||
}),
|
|
||||||
metricReader: new PrometheusExporter({ port }),
|
|
||||||
contextManager: new AsyncLocalStorageContextManager(),
|
|
||||||
instrumentations: [
|
|
||||||
new HttpInstrumentation(),
|
|
||||||
new IORedisInstrumentation(),
|
|
||||||
new NestInstrumentation(),
|
|
||||||
new PgInstrumentation(),
|
|
||||||
],
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
instrumentName: '*',
|
|
||||||
instrumentUnit: 'ms',
|
|
||||||
aggregation: {
|
|
||||||
type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
|
|
||||||
options: { boundaries: aggregationBoundaries },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
instance.start();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const teardownTelemetry = async () => {
|
export const teardownTelemetry = async () => {
|
||||||
if (instance) {
|
// No-op: SDK shutdown is handled by process exit
|
||||||
await instance.shutdown();
|
|
||||||
instance = undefined;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ExifOrientation,
|
ExifOrientation,
|
||||||
|
GeneratedImageType,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
JobName,
|
JobName,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
@@ -356,6 +357,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Preview,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -370,6 +372,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Thumbnail,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -581,6 +584,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Preview,
|
||||||
},
|
},
|
||||||
previewPath,
|
previewPath,
|
||||||
);
|
);
|
||||||
@@ -595,6 +599,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Thumbnail,
|
||||||
},
|
},
|
||||||
thumbnailPath,
|
thumbnailPath,
|
||||||
);
|
);
|
||||||
@@ -630,6 +635,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Preview,
|
||||||
},
|
},
|
||||||
previewPath,
|
previewPath,
|
||||||
);
|
);
|
||||||
@@ -644,6 +650,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Thumbnail,
|
||||||
},
|
},
|
||||||
thumbnailPath,
|
thumbnailPath,
|
||||||
);
|
);
|
||||||
@@ -838,6 +845,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Preview,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -870,6 +878,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Fullsize,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -884,6 +893,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Preview,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -914,6 +924,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Fullsize,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -928,6 +939,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Preview,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -959,6 +971,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Fullsize,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1016,6 +1029,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Fullsize,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1056,6 +1070,7 @@ describe(MediaService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
raw: rawInfo,
|
raw: rawInfo,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
imageType: GeneratedImageType.Fullsize,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1300,6 +1315,7 @@ describe(MediaService.name, () => {
|
|||||||
raw: info,
|
raw: info,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 250,
|
size: 250,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1345,6 +1361,7 @@ describe(MediaService.name, () => {
|
|||||||
raw: info,
|
raw: info,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 250,
|
size: 250,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1388,6 +1405,7 @@ describe(MediaService.name, () => {
|
|||||||
raw: info,
|
raw: info,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 250,
|
size: 250,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1431,6 +1449,7 @@ describe(MediaService.name, () => {
|
|||||||
raw: info,
|
raw: info,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 250,
|
size: 250,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1474,6 +1493,7 @@ describe(MediaService.name, () => {
|
|||||||
raw: info,
|
raw: info,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 250,
|
size: 250,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1517,6 +1537,7 @@ describe(MediaService.name, () => {
|
|||||||
raw: info,
|
raw: info,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 250,
|
size: 250,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
@@ -1565,6 +1586,7 @@ describe(MediaService.name, () => {
|
|||||||
raw: info,
|
raw: info,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 250,
|
size: 250,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
},
|
},
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
|
GeneratedImageType,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
JobName,
|
JobName,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
@@ -329,12 +330,12 @@ export class MediaService extends BaseService {
|
|||||||
this.mediaRepository.generateThumbhash(data, thumbnailOptions),
|
this.mediaRepository.generateThumbhash(data, thumbnailOptions),
|
||||||
this.mediaRepository.generateThumbnail(
|
this.mediaRepository.generateThumbnail(
|
||||||
data,
|
data,
|
||||||
{ ...image.thumbnail, ...thumbnailOptions, edits: useEdits ? asset.edits : [] },
|
{ ...image.thumbnail, ...thumbnailOptions, imageType: GeneratedImageType.Thumbnail, edits: useEdits ? asset.edits : [] },
|
||||||
thumbnailPath,
|
thumbnailPath,
|
||||||
),
|
),
|
||||||
this.mediaRepository.generateThumbnail(
|
this.mediaRepository.generateThumbnail(
|
||||||
data,
|
data,
|
||||||
{ ...image.preview, ...thumbnailOptions, edits: useEdits ? asset.edits : [] },
|
{ ...image.preview, ...thumbnailOptions, imageType: GeneratedImageType.Preview, edits: useEdits ? asset.edits : [] },
|
||||||
previewPath,
|
previewPath,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -352,6 +353,7 @@ export class MediaService extends BaseService {
|
|||||||
format: image.fullsize.format,
|
format: image.fullsize.format,
|
||||||
quality: image.fullsize.quality,
|
quality: image.fullsize.quality,
|
||||||
progressive: image.fullsize.progressive,
|
progressive: image.fullsize.progressive,
|
||||||
|
imageType: GeneratedImageType.Fullsize,
|
||||||
...thumbnailOptions,
|
...thumbnailOptions,
|
||||||
};
|
};
|
||||||
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
||||||
@@ -438,6 +440,7 @@ export class MediaService extends BaseService {
|
|||||||
progressive: false,
|
progressive: false,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: FACE_THUMBNAIL_SIZE,
|
size: FACE_THUMBNAIL_SIZE,
|
||||||
|
imageType: GeneratedImageType.Person,
|
||||||
edits: [
|
edits: [
|
||||||
{
|
{
|
||||||
action: AssetEditAction.Crop,
|
action: AssetEditAction.Crop,
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import '@opentelemetry/api';
|
||||||
|
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
|
||||||
|
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
|
||||||
|
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
||||||
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||||
|
import type { SpanProcessor, ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
||||||
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
||||||
|
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
||||||
|
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
|
||||||
|
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core';
|
||||||
|
import { resourceFromAttributes } from '@opentelemetry/resources';
|
||||||
|
import { AggregationType, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
||||||
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||||
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
||||||
|
import {
|
||||||
|
ATTR_VCS_REPOSITORY_REF_REVISION,
|
||||||
|
ATTR_VCS_REPOSITORY_URL_FULL,
|
||||||
|
} from '@opentelemetry/semantic-conventions/incubating';
|
||||||
|
import { isMainThread } from 'node:worker_threads';
|
||||||
|
import { BullMQOtel } from 'bullmq-otel';
|
||||||
|
|
||||||
|
export const bullMQTelemetry = new BullMQOtel('immich-jobs');
|
||||||
|
|
||||||
|
const ignoredRoutes = new Set(['/api/server/ping']);
|
||||||
|
const ignoredCallbacks = new Set(['pingServer']);
|
||||||
|
|
||||||
|
class FilteringSpanProcessor implements SpanProcessor {
|
||||||
|
constructor(private delegate: SpanProcessor) {}
|
||||||
|
|
||||||
|
onStart(span: ReadableSpan, context: unknown): void {
|
||||||
|
(this.delegate.onStart as (span: ReadableSpan, context: unknown) => void)(span, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnd(span: ReadableSpan): void {
|
||||||
|
const route = span.attributes['http.route'] || span.attributes['http.target'];
|
||||||
|
if (typeof route === 'string' && ignoredRoutes.has(route)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callback = span.attributes['nestjs.callback'];
|
||||||
|
if (typeof callback === 'string' && ignoredCallbacks.has(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.delegate.onEnd(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown(): Promise<void> {
|
||||||
|
return this.delegate.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
forceFlush(): Promise<void> {
|
||||||
|
return this.delegate.forceFlush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect worker type to determine metrics port:
|
||||||
|
// - Forked child (API worker): process.send exists, use IMMICH_API_METRICS_PORT
|
||||||
|
// - Worker thread (microservices): !isMainThread, use IMMICH_MICROSERVICES_METRICS_PORT
|
||||||
|
// - Main process: don't start metrics (avoid port conflicts with workers)
|
||||||
|
const isForkedChild = typeof process.send === 'function';
|
||||||
|
const isWorkerThread = !isMainThread;
|
||||||
|
const isWorkerProcess = isForkedChild || isWorkerThread;
|
||||||
|
|
||||||
|
const telemetryInclude = process.env.IMMICH_TELEMETRY_INCLUDE;
|
||||||
|
const tracingEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
||||||
|
const metricsEnabled = telemetryInclude && telemetryInclude.length > 0;
|
||||||
|
|
||||||
|
// Only start SDK in worker processes (not main/supervisor process)
|
||||||
|
if (isWorkerProcess && (tracingEndpoint || metricsEnabled)) {
|
||||||
|
const workerType = isForkedChild ? 'api' : 'microservices';
|
||||||
|
|
||||||
|
const attributes: Record<string, string> = {
|
||||||
|
[ATTR_SERVICE_NAME]: `immich-${workerType}`,
|
||||||
|
[ATTR_SERVICE_VERSION]: process.env.npm_package_version || 'dev',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.IMMICH_ENV) {
|
||||||
|
attributes['deployment.environment.name'] = process.env.IMMICH_ENV;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.IMMICH_SOURCE_COMMIT) {
|
||||||
|
attributes[ATTR_VCS_REPOSITORY_REF_REVISION] = process.env.IMMICH_SOURCE_COMMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.IMMICH_REPOSITORY_URL) {
|
||||||
|
attributes[ATTR_VCS_REPOSITORY_URL_FULL] = process.env.IMMICH_REPOSITORY_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.IMMICH_BUILD) {
|
||||||
|
attributes['immich.build'] = process.env.IMMICH_BUILD;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.IMMICH_SOURCE_REF) {
|
||||||
|
attributes['immich.source_ref'] = process.env.IMMICH_SOURCE_REF;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.IMMICH_BUILD_IMAGE) {
|
||||||
|
attributes['immich.build_image'] = process.env.IMMICH_BUILD_IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = resourceFromAttributes(attributes);
|
||||||
|
|
||||||
|
const aggregationBoundaries = [
|
||||||
|
0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10_000,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Use appropriate port based on worker type
|
||||||
|
const defaultPort = isForkedChild ? '8081' : '8082';
|
||||||
|
const portEnvVar = isForkedChild ? 'IMMICH_API_METRICS_PORT' : 'IMMICH_MICROSERVICES_METRICS_PORT';
|
||||||
|
const metricsPort = Number.parseInt(process.env[portEnvVar] || defaultPort, 10);
|
||||||
|
|
||||||
|
const traceExporter = tracingEndpoint ? new OTLPTraceExporter({ url: `${tracingEndpoint}/v1/traces` }) : undefined;
|
||||||
|
const spanProcessor = traceExporter ? new FilteringSpanProcessor(new BatchSpanProcessor(traceExporter)) : undefined;
|
||||||
|
|
||||||
|
const metricReaders = [];
|
||||||
|
if (metricsEnabled) {
|
||||||
|
metricReaders.push(new PrometheusExporter({ port: metricsPort }));
|
||||||
|
}
|
||||||
|
if (tracingEndpoint) {
|
||||||
|
metricReaders.push(
|
||||||
|
new PeriodicExportingMetricReader({
|
||||||
|
exporter: new OTLPMetricExporter({ url: `${tracingEndpoint}/v1/metrics` }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sdk = new NodeSDK({
|
||||||
|
resource,
|
||||||
|
spanProcessors: spanProcessor ? [spanProcessor] : undefined,
|
||||||
|
metricReaders: metricReaders.length > 0 ? metricReaders : undefined,
|
||||||
|
views: metricsEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
instrumentName: '*',
|
||||||
|
instrumentUnit: 'ms',
|
||||||
|
aggregation: {
|
||||||
|
type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
|
||||||
|
options: { boundaries: aggregationBoundaries },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
|
contextManager: new AsyncLocalStorageContextManager(),
|
||||||
|
instrumentations: [new HttpInstrumentation(), new IORedisInstrumentation(), new NestInstrumentation()],
|
||||||
|
});
|
||||||
|
|
||||||
|
sdk.start();
|
||||||
|
}
|
||||||
+11
-1
@@ -10,6 +10,7 @@ import {
|
|||||||
AssetType,
|
AssetType,
|
||||||
DatabaseSslMode,
|
DatabaseSslMode,
|
||||||
ExifOrientation,
|
ExifOrientation,
|
||||||
|
GeneratedImageType,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
JobName,
|
JobName,
|
||||||
MemoryType,
|
MemoryType,
|
||||||
@@ -64,7 +65,10 @@ export interface DecodeToBufferOptions extends DecodeImageOptions {
|
|||||||
orientation?: ExifOrientation;
|
orientation?: ExifOrientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GenerateThumbnailOptions = Pick<ImageOptions, 'format' | 'quality' | 'progressive'> & DecodeToBufferOptions;
|
export type GenerateThumbnailOptions = Pick<ImageOptions, 'format' | 'quality' | 'progressive'> &
|
||||||
|
DecodeToBufferOptions & {
|
||||||
|
imageType?: GeneratedImageType;
|
||||||
|
};
|
||||||
|
|
||||||
export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo };
|
export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo };
|
||||||
|
|
||||||
@@ -178,8 +182,14 @@ export type ConcurrentQueueName = Exclude<
|
|||||||
export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] };
|
export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] };
|
||||||
export type JobOf<T extends JobName> = Jobs[T];
|
export type JobOf<T extends JobName> = Jobs[T];
|
||||||
|
|
||||||
|
export interface TraceContext {
|
||||||
|
traceparent: string;
|
||||||
|
tracestate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IBaseJob {
|
export interface IBaseJob {
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
|
traceContext?: TraceContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDelayedJob extends IBaseJob {
|
export interface IDelayedJob extends IBaseJob {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AliasedRawBuilder,
|
AliasedRawBuilder,
|
||||||
DeduplicateJoinsPlugin,
|
DeduplicateJoinsPlugin,
|
||||||
|
Dialect,
|
||||||
Expression,
|
Expression,
|
||||||
ExpressionBuilder,
|
ExpressionBuilder,
|
||||||
ExpressionWrapper,
|
ExpressionWrapper,
|
||||||
@@ -22,6 +23,7 @@ import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } fr
|
|||||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
|
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
|
||||||
|
import { createInstrumentedDriver } from 'src/utils/otel-kysely-plugin';
|
||||||
|
|
||||||
type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
|
type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
|
||||||
|
|
||||||
@@ -59,21 +61,32 @@ export const asPostgresConnectionConfig = (params: DatabaseConnectionParams) =>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DATABASE_MAX_CONNECTIONS = 10;
|
||||||
|
|
||||||
|
function createInstrumentedDialect(innerDialect: Dialect, maxConnections: number): Dialect {
|
||||||
|
return {
|
||||||
|
createAdapter: () => innerDialect.createAdapter(),
|
||||||
|
createDriver: () => createInstrumentedDriver(innerDialect.createDriver(), maxConnections),
|
||||||
|
createIntrospector: (db) => innerDialect.createIntrospector(db),
|
||||||
|
createQueryCompiler: () => innerDialect.createQueryCompiler(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const getKyselyConfig = (
|
export const getKyselyConfig = (
|
||||||
params: DatabaseConnectionParams,
|
params: DatabaseConnectionParams,
|
||||||
options: Partial<postgres.Options<Record<string, postgres.PostgresType>>> = {},
|
options: Partial<postgres.Options<Record<string, postgres.PostgresType>>> = {},
|
||||||
): KyselyConfig => {
|
): KyselyConfig => {
|
||||||
const config = asPostgresConnectionConfig(params);
|
const config = asPostgresConnectionConfig(params);
|
||||||
|
const maxConnections = options.max ?? DATABASE_MAX_CONNECTIONS;
|
||||||
|
|
||||||
return {
|
const innerDialect = new PostgresJSDialect({
|
||||||
dialect: new PostgresJSDialect({
|
|
||||||
postgres: postgres({
|
postgres: postgres({
|
||||||
onnotice: (notice: Notice) => {
|
onnotice: (notice: Notice) => {
|
||||||
if (notice['severity'] !== 'NOTICE') {
|
if (notice['severity'] !== 'NOTICE') {
|
||||||
console.warn('Postgres notice:', notice);
|
console.warn('Postgres notice:', notice);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
max: 10,
|
max: maxConnections,
|
||||||
types: {
|
types: {
|
||||||
date: {
|
date: {
|
||||||
to: 1184,
|
to: 1184,
|
||||||
@@ -99,8 +112,12 @@ export const getKyselyConfig = (
|
|||||||
ssl: config.ssl,
|
ssl: config.ssl,
|
||||||
...options,
|
...options,
|
||||||
}),
|
}),
|
||||||
}),
|
});
|
||||||
log(event) {
|
|
||||||
|
return {
|
||||||
|
dialect: createInstrumentedDialect(innerDialect, maxConnections),
|
||||||
|
plugins: [],
|
||||||
|
log: (event) => {
|
||||||
if (event.level === 'error') {
|
if (event.level === 'error') {
|
||||||
console.error('Query failed:', {
|
console.error('Query failed:', {
|
||||||
durationMs: event.queryDurationMillis,
|
durationMs: event.queryDurationMillis,
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { HttpException, StreamableFile } from '@nestjs/common';
|
import { HttpException, StreamableFile } from '@nestjs/common';
|
||||||
|
import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
|
||||||
import { NextFunction, Response } from 'express';
|
import { NextFunction, Response } from 'express';
|
||||||
import { access, constants } from 'node:fs/promises';
|
import { access, constants, stat } from 'node:fs/promises';
|
||||||
import { basename, extname } from 'node:path';
|
import { basename, extname } from 'node:path';
|
||||||
|
import { PassThrough } from 'node:stream';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { CacheControl } from 'src/enum';
|
import { CacheControl } from 'src/enum';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { ImmichReadStream } from 'src/repositories/storage.repository';
|
import { ImmichReadStream } from 'src/repositories/storage.repository';
|
||||||
import { isConnectionAborted } from 'src/utils/misc';
|
import { isConnectionAborted } from 'src/utils/misc';
|
||||||
|
|
||||||
|
const fileTracer = trace.getTracer('immich-file');
|
||||||
|
|
||||||
export function getFileNameWithoutExtension(path: string): string {
|
export function getFileNameWithoutExtension(path: string): string {
|
||||||
return basename(path, extname(path));
|
return basename(path, extname(path));
|
||||||
}
|
}
|
||||||
@@ -49,6 +53,8 @@ export const sendFile = async (
|
|||||||
const _sendFile = (path: string, options: SendFileOptions) =>
|
const _sendFile = (path: string, options: SendFileOptions) =>
|
||||||
promisify<string, SendFileOptions>(res.sendFile).bind(res)(path, options);
|
promisify<string, SendFileOptions>(res.sendFile).bind(res)(path, options);
|
||||||
|
|
||||||
|
const span = fileTracer.startSpan('file.send', { kind: SpanKind.INTERNAL }, context.active());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = await handler();
|
const file = await handler();
|
||||||
const cacheControlHeader = cacheControlHeaders[file.cacheControl];
|
const cacheControlHeader = cacheControlHeaders[file.cacheControl];
|
||||||
@@ -64,10 +70,24 @@ export const sendFile = async (
|
|||||||
|
|
||||||
await access(file.path, constants.R_OK);
|
await access(file.path, constants.R_OK);
|
||||||
|
|
||||||
return await _sendFile(file.path, { dotfiles: 'allow' });
|
// Get file size for telemetry
|
||||||
|
const fileStats = await stat(file.path);
|
||||||
|
|
||||||
|
span.setAttribute('file.path', file.path);
|
||||||
|
span.setAttribute('file.size', fileStats.size);
|
||||||
|
span.setAttribute('file.content_type', file.contentType);
|
||||||
|
span.setAttribute('file.name', file.fileName || basename(file.path));
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
|
||||||
|
const result = await _sendFile(file.path, { dotfiles: 'allow' });
|
||||||
|
span.end();
|
||||||
|
return result;
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
// ignore client-closed connection
|
// ignore client-closed connection
|
||||||
if (isConnectionAborted(error) || res.headersSent) {
|
if (isConnectionAborted(error) || res.headersSent) {
|
||||||
|
span.setAttribute('file.connection_aborted', true);
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
span.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,11 +96,46 @@ export const sendFile = async (
|
|||||||
logger.error(`Unable to send file: ${error}`, error.stack);
|
logger.error(`Unable to send file: ${error}`, error.stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||||
|
if (error instanceof Error) {
|
||||||
|
span.recordException(error);
|
||||||
|
}
|
||||||
|
span.end();
|
||||||
|
|
||||||
res.header('Cache-Control', 'none');
|
res.header('Cache-Control', 'none');
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
|
export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
|
||||||
return new StreamableFile(stream, { type, length });
|
const span = fileTracer.startSpan('file.stream', { kind: SpanKind.INTERNAL }, context.active());
|
||||||
|
|
||||||
|
if (length !== undefined) {
|
||||||
|
span.setAttribute('file.size', length);
|
||||||
|
}
|
||||||
|
if (type) {
|
||||||
|
span.setAttribute('file.content_type', type);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytesStreamed = 0;
|
||||||
|
|
||||||
|
const trackedStream = new PassThrough();
|
||||||
|
trackedStream.on('data', (chunk: Buffer) => {
|
||||||
|
bytesStreamed += chunk.length;
|
||||||
|
});
|
||||||
|
trackedStream.on('end', () => {
|
||||||
|
span.setAttribute('file.bytes_streamed', bytesStreamed);
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
span.end();
|
||||||
|
});
|
||||||
|
trackedStream.on('error', (error: Error) => {
|
||||||
|
span.setAttribute('file.bytes_streamed', bytesStreamed);
|
||||||
|
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||||
|
span.recordException(error);
|
||||||
|
span.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.pipe(trackedStream);
|
||||||
|
|
||||||
|
return new StreamableFile(trackedStream, { type, length });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { context, Span, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
|
||||||
|
|
||||||
|
const tracer = trace.getTracer('immich');
|
||||||
|
|
||||||
|
type TracedOptions = {
|
||||||
|
name: string;
|
||||||
|
kind?: SpanKind;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Traced(options: string | TracedOptions) {
|
||||||
|
const { name: spanName, kind = SpanKind.INTERNAL } =
|
||||||
|
typeof options === 'string' ? { name: options } : options;
|
||||||
|
|
||||||
|
return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
const originalMethod = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function (...args: unknown[]) {
|
||||||
|
return tracer.startActiveSpan(spanName, { kind }, context.active(), async (span: Span) => {
|
||||||
|
try {
|
||||||
|
const result = await originalMethod.apply(this, args);
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
||||||
|
span.recordException(error as Error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
span.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
import { context, metrics, Span, SpanKind, SpanStatusCode, trace, ValueType } from '@opentelemetry/api';
|
||||||
|
import type { CompiledQuery, DatabaseConnection, Driver, QueryResult, TransactionSettings } from 'kysely';
|
||||||
|
|
||||||
|
const dbTracer = trace.getTracer('immich-db');
|
||||||
|
const instrumentedConnections = new WeakSet<DatabaseConnection>();
|
||||||
|
const traceDbConnections = process.env.IMMICH_OTEL_TRACE_DB_CONNECTIONS === 'true';
|
||||||
|
|
||||||
|
let poolMetricsInitialized = false;
|
||||||
|
|
||||||
|
function initializePoolMetrics(maxConnections: number) {
|
||||||
|
if (poolMetricsInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
poolMetricsInitialized = true;
|
||||||
|
|
||||||
|
const meter = metrics.getMeter('immich-db');
|
||||||
|
|
||||||
|
meter
|
||||||
|
.createObservableGauge('db.client.connections.max', {
|
||||||
|
description: 'Maximum number of connections in the pool',
|
||||||
|
unit: '{connection}',
|
||||||
|
valueType: ValueType.INT,
|
||||||
|
})
|
||||||
|
.addCallback((observable) => {
|
||||||
|
observable.observe(maxConnections, { 'db.system': 'postgresql' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOperationName(sql: string): string {
|
||||||
|
const trimmed = sql.trimStart().toUpperCase();
|
||||||
|
if (trimmed.startsWith('SELECT')) {
|
||||||
|
return 'SELECT';
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith('INSERT')) {
|
||||||
|
return 'INSERT';
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith('UPDATE')) {
|
||||||
|
return 'UPDATE';
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith('DELETE')) {
|
||||||
|
return 'DELETE';
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith('WITH')) {
|
||||||
|
return 'WITH';
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith('BEGIN')) {
|
||||||
|
return 'BEGIN';
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith('COMMIT')) {
|
||||||
|
return 'COMMIT';
|
||||||
|
}
|
||||||
|
if (trimmed.startsWith('ROLLBACK')) {
|
||||||
|
return 'ROLLBACK';
|
||||||
|
}
|
||||||
|
return 'QUERY';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableName(sql: string): string | undefined {
|
||||||
|
const fromMatch = sql.match(/\bFROM\s+"?(\w+)"?/i);
|
||||||
|
if (fromMatch) {
|
||||||
|
return fromMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const intoMatch = sql.match(/\bINTO\s+"?(\w+)"?/i);
|
||||||
|
if (intoMatch) {
|
||||||
|
return intoMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMatch = sql.match(/\bUPDATE\s+"?(\w+)"?/i);
|
||||||
|
if (updateMatch) {
|
||||||
|
return updateMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInstrumentedConnection(
|
||||||
|
connection: DatabaseConnection,
|
||||||
|
transactionSpan?: Span,
|
||||||
|
): DatabaseConnection {
|
||||||
|
if (instrumentedConnections.has(connection)) {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instrumentedConnection: DatabaseConnection = {
|
||||||
|
async executeQuery<R>(compiledQuery: CompiledQuery): Promise<QueryResult<R>> {
|
||||||
|
const sql = compiledQuery.sql;
|
||||||
|
const operationName = getOperationName(sql);
|
||||||
|
const tableName = getTableName(sql);
|
||||||
|
const spanName = tableName ? `${operationName} ${tableName}` : operationName;
|
||||||
|
|
||||||
|
const parentContext = transactionSpan
|
||||||
|
? trace.setSpan(context.active(), transactionSpan)
|
||||||
|
: context.active();
|
||||||
|
|
||||||
|
return dbTracer.startActiveSpan(spanName, { kind: SpanKind.CLIENT }, parentContext, async (span) => {
|
||||||
|
try {
|
||||||
|
span.setAttribute('db.system', 'postgresql');
|
||||||
|
span.setAttribute('db.operation.name', operationName);
|
||||||
|
span.setAttribute('db.query.text', sql);
|
||||||
|
|
||||||
|
if (tableName) {
|
||||||
|
span.setAttribute('db.collection.name', tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compiledQuery.parameters.length > 0) {
|
||||||
|
span.setAttribute('db.query.parameter_count', compiledQuery.parameters.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await connection.executeQuery<R>(compiledQuery);
|
||||||
|
|
||||||
|
span.setAttribute('db.response.row_count', result.rows.length);
|
||||||
|
|
||||||
|
if (result.numAffectedRows !== undefined) {
|
||||||
|
span.setAttribute('db.operation.affected_rows', Number(result.numAffectedRows));
|
||||||
|
}
|
||||||
|
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
span.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
span.recordException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
span.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async *streamQuery<R>(
|
||||||
|
compiledQuery: CompiledQuery,
|
||||||
|
chunkSize: number,
|
||||||
|
): AsyncIterableIterator<QueryResult<R>> {
|
||||||
|
const sql = compiledQuery.sql;
|
||||||
|
const operationName = getOperationName(sql);
|
||||||
|
const tableName = getTableName(sql);
|
||||||
|
const spanName = tableName ? `STREAM ${operationName} ${tableName}` : `STREAM ${operationName}`;
|
||||||
|
|
||||||
|
const parentContext = transactionSpan
|
||||||
|
? trace.setSpan(context.active(), transactionSpan)
|
||||||
|
: context.active();
|
||||||
|
|
||||||
|
const span = dbTracer.startSpan(spanName, { kind: SpanKind.CLIENT }, parentContext);
|
||||||
|
|
||||||
|
span.setAttribute('db.system', 'postgresql');
|
||||||
|
span.setAttribute('db.operation.name', operationName);
|
||||||
|
span.setAttribute('db.query.text', sql);
|
||||||
|
span.setAttribute('db.stream.chunk_size', chunkSize);
|
||||||
|
|
||||||
|
if (tableName) {
|
||||||
|
span.setAttribute('db.collection.name', tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalRows = 0;
|
||||||
|
let chunkCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const chunk of connection.streamQuery<R>(compiledQuery, chunkSize)) {
|
||||||
|
chunkCount++;
|
||||||
|
totalRows += chunk.rows.length;
|
||||||
|
yield chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.setAttribute('db.stream.chunk_count', chunkCount);
|
||||||
|
span.setAttribute('db.stream.total_rows', totalRows);
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
} catch (error) {
|
||||||
|
span.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
span.recordException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
span.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
instrumentedConnections.add(instrumentedConnection);
|
||||||
|
return instrumentedConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactionSpans = new WeakMap<DatabaseConnection, Span>();
|
||||||
|
const unwrappedConnections = new WeakMap<DatabaseConnection, DatabaseConnection>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instrumented driver that wraps a Kysely driver with OpenTelemetry tracing.
|
||||||
|
* Provides:
|
||||||
|
* - Query spans with row counts and affected rows
|
||||||
|
* - Streaming query spans with chunk metrics
|
||||||
|
* - Transaction spans that parent query spans
|
||||||
|
* - Connection acquisition timing
|
||||||
|
*/
|
||||||
|
export function createInstrumentedDriver(driver: Driver, maxConnections: number): Driver {
|
||||||
|
initializePoolMetrics(maxConnections);
|
||||||
|
|
||||||
|
return new Proxy(driver, {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
if (prop === 'init' || prop === 'destroy') {
|
||||||
|
const method = Reflect.get(target, prop, target);
|
||||||
|
if (typeof method === 'function') {
|
||||||
|
return method.bind(target);
|
||||||
|
}
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === 'acquireConnection') {
|
||||||
|
return async function (): Promise<DatabaseConnection> {
|
||||||
|
if (!traceDbConnections) {
|
||||||
|
const connection = await target.acquireConnection();
|
||||||
|
const instrumentedConnection = createInstrumentedConnection(connection);
|
||||||
|
unwrappedConnections.set(instrumentedConnection, connection);
|
||||||
|
return instrumentedConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbTracer.startActiveSpan(
|
||||||
|
'DB Connection Acquire',
|
||||||
|
{ kind: SpanKind.CLIENT },
|
||||||
|
context.active(),
|
||||||
|
async (span) => {
|
||||||
|
try {
|
||||||
|
span.setAttribute('db.system', 'postgresql');
|
||||||
|
|
||||||
|
const connection = await target.acquireConnection();
|
||||||
|
const instrumentedConnection = createInstrumentedConnection(connection);
|
||||||
|
|
||||||
|
unwrappedConnections.set(instrumentedConnection, connection);
|
||||||
|
|
||||||
|
span.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
return instrumentedConnection;
|
||||||
|
} catch (error) {
|
||||||
|
span.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
span.recordException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
span.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === 'beginTransaction') {
|
||||||
|
return async function (
|
||||||
|
connection: DatabaseConnection,
|
||||||
|
settings: TransactionSettings,
|
||||||
|
): Promise<void> {
|
||||||
|
const unwrapped = unwrappedConnections.get(connection) ?? connection;
|
||||||
|
|
||||||
|
const transactionSpan = dbTracer.startSpan(
|
||||||
|
'DB Transaction',
|
||||||
|
{ kind: SpanKind.CLIENT },
|
||||||
|
context.active(),
|
||||||
|
);
|
||||||
|
transactionSpan.setAttribute('db.system', 'postgresql');
|
||||||
|
transactionSpan.setAttribute('db.operation.name', 'TRANSACTION');
|
||||||
|
|
||||||
|
if (settings.isolationLevel) {
|
||||||
|
transactionSpan.setAttribute('db.transaction.isolation_level', settings.isolationLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionSpans.set(connection, transactionSpan);
|
||||||
|
|
||||||
|
const instrumentedForTransaction = createInstrumentedConnection(unwrapped, transactionSpan);
|
||||||
|
unwrappedConnections.set(instrumentedForTransaction, unwrapped);
|
||||||
|
|
||||||
|
Object.assign(connection, instrumentedForTransaction);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await target.beginTransaction(unwrapped, settings);
|
||||||
|
} catch (error) {
|
||||||
|
transactionSpan.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
transactionSpan.recordException(error);
|
||||||
|
}
|
||||||
|
transactionSpan.end();
|
||||||
|
transactionSpans.delete(connection);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === 'commitTransaction') {
|
||||||
|
return async function (connection: DatabaseConnection): Promise<void> {
|
||||||
|
const unwrapped = unwrappedConnections.get(connection) ?? connection;
|
||||||
|
const transactionSpan = transactionSpans.get(connection);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await target.commitTransaction(unwrapped);
|
||||||
|
transactionSpan?.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
} catch (error) {
|
||||||
|
transactionSpan?.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
transactionSpan?.recordException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
transactionSpan?.end();
|
||||||
|
transactionSpans.delete(connection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === 'rollbackTransaction') {
|
||||||
|
return async function (connection: DatabaseConnection): Promise<void> {
|
||||||
|
const unwrapped = unwrappedConnections.get(connection) ?? connection;
|
||||||
|
const transactionSpan = transactionSpans.get(connection);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await target.rollbackTransaction(unwrapped);
|
||||||
|
transactionSpan?.setAttribute('db.transaction.rollback', true);
|
||||||
|
transactionSpan?.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
} catch (error) {
|
||||||
|
transactionSpan?.setStatus({
|
||||||
|
code: SpanStatusCode.ERROR,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
transactionSpan?.recordException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
transactionSpan?.end();
|
||||||
|
transactionSpans.delete(connection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === 'releaseConnection') {
|
||||||
|
return async function (connection: DatabaseConnection): Promise<void> {
|
||||||
|
const unwrapped = unwrappedConnections.get(connection) ?? connection;
|
||||||
|
unwrappedConnections.delete(connection);
|
||||||
|
transactionSpans.delete(connection);
|
||||||
|
return target.releaseConnection(unwrapped);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reflect.get(target, prop, receiver);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Must be first import - sets up OpenTelemetry SDK before any modules load
|
||||||
|
import 'src/telemetry-preload';
|
||||||
|
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { isMainThread } from 'node:worker_threads';
|
import { isMainThread } from 'node:worker_threads';
|
||||||
import { MicroservicesModule } from 'src/app.module';
|
import { MicroservicesModule } from 'src/app.module';
|
||||||
|
|||||||
Reference in New Issue
Block a user