From 4da1951099b8300f947a99dd5393457ffff4da18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 17 Feb 2026 21:13:03 -0300 Subject: [PATCH 01/26] chore: add grafana image --- apps/backend/docker-compose.yml | 10 + apps/backend/package.json | 8 +- apps/backend/src/telemetry/otel.ts | 28 ++ pnpm-lock.yaml | 530 +++++++++++++++++++++++++++-- 4 files changed, 550 insertions(+), 26 deletions(-) create mode 100644 apps/backend/src/telemetry/otel.ts diff --git a/apps/backend/docker-compose.yml b/apps/backend/docker-compose.yml index 4ada7330..fa85b803 100644 --- a/apps/backend/docker-compose.yml +++ b/apps/backend/docker-compose.yml @@ -56,6 +56,16 @@ services: - localstack_data:/var/lib/localstack networks: - plotwist_network + + grafana: + image: grafana/otel-lgtm + container_name: plotwist-grafana + ports: + - 12345:3000 + - 4317:4317 + - 4318:4318 + networks: + - plotwist_network volumes: redis-data: diff --git a/apps/backend/package.json b/apps/backend/package.json index fea1569a..0a6d913d 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -36,6 +36,12 @@ "@fastify/redis": "^7.1.0", "@fastify/swagger": "^9.6.1", "@fastify/swagger-ui": "^5.2.4", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.211.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.211.0", + "@opentelemetry/resources": "^2.5.0", + "@opentelemetry/sdk-metrics": "^2.5.0", + "@opentelemetry/sdk-node": "^0.211.0", + "@opentelemetry/semantic-conventions": "^1.39.0", "@plotwist_app/tmdb": "^0.2.5", "@react-email/components": "^1.0.3", "@swc/core": "^1.15.8", @@ -57,9 +63,9 @@ "fastify-type-provider-zod": "^6.1.0", "google-auth-library": "^9.14.0", "https": "^1.0.0", + "ioredis": "^5.8.2", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", - "ioredis": "^5.8.2", "node-cron": "^4.2.1", "openai": "^6.15.0", "pino": "^10.1.0", diff --git a/apps/backend/src/telemetry/otel.ts b/apps/backend/src/telemetry/otel.ts new file mode 100644 index 00000000..0b8483fc --- /dev/null +++ b/apps/backend/src/telemetry/otel.ts @@ -0,0 +1,28 @@ +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto' +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' +import { NodeSDK } from '@opentelemetry/sdk-node' +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, +} from '@opentelemetry/semantic-conventions' + +const sdk = new NodeSDK({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'plotwist-api', + [ATTR_SERVICE_VERSION]: '0.1.0', + }), + traceExporter: new OTLPTraceExporter({ + url: 'http://localhost:4318/v1/traces', + headers: {}, + }), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ + url: 'http://localhost:4318/v1/metrics', + headers: {}, + }), + }), +}) + +sdk.start() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 288e4e77..2878c087 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,21 @@ importers: '@fastify/swagger-ui': specifier: ^5.2.4 version: 5.2.5 + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.5.0 + version: 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.5.0 + version: 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.39.0 + version: 1.39.0 '@plotwist_app/tmdb': specifier: ^0.2.5 version: 0.2.5(@swc/core@1.15.11)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) @@ -97,10 +112,10 @@ importers: version: 4.1.0 drizzle-orm: specifier: ^0.45.1 - version: 0.45.1(postgres@3.4.8) + version: 0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8) drizzle-zod: specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.45.1(postgres@3.4.8))(zod@4.3.6) + version: 0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8))(zod@4.3.6) env-paths: specifier: ^3.0.0 version: 3.0.0 @@ -221,7 +236,7 @@ importers: version: 6.0.5(typescript@5.9.3)(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^4.0.16 - version: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.3.5 version: 4.3.6 @@ -386,25 +401,25 @@ importers: version: 1.0.0 next: specifier: ^16.1.1 - version: 16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-auth: specifier: ^4.24.13 - version: 4.24.13(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 4.24.13(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-view-transitions: specifier: ^0.3.5 - version: 0.3.5(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.3.5(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nextjs-toploader: specifier: ^3.9.17 - version: 3.9.17(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 3.9.17(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nprogress: specifier: ^0.2.0 version: 0.2.0 nuqs: specifier: ^2.8.6 - version: 2.8.7(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 2.8.7(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.3 version: 19.2.4 @@ -534,7 +549,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.16 - version: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) packages/typescript-config: {} @@ -2570,6 +2585,174 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.211.0': + resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/configuration@0.211.0': + resolution: {integrity: sha512-PNsCkzsYQKyv8wiUIsH+loC4RYyblOaDnVASBtKS22hK55ToWs2UP6IsrcfSWWn54wWTvVe2gnfwz67Pvrxf2Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.5.0': + resolution: {integrity: sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@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.211.0': + resolution: {integrity: sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.211.0': + resolution: {integrity: sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.211.0': + resolution: {integrity: sha512-kMvfKMtY5vJDXeLnwhrZMEwhZ2PN8sROXmzacFU/Fnl4Z79CMrOaL7OE+5X3SObRYlDUa7zVqaXp9ZetYCxfDQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0': + resolution: {integrity: sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@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.211.0': + resolution: {integrity: sha512-61iNbffEpyZv/abHaz3BQM3zUtA2kVIDBM+0dS9RK68ML0QFLRGYa50xVMn2PYMToyfszEPEgFC3ypGae2z8FA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.211.0': + resolution: {integrity: sha512-cD0WleEL3TPqJbvxwz5MVdVJ82H8jl8mvMad4bNU24cB5SH2mRW5aMLDTuV4614ll46R//R3RMmci26mc2L99g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.211.0': + resolution: {integrity: sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@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.211.0': + resolution: {integrity: sha512-DkjXwbPiqpcPlycUojzG2RmR0/SIK8Gi9qWO9znNvSqgzrnAIE9x2n6yPfpZ+kWHZGafvsvA1lVXucTyyQa5Kg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.5.0': + resolution: {integrity: sha512-bk9VJgFgUAzkZzU8ZyXBSWiUGLOM3mZEgKJ1+jsZclhRnAoDNf+YBdq+G9R3cP0+TKjjWad+vVrY/bE/vRR9lA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation@0.211.0': + resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@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.211.0': + resolution: {integrity: sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@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.5.0': + resolution: {integrity: sha512-g10m4KD73RjHrSvUge+sUxUl8m4VlgnGc6OKvo68a4uMfaLjdFU+AULfvMQE/APq38k92oGUxEzBsAZ8RN/YHg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.5.0': + resolution: {integrity: sha512-t70ErZCncAR/zz5AcGkL0TF25mJiK1FfDPEQCgreyAHZ+mRJ/bNUiCnImIBDlP3mSDXy6N09DbUEKq0ktW98Hg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.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.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.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.211.0': + resolution: {integrity: sha512-+s1eGjoqmPCMptNxcJJD4IxbWJKNLOQFNKhpwkzi2gLkEbCj6LzSHJNhPcLeBrBlBLtlSpibM+FuS7fjZ8SSFQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@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.5.0': + resolution: {integrity: sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} + engines: {node: '>=14'} + '@orval/angular@7.21.0': resolution: {integrity: sha512-AGelR1FfuimtIBBVccUI9MyjNOalLEyJFog8a94thFiqRGtz0JFIPnd8+IqRcmw3wE370PKQQMCqZl1WkjZi8w==} @@ -4456,6 +4639,11 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4887,6 +5075,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -6090,6 +6281,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + inflected@2.1.0: resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==} @@ -6838,6 +7032,9 @@ packages: mnemonist@0.40.3: resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + motion-dom@12.29.2: resolution: {integrity: sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA==} @@ -7383,6 +7580,10 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} + protobufjs@8.0.0: + resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} + engines: {node: '>=12.0.0'} + proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -7683,6 +7884,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -11005,6 +11210,241 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.211.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/configuration@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + yaml: 2.8.2 + + '@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@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-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@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/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@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/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-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-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-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.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.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-prometheus@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/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@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-grpc-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-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.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-zipkin@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-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@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.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@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/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.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@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)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.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.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.211.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/configuration': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 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) + '@opentelemetry/sdk-trace-node': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + transitivePeerDependencies: + - supports-color + + '@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.39.0 + + '@opentelemetry/sdk-trace-node@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.39.0': {} + '@orval/angular@7.21.0(openapi-types@12.1.3)(typescript@5.9.3)': dependencies: '@orval/core': 7.21.0(openapi-types@12.1.3)(typescript@5.9.3) @@ -13102,7 +13542,7 @@ snapshots: magicast: 0.5.1 obug: 2.1.1 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -13118,7 +13558,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) '@vitest/expect@4.0.18': dependencies: @@ -13164,7 +13604,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) '@vitest/utils@4.0.18': dependencies: @@ -13188,6 +13628,10 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -13622,6 +14066,8 @@ snapshots: dependencies: consola: 3.4.2 + cjs-module-lexer@2.2.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -14077,13 +14523,14 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(postgres@3.4.8): + drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8): optionalDependencies: + '@opentelemetry/api': 1.9.0 postgres: 3.4.8 - drizzle-zod@0.8.3(drizzle-orm@0.45.1(postgres@3.4.8))(zod@4.3.6): + drizzle-zod@0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8))(zod@4.3.6): dependencies: - drizzle-orm: 0.45.1(postgres@3.4.8) + drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8) zod: 4.3.6 dunder-proto@1.0.1: @@ -14977,6 +15424,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + inflected@2.1.0: {} inherits@2.0.4: {} @@ -15899,6 +16353,8 @@ snapshots: dependencies: obliterator: 2.0.5 + module-details-from-path@1.0.4: {} + motion-dom@12.29.2: dependencies: motion-utils: 12.29.2 @@ -15953,13 +16409,13 @@ snapshots: netmask@2.0.2: {} - next-auth@4.24.13(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next-auth@4.24.13(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.28.6 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.28.3 @@ -15975,13 +16431,13 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next-view-transitions@0.3.5(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next-view-transitions@0.3.5(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - next: 16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 @@ -16000,14 +16456,15 @@ snapshots: '@next/swc-linux-x64-musl': 16.1.6 '@next/swc-win32-arm64-msvc': 16.1.6 '@next/swc-win32-x64-msvc': 16.1.6 + '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-toploader@3.9.17(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + nextjs-toploader@3.9.17(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - next: 16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nprogress: 0.2.0 prop-types: 15.8.1 react: 19.2.4 @@ -16064,12 +16521,12 @@ snapshots: dependencies: esm-env: 1.2.2 - nuqs@2.8.7(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + nuqs@2.8.7(next@16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.4 optionalDependencies: - next: 16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nypm@0.6.2: dependencies: @@ -16516,6 +16973,21 @@ snapshots: '@types/node': 25.1.0 long: 5.3.2 + protobufjs@8.0.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.1.0 + long: 5.3.2 + proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 @@ -16906,6 +17378,13 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + reselect@5.1.1: {} resend@6.9.1(@react-email/render@2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): @@ -17987,7 +18466,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.18(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.1.0)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(msw@2.12.7(@types/node@25.1.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.1.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -18010,6 +18489,7 @@ snapshots: vite: 7.3.1(@types/node@25.1.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 25.1.0 '@vitest/ui': 4.0.18(vitest@4.0.18) jsdom: 27.4.0 From 6af3b42d34f29af3096f958a379697987318d4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 17 Feb 2026 21:13:14 -0300 Subject: [PATCH 02/26] chore: update docker compose --- apps/backend/docker-compose.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/backend/docker-compose.yml b/apps/backend/docker-compose.yml index fa85b803..98125b87 100644 --- a/apps/backend/docker-compose.yml +++ b/apps/backend/docker-compose.yml @@ -57,13 +57,13 @@ services: networks: - plotwist_network - grafana: - image: grafana/otel-lgtm - container_name: plotwist-grafana - ports: - - 12345:3000 - - 4317:4317 - - 4318:4318 + grafana: + image: grafana/otel-lgtm + container_name: plotwist_grafana + ports: + - 12345:3000 + - 4317:4317 + - 4318:4318 networks: - plotwist_network From aa342ea83823c8a6ae8841936358a63305bc4ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 17 Feb 2026 21:37:31 -0300 Subject: [PATCH 03/26] feat: integrate OpenTelemetry for enhanced observability --- apps/backend/package.json | 1 + apps/backend/src/http/routes/healthcheck.ts | 18 ++++++++++++++++-- apps/backend/src/http/transform-schema.ts | 2 +- apps/backend/src/{ => infra}/telemetry/otel.ts | 2 ++ apps/backend/src/main.ts | 1 + pnpm-lock.yaml | 6 ++++++ 6 files changed, 27 insertions(+), 3 deletions(-) rename apps/backend/src/{ => infra}/telemetry/otel.ts (95%) diff --git a/apps/backend/package.json b/apps/backend/package.json index 0a6d913d..831a0363 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -36,6 +36,7 @@ "@fastify/redis": "^7.1.0", "@fastify/swagger": "^9.6.1", "@fastify/swagger-ui": "^5.2.4", + "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.211.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.211.0", "@opentelemetry/resources": "^2.5.0", diff --git a/apps/backend/src/http/routes/healthcheck.ts b/apps/backend/src/http/routes/healthcheck.ts index 752bb74b..b9b3f80d 100644 --- a/apps/backend/src/http/routes/healthcheck.ts +++ b/apps/backend/src/http/routes/healthcheck.ts @@ -1,4 +1,5 @@ -import type { FastifyInstance } from 'fastify' +import { trace } from '@opentelemetry/api' +import type { FastifyInstance, FastifyReply } from 'fastify' export const healthCheck = (app: FastifyInstance) => app.route({ @@ -6,6 +7,19 @@ export const healthCheck = (app: FastifyInstance) => url: '/health', config: { rateLimit: false }, handler: (_request, reply) => { - reply.send({ alive: true }) + return healthcheckController(reply) }, }) + +async function healthcheckController(reply: FastifyReply) { + const tracer = trace.getTracer('healthcheck') + + return tracer.startActiveSpan('healthcheck-controller', async span => { + span.setAttribute('http.method', 'GET') + span.setAttribute('http.url', '/health') + span.setAttribute('http.status_code', '200') + span.end() + + return reply.status(404).send({ error: 'Not Found' }) + }) +} diff --git a/apps/backend/src/http/transform-schema.ts b/apps/backend/src/http/transform-schema.ts index b5ff85c4..db25f211 100644 --- a/apps/backend/src/http/transform-schema.ts +++ b/apps/backend/src/http/transform-schema.ts @@ -5,7 +5,7 @@ export function transformSwaggerSchema( ): ReturnType { const { schema, url } = jsonSchemaTransform(data) - if (schema.consumes?.includes('multipart/form-data')) { + if (schema?.consumes?.includes('multipart/form-data')) { if (schema.body === undefined) { schema.body = { type: 'object', diff --git a/apps/backend/src/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts similarity index 95% rename from apps/backend/src/telemetry/otel.ts rename to apps/backend/src/infra/telemetry/otel.ts index 0b8483fc..8f4f8033 100644 --- a/apps/backend/src/telemetry/otel.ts +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -25,4 +25,6 @@ const sdk = new NodeSDK({ }), }) +console.log('Starting OTLP exporter') + sdk.start() diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 137b9f9a..ec6101b6 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,3 +1,4 @@ +import '@/infra/telemetry/otel' import { startServer } from './http/server' import { startWorkers } from './worker' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2878c087..361870eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,12 @@ importers: '@fastify/swagger-ui': specifier: ^5.2.4 version: 5.2.5 + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/exporter-metrics-otlp-proto': + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-proto': specifier: ^0.211.0 version: 0.211.0(@opentelemetry/api@1.9.0) From 22485e89126530230259d9f8fec6910f7856bb38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 17 Feb 2026 21:46:16 -0300 Subject: [PATCH 04/26] chore: encapluslate controllers inside a telemetry macro --- apps/backend/src/http/routes/feedback.ts | 4 +- apps/backend/src/http/routes/follow.ts | 11 +++-- apps/backend/src/http/routes/healthcheck.ts | 24 +++-------- apps/backend/src/http/routes/images.ts | 5 ++- apps/backend/src/http/routes/import.ts | 7 +++- apps/backend/src/http/routes/likes.ts | 9 ++-- apps/backend/src/http/routes/list-item.ts | 13 ++++-- apps/backend/src/http/routes/lists.ts | 17 ++++---- apps/backend/src/http/routes/login.ts | 7 +++- .../backend/src/http/routes/review-replies.ts | 10 +++-- apps/backend/src/http/routes/reviews.ts | 17 ++++---- apps/backend/src/http/routes/social-auth.ts | 11 ++++- apps/backend/src/http/routes/social-links.ts | 7 +++- apps/backend/src/http/routes/subscriptions.ts | 5 ++- apps/backend/src/http/routes/tmdb-proxy.ts | 6 ++- .../src/http/routes/user-activities.ts | 15 ++++--- apps/backend/src/http/routes/user-episodes.ts | 9 ++-- apps/backend/src/http/routes/user-items.ts | 26 +++++++----- apps/backend/src/http/routes/user-stats.ts | 42 ++++++++++++------- apps/backend/src/http/routes/users.ts | 25 ++++++----- apps/backend/src/http/routes/watch-entries.ts | 11 +++-- apps/backend/src/http/routes/webhook.ts | 5 ++- .../src/infra/telemetry/with-tracing.ts | 36 ++++++++++++++++ 23 files changed, 213 insertions(+), 109 deletions(-) create mode 100644 apps/backend/src/infra/telemetry/with-tracing.ts diff --git a/apps/backend/src/http/routes/feedback.ts b/apps/backend/src/http/routes/feedback.ts index 8c1ca2eb..c8745325 100644 --- a/apps/backend/src/http/routes/feedback.ts +++ b/apps/backend/src/http/routes/feedback.ts @@ -1,6 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createFeedbackController } from '../controllers/feedback-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { @@ -23,7 +25,7 @@ export async function feedbackRoutes(app: FastifyInstance) { response: createFeedbackResponseSchema, security: [{ bearerAuth: [] }], }, - handler: createFeedbackController, + handler: withTracing('create-feedback', createFeedbackController), }) ) } diff --git a/apps/backend/src/http/routes/follow.ts b/apps/backend/src/http/routes/follow.ts index 636e6e87..57a75c58 100644 --- a/apps/backend/src/http/routes/follow.ts +++ b/apps/backend/src/http/routes/follow.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createFollowController, deleteFollowController, @@ -34,7 +37,7 @@ export async function followsRoutes(app: FastifyInstance) { ], body: createFollowBodySchema, }, - handler: createFollowController, + handler: withTracing('create-follow', createFollowController), }) ) @@ -54,7 +57,7 @@ export async function followsRoutes(app: FastifyInstance) { querystring: getFollowQuerySchema, response: getFollowResponseSchema, }, - handler: getFollowController, + handler: withTracing('get-follow', getFollowController), }) ) @@ -73,7 +76,7 @@ export async function followsRoutes(app: FastifyInstance) { ], body: deleteFollowBodySchema, }, - handler: deleteFollowController, + handler: withTracing('delete-follow', deleteFollowController), }) ) @@ -88,7 +91,7 @@ export async function followsRoutes(app: FastifyInstance) { response: getFollowersResponseSchema, operationId: 'getFollowers', }, - handler: getFollowersController, + handler: withTracing('get-followers', getFollowersController), }) ) } diff --git a/apps/backend/src/http/routes/healthcheck.ts b/apps/backend/src/http/routes/healthcheck.ts index b9b3f80d..69aa52bc 100644 --- a/apps/backend/src/http/routes/healthcheck.ts +++ b/apps/backend/src/http/routes/healthcheck.ts @@ -1,25 +1,13 @@ -import { trace } from '@opentelemetry/api' -import type { FastifyInstance, FastifyReply } from 'fastify' +import type { FastifyInstance } from 'fastify' + +import { withTracing } from '@/infra/telemetry/with-tracing' export const healthCheck = (app: FastifyInstance) => app.route({ method: 'GET', url: '/health', config: { rateLimit: false }, - handler: (_request, reply) => { - return healthcheckController(reply) - }, - }) - -async function healthcheckController(reply: FastifyReply) { - const tracer = trace.getTracer('healthcheck') - - return tracer.startActiveSpan('healthcheck-controller', async span => { - span.setAttribute('http.method', 'GET') - span.setAttribute('http.url', '/health') - span.setAttribute('http.status_code', '200') - span.end() - - return reply.status(404).send({ error: 'Not Found' }) + handler: withTracing('healthcheck', (_request, reply) => + reply.send({ alive: true }) + ), }) -} diff --git a/apps/backend/src/http/routes/images.ts b/apps/backend/src/http/routes/images.ts index b45d4fbe..baea3343 100644 --- a/apps/backend/src/http/routes/images.ts +++ b/apps/backend/src/http/routes/images.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createImageController } from '../controllers/images-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { @@ -25,7 +28,7 @@ export async function imagesRoutes(app: FastifyInstance) { querystring: createImageQuerySchema, consumes: ['multipart/form-data'], }, - handler: createImageController, + handler: withTracing('create-image', createImageController), }) ) } diff --git a/apps/backend/src/http/routes/import.ts b/apps/backend/src/http/routes/import.ts index d01a45fc..37984793 100644 --- a/apps/backend/src/http/routes/import.ts +++ b/apps/backend/src/http/routes/import.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createImportController, getDetailedImportController, @@ -30,7 +33,7 @@ export async function importRoutes(app: FastifyInstance) { querystring: createImportRequestSchema, consumes: ['multipart/form-data'], }, - handler: createImportController, + handler: withTracing('create-import', createImportController), }) ) @@ -50,7 +53,7 @@ export async function importRoutes(app: FastifyInstance) { params: getDetailedImportRequestSchema, response: getDetailedImportResponseSchema, }, - handler: getDetailedImportController, + handler: withTracing('get-detailed-import', getDetailedImportController), }) ) } diff --git a/apps/backend/src/http/routes/likes.ts b/apps/backend/src/http/routes/likes.ts index 82956ba3..d1a4e03d 100644 --- a/apps/backend/src/http/routes/likes.ts +++ b/apps/backend/src/http/routes/likes.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createLikeController, deleteLikeController, @@ -31,7 +34,7 @@ export async function likesRoutes(app: FastifyInstance) { }, ], }, - handler: createLikeController, + handler: withTracing('create-like', createLikeController), }) ) @@ -50,7 +53,7 @@ export async function likesRoutes(app: FastifyInstance) { }, ], }, - handler: deleteLikeController, + handler: withTracing('delete-like', deleteLikeController), }) ) @@ -64,7 +67,7 @@ export async function likesRoutes(app: FastifyInstance) { params: getLikesParamsSchema, response: getLikesResponseSchema, }, - handler: getLikesController, + handler: withTracing('get-likes', getLikesController), }) ) } diff --git a/apps/backend/src/http/routes/list-item.ts b/apps/backend/src/http/routes/list-item.ts index 8d0cf504..14053c89 100644 --- a/apps/backend/src/http/routes/list-item.ts +++ b/apps/backend/src/http/routes/list-item.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createListItemController, deleteListItemController, @@ -36,7 +39,7 @@ export async function listItemRoute(app: FastifyInstance) { }, ], }, - handler: createListItemController, + handler: withTracing('create-list-item', createListItemController), }) ) @@ -51,7 +54,9 @@ export async function listItemRoute(app: FastifyInstance) { querystring: languageQuerySchema, response: getListItemsResponseSchema, }, - handler: (req, reply) => getListItemsController(req, reply, app.redis), + handler: withTracing('get-list-items', (req, reply) => + getListItemsController(req, reply, app.redis) + ), }) ) @@ -70,7 +75,7 @@ export async function listItemRoute(app: FastifyInstance) { }, ], }, - handler: deleteListItemController, + handler: withTracing('delete-list-item', deleteListItemController), }) ) @@ -90,7 +95,7 @@ export async function listItemRoute(app: FastifyInstance) { }, ], }, - handler: updateListItemController, + handler: withTracing('update-list-item', updateListItemController), }) ) } diff --git a/apps/backend/src/http/routes/lists.ts b/apps/backend/src/http/routes/lists.ts index f1c73af6..71fa1cae 100644 --- a/apps/backend/src/http/routes/lists.ts +++ b/apps/backend/src/http/routes/lists.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createListController, deleteListController, @@ -45,7 +48,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: createListController, + handler: withTracing('create-list', createListController), }) ) @@ -65,7 +68,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: getListsController, + handler: withTracing('get-lists', getListsController), }) ) @@ -85,7 +88,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: deleteListController, + handler: withTracing('delete-list', deleteListController), }) ) @@ -106,7 +109,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: updateListController, + handler: withTracing('update-list', updateListController), }) ) @@ -126,7 +129,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: getListController, + handler: withTracing('get-list', getListController), }) ) @@ -146,7 +149,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: updateListBannerController, + handler: withTracing('update-list-banner', updateListBannerController), }) ) @@ -167,7 +170,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: getListProgressController, + handler: withTracing('get-list-progress', getListProgressController), }) ) } diff --git a/apps/backend/src/http/routes/login.ts b/apps/backend/src/http/routes/login.ts index cb236832..25cd19f9 100644 --- a/apps/backend/src/http/routes/login.ts +++ b/apps/backend/src/http/routes/login.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { loginController } from '../controllers/login-controller' import { loginBodySchema, loginResponseSchema } from '../schemas/login' @@ -13,6 +16,8 @@ export async function loginRoute(app: FastifyInstance) { body: loginBodySchema, response: loginResponseSchema, }, - handler: (request, reply) => loginController(request, reply, app), + handler: withTracing('login', (request, reply) => + loginController(request, reply, app) + ), }) } diff --git a/apps/backend/src/http/routes/review-replies.ts b/apps/backend/src/http/routes/review-replies.ts index d65e998c..68b23aa3 100644 --- a/apps/backend/src/http/routes/review-replies.ts +++ b/apps/backend/src/http/routes/review-replies.ts @@ -1,6 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createReviewReplyController, deleteReviewReplyController, @@ -38,7 +40,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: createReviewReplyController, + handler: withTracing('create-review-reply', createReviewReplyController), }) ) @@ -58,7 +60,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: getReviewRepliesController, + handler: withTracing('get-review-replies', getReviewRepliesController), }) ) @@ -77,7 +79,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: deleteReviewReplyController, + handler: withTracing('delete-review-reply', deleteReviewReplyController), }) ) @@ -98,7 +100,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: updateReviewReplyController, + handler: withTracing('update-review-reply', updateReviewReplyController), }) ) } diff --git a/apps/backend/src/http/routes/reviews.ts b/apps/backend/src/http/routes/reviews.ts index f839b135..5479723d 100644 --- a/apps/backend/src/http/routes/reviews.ts +++ b/apps/backend/src/http/routes/reviews.ts @@ -1,6 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createReviewController, deleteReviewController, @@ -43,7 +45,7 @@ export async function reviewsRoute(app: FastifyInstance) { }, ], }, - handler: createReviewController, + handler: withTracing('create-review', createReviewController), }) ) @@ -63,7 +65,7 @@ export async function reviewsRoute(app: FastifyInstance) { }, ], }, - handler: getReviewsController, + handler: withTracing('get-reviews', getReviewsController), }) ) @@ -76,7 +78,7 @@ export async function reviewsRoute(app: FastifyInstance) { tags: [reviewsTag], params: reviewParamsSchema, }, - handler: deleteReviewController, + handler: withTracing('delete-review', deleteReviewController), }) ) @@ -91,7 +93,7 @@ export async function reviewsRoute(app: FastifyInstance) { body: updateReviewBodySchema, response: updateReviewResponse, }, - handler: updateReviewController, + handler: withTracing('update-review', updateReviewController), }) ) @@ -106,8 +108,9 @@ export async function reviewsRoute(app: FastifyInstance) { query: getReviewsQuerySchema, response: getDetailedReviewsResponseSchema, }, - handler: (request, reply) => - getDetailedReviewsController(request, reply, app.redis), + handler: withTracing('get-detailed-reviews', (request, reply) => + getDetailedReviewsController(request, reply, app.redis) + ), }) ) @@ -127,7 +130,7 @@ export async function reviewsRoute(app: FastifyInstance) { ], response: getReviewResponseSchema, }, - handler: getReviewController, + handler: withTracing('get-review', getReviewController), }) ) } diff --git a/apps/backend/src/http/routes/social-auth.ts b/apps/backend/src/http/routes/social-auth.ts index ed10ceff..1e51fc2f 100644 --- a/apps/backend/src/http/routes/social-auth.ts +++ b/apps/backend/src/http/routes/social-auth.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { appleAuthController, googleAuthController, @@ -20,7 +23,9 @@ export async function socialAuthRoutes(app: FastifyInstance) { body: appleAuthBodySchema, response: socialAuthResponseSchema, }, - handler: (request, reply) => appleAuthController(request, reply, app), + handler: withTracing('apple-auth', (request, reply) => + appleAuthController(request, reply, app) + ), }) app.withTypeProvider().route({ @@ -32,6 +37,8 @@ export async function socialAuthRoutes(app: FastifyInstance) { body: googleAuthBodySchema, response: socialAuthResponseSchema, }, - handler: (request, reply) => googleAuthController(request, reply, app), + handler: withTracing('google-auth', (request, reply) => + googleAuthController(request, reply, app) + ), }) } diff --git a/apps/backend/src/http/routes/social-links.ts b/apps/backend/src/http/routes/social-links.ts index 2829b741..b66c61a9 100644 --- a/apps/backend/src/http/routes/social-links.ts +++ b/apps/backend/src/http/routes/social-links.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { getSocialLinksController, upsertSocialLinksController, @@ -32,7 +35,7 @@ export async function socialLinksRoute(app: FastifyInstance) { }, ], }, - handler: upsertSocialLinksController, + handler: withTracing('upsert-social-links', upsertSocialLinksController), }) ) @@ -46,7 +49,7 @@ export async function socialLinksRoute(app: FastifyInstance) { querystring: getSocialLinksQuerySchema, response: getSocialLinksResponseSchema, }, - handler: getSocialLinksController, + handler: withTracing('get-social-links', getSocialLinksController), }) ) } diff --git a/apps/backend/src/http/routes/subscriptions.ts b/apps/backend/src/http/routes/subscriptions.ts index 70093408..1bf771a8 100644 --- a/apps/backend/src/http/routes/subscriptions.ts +++ b/apps/backend/src/http/routes/subscriptions.ts @@ -1,4 +1,7 @@ import type { FastifyInstance } from 'fastify' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { deleteSubscriptionController } from '../controllers/subscriptions-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { @@ -24,5 +27,5 @@ export const subscriptionsRoutes = (app: FastifyInstance) => ], }, - handler: deleteSubscriptionController, + handler: withTracing('delete-subscription', deleteSubscriptionController), }) diff --git a/apps/backend/src/http/routes/tmdb-proxy.ts b/apps/backend/src/http/routes/tmdb-proxy.ts index 98eb6a00..f2ab4d9e 100644 --- a/apps/backend/src/http/routes/tmdb-proxy.ts +++ b/apps/backend/src/http/routes/tmdb-proxy.ts @@ -1,6 +1,8 @@ import type { FastifyInstance } from 'fastify' import https from 'https' + import { config } from '@/config' +import { withTracing } from '@/infra/telemetry/with-tracing' const TMDB_BASE_URL = 'https://api.themoviedb.org/3' @@ -85,7 +87,7 @@ export async function tmdbProxyRoutes(app: FastifyInstance) { tags: TMDB_PROXY_TAGS, hide: config.app.APP_ENV === 'production', }, - handler: async (request, reply) => { + handler: withTracing('tmdb-proxy', async (request, reply) => { const tmdbPath = (request.params as { '*': string })['*'] if (!tmdbPath) { @@ -152,7 +154,7 @@ export async function tmdbProxyRoutes(app: FastifyInstance) { reply.header('X-Cache', 'MISS') reply.header('Content-Type', 'application/json') return reply.send(JSON.parse(tmdbResponse.body)) - }, + }), }) ) } diff --git a/apps/backend/src/http/routes/user-activities.ts b/apps/backend/src/http/routes/user-activities.ts index 7cbd54ab..79c627d0 100644 --- a/apps/backend/src/http/routes/user-activities.ts +++ b/apps/backend/src/http/routes/user-activities.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { deleteUserActivityController, getUserActivitiesController, @@ -28,8 +31,9 @@ export async function userActivitiesRoutes(app: FastifyInstance) { params: getUserActivitiesParamsSchema, response: getUserActivitiesResponseSchema, }, - handler: (request, reply) => - getUserActivitiesController(request, reply, app.redis), + handler: withTracing('get-user-activities', (request, reply) => + getUserActivitiesController(request, reply, app.redis) + ), }) ) @@ -43,7 +47,7 @@ export async function userActivitiesRoutes(app: FastifyInstance) { tags: TAGS, params: deleteUserActivityParamsSchema, }, - handler: deleteUserActivityController, + handler: withTracing('delete-user-activity', deleteUserActivityController), }) ) @@ -58,8 +62,9 @@ export async function userActivitiesRoutes(app: FastifyInstance) { querystring: getUserNetworkActivitiesQuerySchema, response: getUserActivitiesResponseSchema, }, - handler: (request, reply) => - getUserNetworkActivitiesController(request, reply, app.redis), + handler: withTracing('get-user-network-activities', (request, reply) => + getUserNetworkActivitiesController(request, reply, app.redis) + ), }) }) } diff --git a/apps/backend/src/http/routes/user-episodes.ts b/apps/backend/src/http/routes/user-episodes.ts index 6f066598..5b129e52 100644 --- a/apps/backend/src/http/routes/user-episodes.ts +++ b/apps/backend/src/http/routes/user-episodes.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createUserEpisodesController, deleteUserEpisodesController, @@ -34,7 +37,7 @@ export async function userEpisodesRoutes(app: FastifyInstance) { }, ], }, - handler: createUserEpisodesController, + handler: withTracing('create-user-episodes', createUserEpisodesController), }) ) @@ -54,7 +57,7 @@ export async function userEpisodesRoutes(app: FastifyInstance) { }, ], }, - handler: getUserEpisodesController, + handler: withTracing('get-user-episodes', getUserEpisodesController), }) ) @@ -74,7 +77,7 @@ export async function userEpisodesRoutes(app: FastifyInstance) { }, ], }, - handler: deleteUserEpisodesController, + handler: withTracing('delete-user-episodes', deleteUserEpisodesController), }) ) } diff --git a/apps/backend/src/http/routes/user-items.ts b/apps/backend/src/http/routes/user-items.ts index 3799cf2a..95f31182 100644 --- a/apps/backend/src/http/routes/user-items.ts +++ b/apps/backend/src/http/routes/user-items.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { deleteUserItemController, getAllUserItemsController, @@ -44,8 +47,9 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: (request, reply) => - upsertUserItemController(request, reply, app.redis), + handler: withTracing('upsert-user-item', (request, reply) => + upsertUserItemController(request, reply, app.redis) + ), }) ) @@ -60,8 +64,9 @@ export async function userItemsRoutes(app: FastifyInstance) { response: getUserItemsResponseSchema, operationId: 'getUserItems', }, - handler: (request, reply) => - getUserItemsController(request, reply, app.redis), + handler: withTracing('get-user-items', (request, reply) => + getUserItemsController(request, reply, app.redis) + ), }) ) @@ -80,8 +85,9 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: (request, reply) => - deleteUserItemController(request, reply, app.redis), + handler: withTracing('delete-user-item', (request, reply) => + deleteUserItemController(request, reply, app.redis) + ), }) ) @@ -101,7 +107,7 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: getUserItemController, + handler: withTracing('get-user-item', getUserItemController), }) ) @@ -116,7 +122,7 @@ export async function userItemsRoutes(app: FastifyInstance) { response: getAllUserItemsResponseSchema, operationId: 'getAllUserItems', }, - handler: getAllUserItemsController, + handler: withTracing('get-all-user-items', getAllUserItemsController), }) ) @@ -135,7 +141,7 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: reorderUserItemsController, + handler: withTracing('reorder-user-items', reorderUserItemsController), }) ) @@ -150,7 +156,7 @@ export async function userItemsRoutes(app: FastifyInstance) { response: getUserItemsCountResponseSchema, operationId: 'getUserItemsCount', }, - handler: getUserItemsCountController, + handler: withTracing('get-user-items-count', getUserItemsCountController), }) ) } diff --git a/apps/backend/src/http/routes/user-stats.ts b/apps/backend/src/http/routes/user-stats.ts index 14c34060..009c0e2d 100644 --- a/apps/backend/src/http/routes/user-stats.ts +++ b/apps/backend/src/http/routes/user-stats.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { getUserBestReviewsController, getUserItemsStatusController, @@ -41,7 +44,7 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserStatsResponseSchema, tags: USER_STATS_TAG, }, - handler: getUserStatsController, + handler: withTracing('get-user-stats', getUserStatsController), }) ) @@ -55,8 +58,9 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserTotalHoursResponseSchema, tags: USER_STATS_TAG, }, - handler: (request, reply) => - getUserTotalHoursController(request, reply, app.redis), + handler: withTracing('get-user-total-hours', (request, reply) => + getUserTotalHoursController(request, reply, app.redis) + ), }) ) @@ -70,7 +74,7 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserReviewsCountResponseSchema, tags: USER_STATS_TAG, }, - handler: getUserReviewsCountController, + handler: withTracing('get-user-reviews-count', getUserReviewsCountController), }) ) @@ -85,8 +89,9 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserMostWatchedSeriesResponseSchema, tags: USER_STATS_TAG, }, - handler: (request, reply) => - getUserMostWatchedSeriesController(request, reply, app.redis), + handler: withTracing('get-user-most-watched-series', (request, reply) => + getUserMostWatchedSeriesController(request, reply, app.redis) + ), }) ) @@ -101,8 +106,9 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserWatchedGenresResponseSchema, tags: USER_STATS_TAG, }, - handler: (request, reply) => - getUserWatchedGenresController(request, reply, app.redis), + handler: withTracing('get-user-watched-genres', (request, reply) => + getUserWatchedGenresController(request, reply, app.redis) + ), }) ) @@ -117,8 +123,9 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserWatchedCastResponseSchema, tags: USER_STATS_TAG, }, - handler: (request, reply) => - getUserWatchedCastController(request, reply, app.redis), + handler: withTracing('get-user-watched-cast', (request, reply) => + getUserWatchedCastController(request, reply, app.redis) + ), }) ) @@ -133,8 +140,9 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserWatchedCountriesResponseSchema, tags: USER_STATS_TAG, }, - handler: (request, reply) => - getUserWatchedCountriesController(request, reply, app.redis), + handler: withTracing('get-user-watched-countries', (request, reply) => + getUserWatchedCountriesController(request, reply, app.redis) + ), }) ) @@ -149,8 +157,9 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserBestReviewsResponseSchema, tags: USER_STATS_TAG, }, - handler: (request, reply) => - getUserBestReviewsController(request, reply, app.redis), + handler: withTracing('get-user-best-reviews', (request, reply) => + getUserBestReviewsController(request, reply, app.redis) + ), }) ) @@ -164,8 +173,9 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserItemsStatusResponseSchema, tags: USER_STATS_TAG, }, - handler: (request, reply) => - getUserItemsStatusController(request, reply, app.redis), + handler: withTracing('get-user-items-status', (request, reply) => + getUserItemsStatusController(request, reply, app.redis) + ), }) ) } diff --git a/apps/backend/src/http/routes/users.ts b/apps/backend/src/http/routes/users.ts index 39de276a..48f8e066 100644 --- a/apps/backend/src/http/routes/users.ts +++ b/apps/backend/src/http/routes/users.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createUserController, getMeController, @@ -50,7 +53,7 @@ export async function usersRoute(app: FastifyInstance) { body: createUserBodySchema, response: createUserResponseSchema, }, - handler: createUserController, + handler: withTracing('create-user', createUserController), }) ) @@ -64,7 +67,7 @@ export async function usersRoute(app: FastifyInstance) { querystring: checkAvailableUsernameQuerySchema, response: checkAvailableUsernameResponseSchema, }, - handler: isUsernameAvailableController, + handler: withTracing('is-username-available', isUsernameAvailableController), }) ) @@ -78,7 +81,7 @@ export async function usersRoute(app: FastifyInstance) { querystring: isEmailAvailableQuerySchema, response: isEmailAvailableResponseSchema, }, - handler: isEmailAvailableController, + handler: withTracing('is-email-available', isEmailAvailableController), }) ) @@ -92,7 +95,7 @@ export async function usersRoute(app: FastifyInstance) { params: getUserByUsernameParamsSchema, response: getUserByUsernameResponseSchema, }, - handler: getUserByUsernameController, + handler: withTracing('get-user-by-username', getUserByUsernameController), }) ) @@ -106,7 +109,7 @@ export async function usersRoute(app: FastifyInstance) { params: getUserByIdParamsSchema, response: getUserByIdResponseSchema, }, - handler: getUserByIdController, + handler: withTracing('get-user-by-id', getUserByIdController), }) ) @@ -125,7 +128,7 @@ export async function usersRoute(app: FastifyInstance) { }, ], }, - handler: getMeController, + handler: withTracing('get-me', getMeController), }) ) @@ -141,7 +144,7 @@ export async function usersRoute(app: FastifyInstance) { response: updateUserResponseSchema, security: [{ bearerAuth: [] }], }, - handler: updateUserController, + handler: withTracing('update-user', updateUserController), }) ) @@ -155,7 +158,7 @@ export async function usersRoute(app: FastifyInstance) { body: updateUserPasswordBodySchema, response: updateUserPasswordResponseSchema, }, - handler: updateUserPasswordController, + handler: withTracing('update-user-password', updateUserPasswordController), }) ) @@ -176,7 +179,7 @@ export async function usersRoute(app: FastifyInstance) { }, ], }, - handler: updateUserPreferencesController, + handler: withTracing('update-user-preferences', updateUserPreferencesController), }) ) @@ -196,7 +199,7 @@ export async function usersRoute(app: FastifyInstance) { }, ], }, - handler: getUserPreferencesController, + handler: withTracing('get-user-preferences', getUserPreferencesController), }) ) @@ -210,7 +213,7 @@ export async function usersRoute(app: FastifyInstance) { querystring: searchUsersByUsernameQuerySchema, response: searchUsersByUsernameResponseSchema, }, - handler: searchUsersByUsernameController, + handler: withTracing('search-users-by-username', searchUsersByUsernameController), }) ) } diff --git a/apps/backend/src/http/routes/watch-entries.ts b/apps/backend/src/http/routes/watch-entries.ts index 4e8130cf..1e45b0ff 100644 --- a/apps/backend/src/http/routes/watch-entries.ts +++ b/apps/backend/src/http/routes/watch-entries.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { createWatchEntryController, deleteWatchEntryController, @@ -33,7 +36,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { response: createWatchEntryResponseSchema, security: [{ bearerAuth: [] }], }, - handler: createWatchEntryController, + handler: withTracing('create-watch-entry', createWatchEntryController), }) ) @@ -49,7 +52,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { response: getWatchEntriesResponseSchema, security: [{ bearerAuth: [] }], }, - handler: getWatchEntriesController, + handler: withTracing('get-watch-entries', getWatchEntriesController), }) ) @@ -66,7 +69,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { response: updateWatchEntryResponseSchema, security: [{ bearerAuth: [] }], }, - handler: updateWatchEntryController, + handler: withTracing('update-watch-entry', updateWatchEntryController), }) ) @@ -81,7 +84,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { params: deleteWatchEntryParamsSchema, security: [{ bearerAuth: [] }], }, - handler: deleteWatchEntryController, + handler: withTracing('delete-watch-entry', deleteWatchEntryController), }) ) } diff --git a/apps/backend/src/http/routes/webhook.ts b/apps/backend/src/http/routes/webhook.ts index a7ad56ba..2df08c63 100644 --- a/apps/backend/src/http/routes/webhook.ts +++ b/apps/backend/src/http/routes/webhook.ts @@ -1,5 +1,8 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + +import { withTracing } from '@/infra/telemetry/with-tracing' + import { stripeWebhookController } from '../controllers/stripe-webhook-controller' export async function webhookRoutes(app: FastifyInstance) { @@ -26,7 +29,7 @@ export async function webhookRoutes(app: FastifyInstance) { description: 'Webhook route', tags: ['Webhook'], }, - handler: stripeWebhookController, + handler: withTracing('stripe-webhook', stripeWebhookController), }) ) } diff --git a/apps/backend/src/infra/telemetry/with-tracing.ts b/apps/backend/src/infra/telemetry/with-tracing.ts new file mode 100644 index 00000000..cbdc59ea --- /dev/null +++ b/apps/backend/src/infra/telemetry/with-tracing.ts @@ -0,0 +1,36 @@ +import { trace } from '@opentelemetry/api' +import type { FastifyRequest } from 'fastify' + +// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any handler signature +export function withTracing any>( + spanName: string, + handler: T, + options?: { method?: string; url?: string } +): T { + const tracer = trace.getTracer('plotwist-api', '0.1.0') + + return (async (...args: Parameters) => { + return tracer.startActiveSpan(spanName, async span => { + try { + const request = args[0] as FastifyRequest | undefined + if (request) { + span.setAttribute('http.method', options?.method ?? request.method) + span.setAttribute( + 'http.url', + options?.url ?? request.routeOptions?.url ?? request.url ?? '' + ) + } + + const result = await handler(...args) + span.setAttribute('http.status_code', 200) + return result + } catch (err) { + span.recordException(err as Error) + span.setAttribute('http.status_code', 500) + throw err + } finally { + span.end() + } + }) + }) as T +} From 721e95fc10d10aa1dec9f543830156ec9fef3728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 17 Feb 2026 21:58:28 -0300 Subject: [PATCH 05/26] feat: implement service tracing for various domain services --- .../services/feedback/create-feedback.ts | 8 +++++- .../domain/services/follows/create-follow.ts | 10 +++++-- .../domain/services/follows/delete-follow.ts | 10 +++++-- .../src/domain/services/follows/get-follow.ts | 10 +++++-- .../domain/services/follows/get-followers.ts | 8 +++++- .../src/domain/services/likes/create-like.ts | 8 +++++- .../src/domain/services/likes/delete-like.ts | 8 +++++- .../src/domain/services/likes/get-likes.ts | 8 +++++- .../services/list-item/create-list-item.ts | 10 +++++-- .../services/list-item/delete-list-item.ts | 10 +++++-- .../services/list-item/get-list-items.ts | 8 +++++- .../services/list-item/update-list-items.ts | 10 +++++-- .../src/domain/services/lists/create-list.ts | 7 +++-- .../src/domain/services/lists/delete-list.ts | 8 +++++- .../services/lists/get-list-progress.ts | 10 +++++-- .../src/domain/services/lists/get-list.ts | 7 +++-- .../src/domain/services/lists/get-lists.ts | 8 +++++- .../services/lists/update-list-banner.ts | 8 +++++- .../src/domain/services/lists/update-list.ts | 10 +++++-- .../src/domain/services/login/login.ts | 5 +++- .../magic-link/generate-magic-link.ts | 9 ++++++- .../magic-link/send-magic-link-email.ts | 10 +++++-- .../review-replies/create-review-reply.ts | 10 ++++++- .../domain/services/reviews/create-review.ts | 8 +++++- .../domain/services/reviews/delete-review.ts | 8 +++++- .../src/domain/services/reviews/get-review.ts | 8 +++++- .../domain/services/reviews/get-reviews.ts | 8 +++++- .../domain/services/reviews/update-review.ts | 8 +++++- .../services/social-links/get-social-links.ts | 8 +++++- .../social-links/upsert-social-links.ts | 8 +++++- .../src/domain/services/tmdb/get-tmdb-data.ts | 11 ++++++-- .../user-activities/create-user-activity.ts | 8 +++++- .../services/user-items/get-user-items.ts | 8 +++++- .../services/user-items/upsert-user-item.ts | 9 ++++++- .../user-preferences/get-user-preferences.ts | 10 +++++-- .../update-user-preferences.ts | 10 +++++-- .../src/domain/services/users/create-user.ts | 7 +++-- .../src/domain/services/users/get-by-id.ts | 5 +++- .../services/users/get-user-by-username.ts | 10 ++++++- .../services/users/is-email-available.ts | 10 ++++++- .../services/users/is-username-available.ts | 10 +++++-- .../users/search-users-by-username.ts | 8 +++++- .../services/users/update-user-password.ts | 10 +++++-- .../src/domain/services/users/update-user.ts | 10 +++++-- .../infra/telemetry/with-service-tracing.ts | 26 +++++++++++++++++++ .../src/infra/telemetry/with-tracing.ts | 5 +++- apps/web/next-env.d.ts | 2 +- 47 files changed, 351 insertions(+), 64 deletions(-) create mode 100644 apps/backend/src/infra/telemetry/with-service-tracing.ts diff --git a/apps/backend/src/domain/services/feedback/create-feedback.ts b/apps/backend/src/domain/services/feedback/create-feedback.ts index 54079d32..e63e83c5 100644 --- a/apps/backend/src/domain/services/feedback/create-feedback.ts +++ b/apps/backend/src/domain/services/feedback/create-feedback.ts @@ -2,8 +2,9 @@ import { insertFeedback } from '@/db/repositories/feedback-repository' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import type { InsertFeedbackModel } from '@/domain/entities/feedback' import { UserNotFoundError } from '@/domain/errors/user-not-found' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -export async function createFeedbackService(params: InsertFeedbackModel) { +const createFeedbackServiceImpl = async (params: InsertFeedbackModel) => { try { const [feedback] = await insertFeedback(params) return { feedback } @@ -14,3 +15,8 @@ export async function createFeedbackService(params: InsertFeedbackModel) { throw error } } + +export const createFeedbackService = withServiceTracing( + 'create-feedback', + createFeedbackServiceImpl +) diff --git a/apps/backend/src/domain/services/follows/create-follow.ts b/apps/backend/src/domain/services/follows/create-follow.ts index a03e45b0..b4deb5fd 100644 --- a/apps/backend/src/domain/services/follows/create-follow.ts +++ b/apps/backend/src/domain/services/follows/create-follow.ts @@ -1,16 +1,17 @@ import { insertFollow } from '@/db/repositories/followers-repository' import { isUniqueViolation } from '@/db/utils/postgres-errors' import { FollowAlreadyRegisteredError } from '@/domain/errors/follow-already-registered' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type CreateFollowServiceInput = { followerId: string followedId: string } -export async function createFollowService({ +const createFollowServiceImpl = async ({ followedId, followerId, -}: CreateFollowServiceInput) { +}: CreateFollowServiceInput) => { try { const [follow] = await insertFollow({ followedId, followerId }) @@ -23,3 +24,8 @@ export async function createFollowService({ throw error } } + +export const createFollowService = withServiceTracing( + 'create-follow', + createFollowServiceImpl +) diff --git a/apps/backend/src/domain/services/follows/delete-follow.ts b/apps/backend/src/domain/services/follows/delete-follow.ts index 5130aeb3..bf098453 100644 --- a/apps/backend/src/domain/services/follows/delete-follow.ts +++ b/apps/backend/src/domain/services/follows/delete-follow.ts @@ -1,15 +1,21 @@ import { deleteFollow } from '@/db/repositories/followers-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type DeleteFollowServiceInput = { followerId: string followedId: string } -export async function deleteFollowService({ +const deleteFollowServiceImpl = async ({ followedId, followerId, -}: DeleteFollowServiceInput) { +}: DeleteFollowServiceInput) => { const [deletedFollow] = await deleteFollow({ followedId, followerId }) return { follow: deletedFollow } } + +export const deleteFollowService = withServiceTracing( + 'delete-follow', + deleteFollowServiceImpl +) diff --git a/apps/backend/src/domain/services/follows/get-follow.ts b/apps/backend/src/domain/services/follows/get-follow.ts index 8b0cd437..035a8421 100644 --- a/apps/backend/src/domain/services/follows/get-follow.ts +++ b/apps/backend/src/domain/services/follows/get-follow.ts @@ -1,15 +1,21 @@ import { getFollow } from '@/db/repositories/followers-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetFollowServiceInput = { followerId: string followedId: string } -export async function getFollowService({ +const getFollowServiceImpl = async ({ followedId, followerId, -}: GetFollowServiceInput) { +}: GetFollowServiceInput) => { const [follow] = await getFollow({ followedId, followerId }) return { follow: follow || null } } + +export const getFollowService = withServiceTracing( + 'get-follow', + getFollowServiceImpl +) diff --git a/apps/backend/src/domain/services/follows/get-followers.ts b/apps/backend/src/domain/services/follows/get-followers.ts index 45714eee..2c1183a0 100644 --- a/apps/backend/src/domain/services/follows/get-followers.ts +++ b/apps/backend/src/domain/services/follows/get-followers.ts @@ -1,4 +1,5 @@ import { selectFollowers } from '@/db/repositories/followers-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetFollowersInput = { followedId?: string @@ -7,7 +8,7 @@ export type GetFollowersInput = { pageSize: number } -export async function getFollowersService(input: GetFollowersInput) { +const getFollowersServiceImpl = async (input: GetFollowersInput) => { const followers = await selectFollowers(input) return { @@ -15,3 +16,8 @@ export async function getFollowersService(input: GetFollowersInput) { nextCursor: followers[input.pageSize]?.createdAt.toISOString() || null, } } + +export const getFollowersService = withServiceTracing( + 'get-followers', + getFollowersServiceImpl +) diff --git a/apps/backend/src/domain/services/likes/create-like.ts b/apps/backend/src/domain/services/likes/create-like.ts index 4acd6190..7ef5fbd7 100644 --- a/apps/backend/src/domain/services/likes/create-like.ts +++ b/apps/backend/src/domain/services/likes/create-like.ts @@ -1,7 +1,13 @@ import { insertLike } from '@/db/repositories/likes-repository' import type { InsertLike } from '@/domain/entities/likes' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -export async function createLikeService(values: InsertLike) { +const createLikeServiceImpl = async (values: InsertLike) => { const [like] = await insertLike(values) return { like } } + +export const createLikeService = withServiceTracing( + 'create-like', + createLikeServiceImpl +) diff --git a/apps/backend/src/domain/services/likes/delete-like.ts b/apps/backend/src/domain/services/likes/delete-like.ts index 7cecad15..6e71a28d 100644 --- a/apps/backend/src/domain/services/likes/delete-like.ts +++ b/apps/backend/src/domain/services/likes/delete-like.ts @@ -1,7 +1,13 @@ import { deleteLike } from '@/db/repositories/likes-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -export async function deleteLikeService(id: string) { +const deleteLikeServiceImpl = async (id: string) => { const [like] = await deleteLike(id) return { like } } + +export const deleteLikeService = withServiceTracing( + 'delete-like', + deleteLikeServiceImpl +) diff --git a/apps/backend/src/domain/services/likes/get-likes.ts b/apps/backend/src/domain/services/likes/get-likes.ts index 402ce86c..0523f333 100644 --- a/apps/backend/src/domain/services/likes/get-likes.ts +++ b/apps/backend/src/domain/services/likes/get-likes.ts @@ -1,7 +1,13 @@ import { selectLikes } from '@/db/repositories/likes-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -export async function getLikesService(entityId: string) { +const getLikesServiceImpl = async (entityId: string) => { const likes = await selectLikes(entityId) return { likes } } + +export const getLikesService = withServiceTracing( + 'get-likes', + getLikesServiceImpl +) diff --git a/apps/backend/src/domain/services/list-item/create-list-item.ts b/apps/backend/src/domain/services/list-item/create-list-item.ts index f65e645c..f12cfa87 100644 --- a/apps/backend/src/domain/services/list-item/create-list-item.ts +++ b/apps/backend/src/domain/services/list-item/create-list-item.ts @@ -1,12 +1,13 @@ import { insertListItem } from '@/db/repositories/list-item-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import type { InsertListItem } from '../../entities/list-item' import { ListNotFoundError } from '../../errors/list-not-found-error' -export async function createListItemService( +const createListItemServiceImpl = async ( values: InsertListItem, _userId: string -) { +) => { try { const [listItem] = await insertListItem(values) @@ -19,3 +20,8 @@ export async function createListItemService( throw error } } + +export const createListItemService = withServiceTracing( + 'create-list-item', + createListItemServiceImpl +) diff --git a/apps/backend/src/domain/services/list-item/delete-list-item.ts b/apps/backend/src/domain/services/list-item/delete-list-item.ts index 218f69e5..22daf475 100644 --- a/apps/backend/src/domain/services/list-item/delete-list-item.ts +++ b/apps/backend/src/domain/services/list-item/delete-list-item.ts @@ -1,12 +1,13 @@ import { deleteListItem } from '@/db/repositories/list-item-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ListItemNotFoundError } from '@/domain/errors/list-item-not-found-error' type DeleteListItemInput = { id: string; userId: string } -export async function deleteListItemService({ +const deleteListItemServiceImpl = async ({ id, userId: _userId, -}: DeleteListItemInput) { +}: DeleteListItemInput) => { const [deletedListItem] = await deleteListItem(id) if (!deletedListItem) { @@ -15,3 +16,8 @@ export async function deleteListItemService({ return { deletedListItem } } + +export const deleteListItemService = withServiceTracing( + 'delete-list-item', + deleteListItemServiceImpl +) diff --git a/apps/backend/src/domain/services/list-item/get-list-items.ts b/apps/backend/src/domain/services/list-item/get-list-items.ts index 86825b2f..e4ecb6b2 100644 --- a/apps/backend/src/domain/services/list-item/get-list-items.ts +++ b/apps/backend/src/domain/services/list-item/get-list-items.ts @@ -1,10 +1,11 @@ import { selectListItems } from '@/db/repositories/list-item-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { getListById } from '@/db/repositories/list-repository' import { ListNotFoundError } from '../../errors/list-not-found-error' type GetListItemsInput = { listId: string } -export async function getListItemsService({ listId }: GetListItemsInput) { +const getListItemsServiceImpl = async ({ listId }: GetListItemsInput) => { const [list] = await getListById(listId) if (!list) { @@ -15,3 +16,8 @@ export async function getListItemsService({ listId }: GetListItemsInput) { return { listItems } } + +export const getListItemsService = withServiceTracing( + 'get-list-items', + getListItemsServiceImpl +) diff --git a/apps/backend/src/domain/services/list-item/update-list-items.ts b/apps/backend/src/domain/services/list-item/update-list-items.ts index 7b42ebbd..9ec32780 100644 --- a/apps/backend/src/domain/services/list-item/update-list-items.ts +++ b/apps/backend/src/domain/services/list-item/update-list-items.ts @@ -1,14 +1,20 @@ import { updateListItems } from '@/db/repositories/list-item-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type UpdateListItemsServiceInput = { listItems: Array<{ id: string; position: number }> } -export async function updateListItemsService( +const updateListItemsServiceImpl = async ( input: UpdateListItemsServiceInput -) { +) => { const result = await updateListItems(input) const listItems = result.flat() return { listItems } } + +export const updateListItemsService = withServiceTracing( + 'update-list-items', + updateListItemsServiceImpl +) diff --git a/apps/backend/src/domain/services/lists/create-list.ts b/apps/backend/src/domain/services/lists/create-list.ts index fe580f7d..bfbd3be0 100644 --- a/apps/backend/src/domain/services/lists/create-list.ts +++ b/apps/backend/src/domain/services/lists/create-list.ts @@ -1,17 +1,18 @@ import type { InferInsertModel } from 'drizzle-orm' import { insertList } from '@/db/repositories/list-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { schema } from '@/db/schema' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import { UserNotFoundError } from '../../errors/user-not-found' export type CreateListInput = InferInsertModel -export async function createList({ +const createListImpl = async ({ title, description, visibility = 'PUBLIC', userId, -}: CreateListInput) { +}: CreateListInput) => { try { const [list] = await insertList({ title, description, visibility, userId }) @@ -24,3 +25,5 @@ export async function createList({ throw error } } + +export const createList = withServiceTracing('create-list', createListImpl) diff --git a/apps/backend/src/domain/services/lists/delete-list.ts b/apps/backend/src/domain/services/lists/delete-list.ts index a226cd2b..cfd2e49b 100644 --- a/apps/backend/src/domain/services/lists/delete-list.ts +++ b/apps/backend/src/domain/services/lists/delete-list.ts @@ -1,9 +1,15 @@ import { deleteList } from '@/db/repositories/list-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' type DeleteListInput = { id: string; userId: string } -export async function deleteListService({ id }: DeleteListInput) { +const deleteListServiceImpl = async ({ id }: DeleteListInput) => { const [deletedList] = await deleteList(id) return deletedList } + +export const deleteListService = withServiceTracing( + 'delete-list', + deleteListServiceImpl +) diff --git a/apps/backend/src/domain/services/lists/get-list-progress.ts b/apps/backend/src/domain/services/lists/get-list-progress.ts index e7faeccf..885314a2 100644 --- a/apps/backend/src/domain/services/lists/get-list-progress.ts +++ b/apps/backend/src/domain/services/lists/get-list-progress.ts @@ -1,4 +1,5 @@ import { selectListItems } from '@/db/repositories/list-item-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { selectAllUserItemsByStatus } from '@/db/repositories/user-item-repository' type GetListProgressServiceParams = { @@ -6,10 +7,10 @@ type GetListProgressServiceParams = { authenticatedUserId: string } -export async function getListProgressService({ +const getListProgressServiceImpl = async ({ id, authenticatedUserId, -}: GetListProgressServiceParams) { +}: GetListProgressServiceParams) => { const listItems = await selectListItems(id) if (listItems.length === 0) { return { @@ -40,3 +41,8 @@ export async function getListProgressService({ percentage, } } + +export const getListProgressService = withServiceTracing( + 'get-list-progress', + getListProgressServiceImpl +) diff --git a/apps/backend/src/domain/services/lists/get-list.ts b/apps/backend/src/domain/services/lists/get-list.ts index fd3e10f8..f750fb4c 100644 --- a/apps/backend/src/domain/services/lists/get-list.ts +++ b/apps/backend/src/domain/services/lists/get-list.ts @@ -1,4 +1,5 @@ import { getListById } from '@/db/repositories/list-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ListNotFoundError } from '../../errors/list-not-found-error' type GetListInput = { @@ -6,10 +7,10 @@ type GetListInput = { authenticatedUserId?: string } -export async function getListService({ +const getListServiceImpl = async ({ id, authenticatedUserId, -}: GetListInput) { +}: GetListInput) => { const [list] = await getListById(id, authenticatedUserId) if (!list) { @@ -18,3 +19,5 @@ export async function getListService({ return { list } } + +export const getListService = withServiceTracing('get-list', getListServiceImpl) diff --git a/apps/backend/src/domain/services/lists/get-lists.ts b/apps/backend/src/domain/services/lists/get-lists.ts index 3906d20b..d7ed4d2d 100644 --- a/apps/backend/src/domain/services/lists/get-lists.ts +++ b/apps/backend/src/domain/services/lists/get-lists.ts @@ -1,4 +1,5 @@ import { selectLists } from '@/db/repositories/list-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetListsInput = { userId?: string @@ -8,8 +9,13 @@ export type GetListsInput = { hasBanner?: boolean } -export async function getListsServices(input: GetListsInput) { +const getListsServicesImpl = async (input: GetListsInput) => { const lists = await selectLists(input) return { lists } } + +export const getListsServices = withServiceTracing( + 'get-lists', + getListsServicesImpl +) diff --git a/apps/backend/src/domain/services/lists/update-list-banner.ts b/apps/backend/src/domain/services/lists/update-list-banner.ts index 89a9da6a..2a16c5d9 100644 --- a/apps/backend/src/domain/services/lists/update-list-banner.ts +++ b/apps/backend/src/domain/services/lists/update-list-banner.ts @@ -1,4 +1,5 @@ import { updateListBanner } from '@/db/repositories/list-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ListNotFoundError } from '@/domain/errors/list-not-found-error' export type UpdateListBannerInput = { @@ -7,7 +8,7 @@ export type UpdateListBannerInput = { userId: string } -export async function updateListBannerService(input: UpdateListBannerInput) { +const updateListBannerServiceImpl = async (input: UpdateListBannerInput) => { const [list] = await updateListBanner(input) if (!list) { @@ -16,3 +17,8 @@ export async function updateListBannerService(input: UpdateListBannerInput) { return { list } } + +export const updateListBannerService = withServiceTracing( + 'update-list-banner', + updateListBannerServiceImpl +) diff --git a/apps/backend/src/domain/services/lists/update-list.ts b/apps/backend/src/domain/services/lists/update-list.ts index 24a42725..a5b1bc35 100644 --- a/apps/backend/src/domain/services/lists/update-list.ts +++ b/apps/backend/src/domain/services/lists/update-list.ts @@ -1,5 +1,6 @@ import type { InferInsertModel } from 'drizzle-orm' import { updateList } from '@/db/repositories/list-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { schema } from '@/db/schema' export type UpdateListValues = Omit< @@ -13,11 +14,16 @@ type UpdateListInput = { values: UpdateListValues } -export async function updateListService({ +const updateListServiceImpl = async ({ id, userId, values, -}: UpdateListInput) { +}: UpdateListInput) => { const [updatedList] = await updateList(id, userId, values) return { list: updatedList } } + +export const updateListService = withServiceTracing( + 'update-list', + updateListServiceImpl +) diff --git a/apps/backend/src/domain/services/login/login.ts b/apps/backend/src/domain/services/login/login.ts index fce21756..0d8a5a46 100644 --- a/apps/backend/src/domain/services/login/login.ts +++ b/apps/backend/src/domain/services/login/login.ts @@ -1,5 +1,6 @@ import type { z } from 'zod' import { findUserByEmailOrUsername } from '@/db/repositories/login-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { InvalidPasswordError } from '@/domain/errors/invalid-password-error' import type { loginBodySchema } from '@/http/schemas/login' import { comparePassword } from '@/utils/password' @@ -9,7 +10,7 @@ import { sendMagicLinkEmailService } from '../magic-link/send-magic-link-email' type LoginInput = z.infer -export async function loginService({ login, password, url }: LoginInput) { +const loginServiceImpl = async ({ login, password, url }: LoginInput) => { const user = await findUserByEmailOrUsername(login) if (!user) { @@ -31,3 +32,5 @@ export async function loginService({ login, password, url }: LoginInput) { const { password: removedPassword, ...formattedUser } = user return { user: formattedUser } } + +export const loginService = withServiceTracing('login', loginServiceImpl) diff --git a/apps/backend/src/domain/services/magic-link/generate-magic-link.ts b/apps/backend/src/domain/services/magic-link/generate-magic-link.ts index a5a67144..e055b792 100644 --- a/apps/backend/src/domain/services/magic-link/generate-magic-link.ts +++ b/apps/backend/src/domain/services/magic-link/generate-magic-link.ts @@ -1,11 +1,18 @@ import { randomBytes } from 'node:crypto' + +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { insertMagicToken } from '@/db/repositories/magic-tokens' const FIFTEEN_MINUTES = new Date(Date.now() + 15 * 60000) -export async function generateMagicLinkTokenService(userId: string) { +const generateMagicLinkTokenServiceImpl = async (userId: string) => { const token = randomBytes(32).toString('hex') await insertMagicToken({ token, userId, expiresAt: FIFTEEN_MINUTES }) return { token } } + +export const generateMagicLinkTokenService = withServiceTracing( + 'generate-magic-link-token', + generateMagicLinkTokenServiceImpl +) diff --git a/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts b/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts index df7292a1..89470243 100644 --- a/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts +++ b/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts @@ -1,4 +1,5 @@ import { config } from '@/config' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { EmailMessage } from '@/domain/entities/email-message' import { emailServiceFactory } from '@/factories/resend-factory' @@ -8,11 +9,11 @@ type SendMagicLinkEmailServiceInput = { url?: string } -export async function sendMagicLinkEmailService({ +const sendMagicLinkEmailServiceImpl = async ({ email, token, url: _url, -}: SendMagicLinkEmailServiceInput) { +}: SendMagicLinkEmailServiceInput) => { const link = `${config.app.CLIENT_URL}/reset-password?token=${token}` const html = `Please use the following link to login and set your new password: Login` @@ -30,3 +31,8 @@ export async function sendMagicLinkEmailService({ await emailService.sendEmail(emailMessage) } + +export const sendMagicLinkEmailService = withServiceTracing( + 'send-magic-link-email', + sendMagicLinkEmailServiceImpl +) diff --git a/apps/backend/src/domain/services/review-replies/create-review-reply.ts b/apps/backend/src/domain/services/review-replies/create-review-reply.ts index c9f09649..14952123 100644 --- a/apps/backend/src/domain/services/review-replies/create-review-reply.ts +++ b/apps/backend/src/domain/services/review-replies/create-review-reply.ts @@ -1,4 +1,5 @@ import { insertReviewReply } from '@/db/repositories/review-replies-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { getPostgresError, isForeignKeyViolation, @@ -7,7 +8,9 @@ import type { InsertReviewReplyModel } from '@/domain/entities/review-reply' import { ReviewNotFoundError } from '@/domain/errors/review-not-found-error' import { UserNotFoundError } from '@/domain/errors/user-not-found' -export async function createReviewReplyService(params: InsertReviewReplyModel) { +const createReviewReplyServiceImpl = async ( + params: InsertReviewReplyModel +) => { try { const [reviewReply] = await insertReviewReply(params) @@ -29,3 +32,8 @@ export async function createReviewReplyService(params: InsertReviewReplyModel) { throw error } } + +export const createReviewReplyService = withServiceTracing( + 'create-review-reply', + createReviewReplyServiceImpl +) diff --git a/apps/backend/src/domain/services/reviews/create-review.ts b/apps/backend/src/domain/services/reviews/create-review.ts index aede592b..b1fb18dc 100644 --- a/apps/backend/src/domain/services/reviews/create-review.ts +++ b/apps/backend/src/domain/services/reviews/create-review.ts @@ -1,9 +1,10 @@ import { insertReview } from '@/db/repositories/reviews-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import { UserNotFoundError } from '@/domain/errors/user-not-found' import type { InsertReviewModel } from '../../entities/review' -export async function createReviewService(params: InsertReviewModel) { +const createReviewServiceImpl = async (params: InsertReviewModel) => { try { const [review] = await insertReview(params) @@ -16,3 +17,8 @@ export async function createReviewService(params: InsertReviewModel) { throw error } } + +export const createReviewService = withServiceTracing( + 'create-review', + createReviewServiceImpl +) diff --git a/apps/backend/src/domain/services/reviews/delete-review.ts b/apps/backend/src/domain/services/reviews/delete-review.ts index 8a1762ac..37a47678 100644 --- a/apps/backend/src/domain/services/reviews/delete-review.ts +++ b/apps/backend/src/domain/services/reviews/delete-review.ts @@ -1,7 +1,8 @@ import { deleteReview } from '@/db/repositories/reviews-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ReviewNotFoundError } from '@/domain/errors/review-not-found-error' -export async function deleteReviewService(id: string) { +const deleteReviewServiceImpl = async (id: string) => { const [review] = await deleteReview(id) if (!review) { @@ -10,3 +11,8 @@ export async function deleteReviewService(id: string) { return review } + +export const deleteReviewService = withServiceTracing( + 'delete-review', + deleteReviewServiceImpl +) diff --git a/apps/backend/src/domain/services/reviews/get-review.ts b/apps/backend/src/domain/services/reviews/get-review.ts index 208f2ff3..da031ef4 100644 --- a/apps/backend/src/domain/services/reviews/get-review.ts +++ b/apps/backend/src/domain/services/reviews/get-review.ts @@ -1,4 +1,5 @@ import { selectReview } from '@/db/repositories/reviews-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { getReviewQuerySchema } from '@/http/schemas/reviews' export type GetReviewInput = { @@ -9,8 +10,13 @@ export type GetReviewInput = { episodeNumber?: number } -export async function getReviewService(input: GetReviewInput) { +const getReviewServiceImpl = async (input: GetReviewInput) => { const [review] = await selectReview(input) return { review: review || null } } + +export const getReviewService = withServiceTracing( + 'get-review', + getReviewServiceImpl +) diff --git a/apps/backend/src/domain/services/reviews/get-reviews.ts b/apps/backend/src/domain/services/reviews/get-reviews.ts index d9c4a97b..fb0cd293 100644 --- a/apps/backend/src/domain/services/reviews/get-reviews.ts +++ b/apps/backend/src/domain/services/reviews/get-reviews.ts @@ -7,6 +7,7 @@ import { startOfWeek, } from 'date-fns' import { selectReviews } from '@/db/repositories/reviews-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { getReviewsQuerySchema } from '@/http/schemas/reviews' export type GetReviewsServiceInput = Omit< @@ -53,9 +54,14 @@ function getIntervalDate(interval: GetReviewsServiceInput['interval']) { } } -export async function getReviewsService(input: GetReviewsServiceInput) { +const getReviewsServiceImpl = async (input: GetReviewsServiceInput) => { const { startDate, endDate } = getIntervalDate(input.interval) const reviews = await selectReviews({ ...input, startDate, endDate }) return { reviews } } + +export const getReviewsService = withServiceTracing( + 'get-reviews', + getReviewsServiceImpl +) diff --git a/apps/backend/src/domain/services/reviews/update-review.ts b/apps/backend/src/domain/services/reviews/update-review.ts index adba9460..dfefa4db 100644 --- a/apps/backend/src/domain/services/reviews/update-review.ts +++ b/apps/backend/src/domain/services/reviews/update-review.ts @@ -1,4 +1,5 @@ import { updateReview } from '@/db/repositories/reviews-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { reviewParamsSchema, updateReviewBodySchema, @@ -7,8 +8,13 @@ import type { export type UpdateReviewInput = typeof updateReviewBodySchema._type & typeof reviewParamsSchema._type -export async function updateReviewService(input: UpdateReviewInput) { +const updateReviewServiceImpl = async (input: UpdateReviewInput) => { const [review] = await updateReview(input) return { review } } + +export const updateReviewService = withServiceTracing( + 'update-review', + updateReviewServiceImpl +) diff --git a/apps/backend/src/domain/services/social-links/get-social-links.ts b/apps/backend/src/domain/services/social-links/get-social-links.ts index 6556e63a..2ff3b26f 100644 --- a/apps/backend/src/domain/services/social-links/get-social-links.ts +++ b/apps/backend/src/domain/services/social-links/get-social-links.ts @@ -1,11 +1,17 @@ import { selectSocialLinks } from '@/db/repositories/social-links-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' type Input = { userId: string } -export async function getSocialLinksService({ userId }: Input) { +const getSocialLinksServiceImpl = async ({ userId }: Input) => { const socialLinks = await selectSocialLinks(userId) return { socialLinks } } + +export const getSocialLinksService = withServiceTracing( + 'get-social-links', + getSocialLinksServiceImpl +) diff --git a/apps/backend/src/domain/services/social-links/upsert-social-links.ts b/apps/backend/src/domain/services/social-links/upsert-social-links.ts index d47fa327..74bc0611 100644 --- a/apps/backend/src/domain/services/social-links/upsert-social-links.ts +++ b/apps/backend/src/domain/services/social-links/upsert-social-links.ts @@ -2,6 +2,7 @@ import { deleteSocialLink, insertSocialLink, } from '@/db/repositories/social-links-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { InsertSocialLink } from '@/domain/entities/social-link' import type { socialLinksBodySchema } from '@/http/schemas/social-links' @@ -10,7 +11,7 @@ type Input = { userId: string } -export async function upsertSocialLinksService({ userId, values }: Input) { +const upsertSocialLinksServiceImpl = async ({ userId, values }: Input) => { const updates = Object.entries(values).map(async ([platform, url]) => { if (url) { const teste = await insertSocialLink({ @@ -27,3 +28,8 @@ export async function upsertSocialLinksService({ userId, values }: Input) { await Promise.all(updates) } + +export const upsertSocialLinksService = withServiceTracing( + 'upsert-social-links', + upsertSocialLinksServiceImpl +) diff --git a/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts b/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts index 19ebbf11..85e90bcf 100644 --- a/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts +++ b/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts @@ -1,4 +1,6 @@ import type { FastifyRedis } from '@fastify/redis' + +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { Language } from '@plotwist_app/tmdb' import { tmdb } from '@/adapters/tmdb' @@ -10,10 +12,10 @@ type GetTMDBDataServiceInput = { const THIRTY_DAYS_IN_SECONDS = 30 * 24 * 60 * 60 -export async function getTMDBDataService( +const getTMDBDataServiceImpl = async ( redis: FastifyRedis, input: GetTMDBDataServiceInput -) { +) => { const { mediaType, language, tmdbId } = input const cacheKey = `${mediaType}:${tmdbId}:${language}` @@ -64,3 +66,8 @@ export async function getTMDBDataService( backdropPath: data.backdrop_path, } } + +export const getTMDBDataService = withServiceTracing( + 'get-tmdb-data', + getTMDBDataServiceImpl +) diff --git a/apps/backend/src/domain/services/user-activities/create-user-activity.ts b/apps/backend/src/domain/services/user-activities/create-user-activity.ts index 3d6b29bc..78a1345c 100644 --- a/apps/backend/src/domain/services/user-activities/create-user-activity.ts +++ b/apps/backend/src/domain/services/user-activities/create-user-activity.ts @@ -1,6 +1,12 @@ import { insertUserActivity } from '@/db/repositories/user-activities' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { InsertUserActivity } from '@/domain/entities/user-activity' -export async function createUserActivity(params: InsertUserActivity) { +const createUserActivityImpl = async (params: InsertUserActivity) => { return await insertUserActivity(params) } + +export const createUserActivity = withServiceTracing( + 'create-user-activity', + createUserActivityImpl +) diff --git a/apps/backend/src/domain/services/user-items/get-user-items.ts b/apps/backend/src/domain/services/user-items/get-user-items.ts index 613a6693..35f8d9c2 100644 --- a/apps/backend/src/domain/services/user-items/get-user-items.ts +++ b/apps/backend/src/domain/services/user-items/get-user-items.ts @@ -1,7 +1,8 @@ import { selectUserItems } from '@/db/repositories/user-item-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { SelectUserItems } from '@/domain/entities/user-item' -export async function getUserItemsService(input: SelectUserItems) { +const getUserItemsServiceImpl = async (input: SelectUserItems) => { try { const userItems = await selectUserItems({ ...input }) @@ -16,3 +17,8 @@ export async function getUserItemsService(input: SelectUserItems) { throw error } } + +export const getUserItemsService = withServiceTracing( + 'get-user-items', + getUserItemsServiceImpl +) diff --git a/apps/backend/src/domain/services/user-items/upsert-user-item.ts b/apps/backend/src/domain/services/user-items/upsert-user-item.ts index b63dbdee..53c1bc4c 100644 --- a/apps/backend/src/domain/services/user-items/upsert-user-item.ts +++ b/apps/backend/src/domain/services/user-items/upsert-user-item.ts @@ -1,9 +1,16 @@ import * as changeKeys from 'change-case/keys' + +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { upsertUserItem } from '@/db/repositories/user-item-repository' import type { InsertUserItem, UserItem } from '@/domain/entities/user-item' -export async function upsertUserItemService(values: InsertUserItem) { +const upsertUserItemServiceImpl = async (values: InsertUserItem) => { const [userItem] = await upsertUserItem(values) return { userItem: changeKeys.camelCase(userItem) as UserItem } } + +export const upsertUserItemService = withServiceTracing( + 'upsert-user-item', + upsertUserItemServiceImpl +) diff --git a/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts b/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts index 93e29b84..5dc9da29 100644 --- a/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts +++ b/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts @@ -1,13 +1,19 @@ import { selectUserPreferences } from '@/db/repositories/user-preferences' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetUserPreferencesParams = { userId: string } -export async function getUserPreferencesService({ +const getUserPreferencesServiceImpl = async ({ userId, -}: GetUserPreferencesParams) { +}: GetUserPreferencesParams) => { const [userPreferences] = await selectUserPreferences(userId) return { userPreferences: userPreferences ?? null } } + +export const getUserPreferencesService = withServiceTracing( + 'get-user-preferences', + getUserPreferencesServiceImpl +) diff --git a/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts b/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts index abf30236..4c2947a6 100644 --- a/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts +++ b/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts @@ -1,10 +1,16 @@ import { updateUserPreferences } from '@/db/repositories/user-preferences' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { UpdateUserPreferencesParams } from '@/domain/entities/user-preferences' -export async function updateUserPreferencesService( +const updateUserPreferencesServiceImpl = async ( params: UpdateUserPreferencesParams -) { +) => { const [userPreferences] = await updateUserPreferences(params) return { userPreferences } } + +export const updateUserPreferencesService = withServiceTracing( + 'update-user-preferences', + updateUserPreferencesServiceImpl +) diff --git a/apps/backend/src/domain/services/users/create-user.ts b/apps/backend/src/domain/services/users/create-user.ts index 6bfc39f9..a7c667cd 100644 --- a/apps/backend/src/domain/services/users/create-user.ts +++ b/apps/backend/src/domain/services/users/create-user.ts @@ -1,4 +1,5 @@ import { insertUser } from '@/db/repositories/user-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isUniqueViolation } from '@/db/utils/postgres-errors' import { hashPassword } from '@/utils/password' import { EmailOrUsernameAlreadyRegisteredError } from '../../errors/email-or-username-already-registered-error' @@ -11,12 +12,12 @@ export type CreateUserInterface = { displayName?: string } -export async function createUser({ +const createUserImpl = async ({ username, email, password, displayName, -}: CreateUserInterface) { +}: CreateUserInterface) => { let hashedPassword: string try { @@ -44,3 +45,5 @@ export async function createUser({ throw error } } + +export const createUser = withServiceTracing('create-user', createUserImpl) diff --git a/apps/backend/src/domain/services/users/get-by-id.ts b/apps/backend/src/domain/services/users/get-by-id.ts index cdb44273..859a4ebb 100644 --- a/apps/backend/src/domain/services/users/get-by-id.ts +++ b/apps/backend/src/domain/services/users/get-by-id.ts @@ -1,7 +1,8 @@ import { getUserById as getById } from '@/db/repositories/user-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { UserNotFoundError } from '../../errors/user-not-found' -export async function getUserById(id: string) { +const getUserByIdImpl = async (id: string) => { const [user] = await getById(id) if (!user) { @@ -10,3 +11,5 @@ export async function getUserById(id: string) { return { user } } + +export const getUserById = withServiceTracing('get-user-by-id', getUserByIdImpl) diff --git a/apps/backend/src/domain/services/users/get-user-by-username.ts b/apps/backend/src/domain/services/users/get-user-by-username.ts index 3afdc66b..9df362df 100644 --- a/apps/backend/src/domain/services/users/get-user-by-username.ts +++ b/apps/backend/src/domain/services/users/get-user-by-username.ts @@ -1,11 +1,14 @@ import { getUserByUsername as getByUsername } from '@/db/repositories/user-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { UserNotFoundError } from '../../errors/user-not-found' type GetUserByUsernameInput = { username: string } -export async function getUserByUsername({ username }: GetUserByUsernameInput) { +const getUserByUsernameImpl = async ({ + username, +}: GetUserByUsernameInput) => { const [user] = await getByUsername(username) if (!user) { @@ -14,3 +17,8 @@ export async function getUserByUsername({ username }: GetUserByUsernameInput) { return { user } } + +export const getUserByUsername = withServiceTracing( + 'get-user-by-username', + getUserByUsernameImpl +) diff --git a/apps/backend/src/domain/services/users/is-email-available.ts b/apps/backend/src/domain/services/users/is-email-available.ts index 4051ee8a..45d5cee7 100644 --- a/apps/backend/src/domain/services/users/is-email-available.ts +++ b/apps/backend/src/domain/services/users/is-email-available.ts @@ -1,11 +1,14 @@ import { getUserByEmail } from '@/db/repositories/user-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { EmailAlreadyRegisteredError } from '../../errors/email-already-registered' type IsEmailAvailableInterface = { email: string } -export async function isEmailAvailable({ email }: IsEmailAvailableInterface) { +const isEmailAvailableImpl = async ({ + email, +}: IsEmailAvailableInterface) => { const [user] = await getUserByEmail(email) if (user) { @@ -14,3 +17,8 @@ export async function isEmailAvailable({ email }: IsEmailAvailableInterface) { return { available: true } } + +export const isEmailAvailable = withServiceTracing( + 'is-email-available', + isEmailAvailableImpl +) diff --git a/apps/backend/src/domain/services/users/is-username-available.ts b/apps/backend/src/domain/services/users/is-username-available.ts index a71e7170..e7a3255b 100644 --- a/apps/backend/src/domain/services/users/is-username-available.ts +++ b/apps/backend/src/domain/services/users/is-username-available.ts @@ -1,13 +1,14 @@ import { getUserByUsername } from '@/db/repositories/user-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { UsernameAlreadyRegisteredError } from '../../errors/username-already-registered' interface IsUsernameAvailableInterface { username: string } -export async function checkAvailableUsername({ +const checkAvailableUsernameImpl = async ({ username, -}: IsUsernameAvailableInterface) { +}: IsUsernameAvailableInterface) => { const [user] = await getUserByUsername(username) if (user) { @@ -16,3 +17,8 @@ export async function checkAvailableUsername({ return { available: true } } + +export const checkAvailableUsername = withServiceTracing( + 'check-username-available', + checkAvailableUsernameImpl +) diff --git a/apps/backend/src/domain/services/users/search-users-by-username.ts b/apps/backend/src/domain/services/users/search-users-by-username.ts index 1de9526c..3dff1002 100644 --- a/apps/backend/src/domain/services/users/search-users-by-username.ts +++ b/apps/backend/src/domain/services/users/search-users-by-username.ts @@ -1,7 +1,13 @@ import { listUsersByUsernameLike } from '@/db/repositories/user-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -export async function searchUsersByUsername(username: string) { +const searchUsersByUsernameImpl = async (username: string) => { const users = await listUsersByUsernameLike(username) return users } + +export const searchUsersByUsername = withServiceTracing( + 'search-users-by-username', + searchUsersByUsernameImpl +) diff --git a/apps/backend/src/domain/services/users/update-user-password.ts b/apps/backend/src/domain/services/users/update-user-password.ts index c7853f37..a9d3217d 100644 --- a/apps/backend/src/domain/services/users/update-user-password.ts +++ b/apps/backend/src/domain/services/users/update-user-password.ts @@ -2,6 +2,7 @@ import { invalidateMagicToken, selectMagicToken, } from '@/db/repositories/magic-tokens' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { updateUserPassword } from '@/db/repositories/user-repository' import { InvalidTokenError } from '@/domain/errors/invalid-token-error' import type { updateUserPasswordBodySchema } from '@/http/schemas/users' @@ -9,10 +10,10 @@ import { hashPassword } from '@/utils/password' type UpdatePasswordInput = typeof updateUserPasswordBodySchema._type -export async function updatePasswordService({ +const updatePasswordServiceImpl = async ({ password, token, -}: UpdatePasswordInput) { +}: UpdatePasswordInput) => { const [tokenRecord] = await selectMagicToken(token) if (!token) { @@ -25,3 +26,8 @@ export async function updatePasswordService({ return { status: 'password_set' } } + +export const updatePasswordService = withServiceTracing( + 'update-password', + updatePasswordServiceImpl +) diff --git a/apps/backend/src/domain/services/users/update-user.ts b/apps/backend/src/domain/services/users/update-user.ts index c445e367..6e735d91 100644 --- a/apps/backend/src/domain/services/users/update-user.ts +++ b/apps/backend/src/domain/services/users/update-user.ts @@ -1,4 +1,5 @@ import { getUserById, updateUser } from '@/db/repositories/user-repository' +import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isUniqueViolation } from '@/db/utils/postgres-errors' import { NoValidFieldsError } from '@/domain/errors/no-valid-fields' import { UserNotFoundError } from '@/domain/errors/user-not-found' @@ -7,12 +8,12 @@ import type { updateUserBodySchema } from '@/http/schemas/users' export type UpdateUserInput = typeof updateUserBodySchema._type -export async function updateUserService({ +const updateUserServiceImpl = async ({ userId, ...data }: UpdateUserInput & { userId: string -}) { +}) => { const validData = Object.fromEntries( Object.entries(data).filter(([_, value]) => value !== undefined) ) @@ -44,3 +45,8 @@ export async function updateUserService({ throw error } } + +export const updateUserService = withServiceTracing( + 'update-user', + updateUserServiceImpl +) diff --git a/apps/backend/src/infra/telemetry/with-service-tracing.ts b/apps/backend/src/infra/telemetry/with-service-tracing.ts new file mode 100644 index 00000000..a236f25d --- /dev/null +++ b/apps/backend/src/infra/telemetry/with-service-tracing.ts @@ -0,0 +1,26 @@ +import { trace } from '@opentelemetry/api' + +// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any service signature +export function withServiceTracing any>( + spanName: string, + fn: T +): T { + const tracer = trace.getTracer('plotwist-api', '0.1.0') + const fullSpanName = spanName.endsWith('-service') + ? spanName + : `${spanName}-service` + + return (async (...args: Parameters) => { + return tracer.startActiveSpan(fullSpanName, async span => { + try { + const result = await fn(...args) + return result + } catch (err) { + span.recordException(err as Error) + throw err + } finally { + span.end() + } + }) + }) as T +} diff --git a/apps/backend/src/infra/telemetry/with-tracing.ts b/apps/backend/src/infra/telemetry/with-tracing.ts index cbdc59ea..55e88254 100644 --- a/apps/backend/src/infra/telemetry/with-tracing.ts +++ b/apps/backend/src/infra/telemetry/with-tracing.ts @@ -8,9 +8,12 @@ export function withTracing any>( options?: { method?: string; url?: string } ): T { const tracer = trace.getTracer('plotwist-api', '0.1.0') + const fullSpanName = spanName.endsWith('-controller') + ? spanName + : `${spanName}-controller` return (async (...args: Parameters) => { - return tracer.startActiveSpan(spanName, async span => { + return tracer.startActiveSpan(fullSpanName, async span => { try { const request = args[0] as FastifyRequest | undefined if (request) { diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index a3e4680c..c4b7818f 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import './.next/dev/types/routes.d.ts' +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From 2c59e6757fabfb3a9699f413c6705c17a959da97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 17 Feb 2026 22:25:20 -0300 Subject: [PATCH 06/26] feat: implement telemetry tracing for database and adapter operations --- apps/backend/src/adapters/my-anime-list.ts | 11 ++- apps/backend/src/adapters/open-ai.ts | 3 +- apps/backend/src/adapters/r2-storage.ts | 5 +- apps/backend/src/adapters/resend.ts | 3 +- apps/backend/src/adapters/sqs.ts | 12 ++-- .../db/repositories/feedback-repository.ts | 5 +- .../db/repositories/followers-repository.ts | 28 +++++--- .../src/db/repositories/likes-repository.ts | 13 +++- .../db/repositories/list-item-repository.ts | 35 +++++++-- .../src/db/repositories/list-repository.ts | 34 ++++++--- .../src/db/repositories/login-repository.ts | 8 ++- .../src/db/repositories/magic-tokens.ts | 22 +++++- .../repositories/review-replies-repository.ts | 38 ++++++++-- .../src/db/repositories/reviews-repository.ts | 51 ++++++++++--- .../repositories/social-links-repository.ts | 26 +++++-- .../repositories/subscription-repository.ts | 47 ++++++++++-- .../src/db/repositories/user-activities.ts | 42 ++++++++--- .../src/db/repositories/user-episode.ts | 31 ++++++-- .../db/repositories/user-item-repository.ts | 71 +++++++++++++++---- .../src/db/repositories/user-preferences.ts | 17 ++++- .../src/db/repositories/user-repository.ts | 50 ++++++++++--- .../backend/src/db/repositories/user-stats.ts | 8 ++- .../user-watch-entries-repository.ts | 38 ++++++++-- .../infra/telemetry/with-adapter-tracing.ts | 26 +++++++ .../src/infra/telemetry/with-db-tracing.ts | 26 +++++++ 25 files changed, 533 insertions(+), 117 deletions(-) create mode 100644 apps/backend/src/infra/telemetry/with-adapter-tracing.ts create mode 100644 apps/backend/src/infra/telemetry/with-db-tracing.ts diff --git a/apps/backend/src/adapters/my-anime-list.ts b/apps/backend/src/adapters/my-anime-list.ts index a9524c35..efa9b870 100644 --- a/apps/backend/src/adapters/my-anime-list.ts +++ b/apps/backend/src/adapters/my-anime-list.ts @@ -1,10 +1,11 @@ import axios from 'axios' import type { AnimeDetails } from '@/@types/my-anime-list-request' import { config } from '@/config' +import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' const BASE_URL = 'https://api.myanimelist.net/v2' -export async function searchAnime(query: string) { +async function searchAnimeImpl(query: string) { try { const response = await axios.get(`${BASE_URL}/anime`, { params: { q: query, limit: 5 }, @@ -18,7 +19,7 @@ export async function searchAnime(query: string) { } } -export async function searchAnimeById(animedbId: string) { +async function searchAnimeByIdImpl(animedbId: string) { try { const response = await axios.get(`${BASE_URL}/anime/${animedbId}`, { params: { @@ -36,3 +37,9 @@ export async function searchAnimeById(animedbId: string) { ) } } + +export const searchAnime = withAdapterTracing('mal-search-anime', searchAnimeImpl) +export const searchAnimeById = withAdapterTracing( + 'mal-search-anime-by-id', + searchAnimeByIdImpl +) diff --git a/apps/backend/src/adapters/open-ai.ts b/apps/backend/src/adapters/open-ai.ts index e64ca971..90b233ff 100644 --- a/apps/backend/src/adapters/open-ai.ts +++ b/apps/backend/src/adapters/open-ai.ts @@ -1,5 +1,6 @@ import OpenAI from 'openai' import { config } from '@/config' +import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { AIService } from '@/ports/ai-service' const openai = new OpenAI({ @@ -27,7 +28,7 @@ async function generateMessage(prompt: string, content: string) { } const OpenAIService: AIService = { - generateMessage: (prefix, content) => generateMessage(prefix, content), + generateMessage: withAdapterTracing('openai-generate-message', generateMessage), } export { OpenAIService } diff --git a/apps/backend/src/adapters/r2-storage.ts b/apps/backend/src/adapters/r2-storage.ts index 3bcfe5aa..b6af0cff 100644 --- a/apps/backend/src/adapters/r2-storage.ts +++ b/apps/backend/src/adapters/r2-storage.ts @@ -6,6 +6,7 @@ import { import { Upload } from '@aws-sdk/lib-storage' import type { UploadImageInput } from '@/@types/r2-storage' import { config } from '@/config' +import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { CloudStorage } from '@/ports/cloud-storage' const r2Storage = new S3Client({ @@ -75,8 +76,8 @@ async function uploadImage({ } const R2Storage: CloudStorage = { - deleteOldImages: prefix => deleteOldImages(prefix), - uploadImage: uploadImageInput => uploadImage(uploadImageInput), + deleteOldImages: withAdapterTracing('r2-delete-old-images', deleteOldImages), + uploadImage: withAdapterTracing('r2-upload-image', uploadImage), } export { R2Storage } diff --git a/apps/backend/src/adapters/resend.ts b/apps/backend/src/adapters/resend.ts index b91c7f2a..ad74a057 100644 --- a/apps/backend/src/adapters/resend.ts +++ b/apps/backend/src/adapters/resend.ts @@ -1,6 +1,7 @@ import { Resend } from 'resend' import { config } from '@/config' import type { EmailMessage } from '@/domain/entities/email-message' +import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { EmailService } from '@/ports/email-service' const resend = new Resend(config.services.RESEND_API_KEY) @@ -15,7 +16,7 @@ async function sendEmail(emailMessage: EmailMessage) { } const ResendAdapter: EmailService = { - sendEmail: emailMessage => sendEmail(emailMessage), + sendEmail: withAdapterTracing('resend-send-email', sendEmail), } export { ResendAdapter } diff --git a/apps/backend/src/adapters/sqs.ts b/apps/backend/src/adapters/sqs.ts index 1e5aec46..d5d3b902 100644 --- a/apps/backend/src/adapters/sqs.ts +++ b/apps/backend/src/adapters/sqs.ts @@ -8,6 +8,7 @@ import { } from '@aws-sdk/client-sqs' import { config } from '@/config' import type { QueueMessage } from '@/domain/entities/queue-message' +import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { QueueService } from '@/ports/queue-service' import { logger } from './logger' @@ -118,11 +119,12 @@ async function deleteMessage(queueUrl: string, receiptHandle: string) { } const SQSAdapter: QueueService = { - publish: queueMessage => publish(queueMessage), - receiveMessage: queueUrl => receiveMessage(queueUrl), - initialize: () => initializeSQS(createSqsClient()), - deleteMessage: (queueUrl, receiptHandle) => - deleteMessage(queueUrl, receiptHandle), + publish: withAdapterTracing('sqs-publish', publish), + receiveMessage: withAdapterTracing('sqs-receive-message', receiveMessage), + initialize: withAdapterTracing('sqs-initialize', () => + initializeSQS(createSqsClient()) + ), + deleteMessage: withAdapterTracing('sqs-delete-message', deleteMessage), } export { SQSAdapter } diff --git a/apps/backend/src/db/repositories/feedback-repository.ts b/apps/backend/src/db/repositories/feedback-repository.ts index fbe72f74..4be1d01f 100644 --- a/apps/backend/src/db/repositories/feedback-repository.ts +++ b/apps/backend/src/db/repositories/feedback-repository.ts @@ -1,7 +1,10 @@ import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertFeedbackModel } from '@/domain/entities/feedback' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -export async function insertFeedback(params: InsertFeedbackModel) { +const insertFeedbackImpl = async (params: InsertFeedbackModel) => { return db.insert(schema.feedbacks).values(params).returning() } + +export const insertFeedback = withDbTracing('insert-feedback', insertFeedbackImpl) diff --git a/apps/backend/src/db/repositories/followers-repository.ts b/apps/backend/src/db/repositories/followers-repository.ts index e2d6e8c9..bfc76531 100644 --- a/apps/backend/src/db/repositories/followers-repository.ts +++ b/apps/backend/src/db/repositories/followers-repository.ts @@ -3,13 +3,14 @@ import type { CreateFollowServiceInput } from '@/domain/services/follows/create- import type { DeleteFollowServiceInput } from '@/domain/services/follows/delete-follow' import type { GetFollowServiceInput } from '@/domain/services/follows/get-follow' import type { GetFollowersInput } from '@/domain/services/follows/get-followers' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function insertFollow({ +const insertFollowImpl = async ({ followedId, followerId, -}: CreateFollowServiceInput) { +}: CreateFollowServiceInput) => { return db .insert(schema.followers) .values({ @@ -19,10 +20,12 @@ export async function insertFollow({ .returning() } -export async function getFollow({ +export const insertFollow = withDbTracing('insert-follow', insertFollowImpl) + +const getFollowImpl = async ({ followedId, followerId, -}: GetFollowServiceInput) { +}: GetFollowServiceInput) => { return db .select() .from(schema.followers) @@ -34,10 +37,12 @@ export async function getFollow({ ) } -export async function deleteFollow({ +export const getFollow = withDbTracing('get-follow', getFollowImpl) + +const deleteFollowImpl = async ({ followedId, followerId, -}: DeleteFollowServiceInput) { +}: DeleteFollowServiceInput) => { return db .delete(schema.followers) .where( @@ -49,12 +54,14 @@ export async function deleteFollow({ .returning() } -export async function selectFollowers({ +export const deleteFollow = withDbTracing('delete-follow', deleteFollowImpl) + +const selectFollowersImpl = async ({ followedId, followerId, cursor, pageSize, -}: GetFollowersInput) { +}: GetFollowersInput) => { return db .select({ ...getTableColumns(schema.followers), @@ -89,3 +96,8 @@ export async function selectFollowers({ ) .limit(pageSize + 1) } + +export const selectFollowers = withDbTracing( + 'select-followers', + selectFollowersImpl +) diff --git a/apps/backend/src/db/repositories/likes-repository.ts b/apps/backend/src/db/repositories/likes-repository.ts index eaea4bc1..8a26fcbf 100644 --- a/apps/backend/src/db/repositories/likes-repository.ts +++ b/apps/backend/src/db/repositories/likes-repository.ts @@ -1,17 +1,22 @@ import { eq, getTableColumns, sql } from 'drizzle-orm' import type { InsertLike } from '@/domain/entities/likes' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function insertLike(values: InsertLike) { +const insertLikeImpl = async (values: InsertLike) => { return db.insert(schema.likes).values(values).returning() } -export async function deleteLike(id: string) { +export const insertLike = withDbTracing('insert-like', insertLikeImpl) + +const deleteLikeImpl = async (id: string) => { return db.delete(schema.likes).where(eq(schema.likes.id, id)).returning() } -export async function selectLikes(entityId: string) { +export const deleteLike = withDbTracing('delete-like', deleteLikeImpl) + +const selectLikesImpl = async (entityId: string) => { return db .select({ ...getTableColumns(schema.likes), @@ -30,3 +35,5 @@ export async function selectLikes(entityId: string) { eq(schema.users.id, schema.subscriptions.userId) ) } + +export const selectLikes = withDbTracing('select-likes', selectLikesImpl) diff --git a/apps/backend/src/db/repositories/list-item-repository.ts b/apps/backend/src/db/repositories/list-item-repository.ts index d53c8e83..1043f215 100644 --- a/apps/backend/src/db/repositories/list-item-repository.ts +++ b/apps/backend/src/db/repositories/list-item-repository.ts @@ -1,14 +1,20 @@ import { eq } from 'drizzle-orm' import type { InsertListItem } from '@/domain/entities/list-item' import type { UpdateListItemsServiceInput } from '@/domain/services/list-item/update-list-items' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function insertListItem(input: InsertListItem) { +const insertListItemImpl = async (input: InsertListItem) => { return db.insert(schema.listItems).values(input).returning() } -export async function selectListItems(listId: string) { +export const insertListItem = withDbTracing( + 'insert-list-item', + insertListItemImpl +) + +const selectListItemsImpl = async (listId: string) => { return db .select() .from(schema.listItems) @@ -16,20 +22,32 @@ export async function selectListItems(listId: string) { .orderBy(schema.listItems.position) } -export async function deleteListItem(id: string) { +export const selectListItems = withDbTracing( + 'select-list-items', + selectListItemsImpl +) + +const deleteListItemImpl = async (id: string) => { return db .delete(schema.listItems) .where(eq(schema.listItems.id, id)) .returning() } -export async function getListItem(id: string) { +export const deleteListItem = withDbTracing( + 'delete-list-item', + deleteListItemImpl +) + +const getListItemImpl = async (id: string) => { return db.select().from(schema.listItems).where(eq(schema.listItems.id, id)) } -export async function updateListItems({ +export const getListItem = withDbTracing('get-list-item', getListItemImpl) + +const updateListItemsImpl = async ({ listItems, -}: UpdateListItemsServiceInput) { +}: UpdateListItemsServiceInput) => { return db.transaction(async tx => { const promises = listItems.map(({ id, position }) => tx @@ -42,3 +60,8 @@ export async function updateListItems({ return await Promise.all(promises) }) } + +export const updateListItems = withDbTracing( + 'update-list-items', + updateListItemsImpl +) diff --git a/apps/backend/src/db/repositories/list-repository.ts b/apps/backend/src/db/repositories/list-repository.ts index 4e47e054..83915d7f 100644 --- a/apps/backend/src/db/repositories/list-repository.ts +++ b/apps/backend/src/db/repositories/list-repository.ts @@ -3,16 +3,17 @@ import type { InsertListModel } from '@/domain/entities/lists' import type { GetListsInput } from '@/domain/services/lists/get-lists' import type { UpdateListValues } from '@/domain/services/lists/update-list' import type { UpdateListBannerInput } from '@/domain/services/lists/update-list-banner' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export function selectLists({ +const selectListsImpl = ({ userId, limit = 5, authenticatedUserId, visibility, hasBanner, -}: GetListsInput) { +}: GetListsInput) => { return db .select({ ...getTableColumns(schema.lists), @@ -59,22 +60,28 @@ export function selectLists({ .limit(limit) } -export async function insertList(input: InsertListModel) { +export const selectLists = withDbTracing('select-lists', selectListsImpl) + +const insertListImpl = async (input: InsertListModel) => { return db .insert(schema.lists) .values({ ...input }) .returning() } -export async function deleteList(id: string) { +export const insertList = withDbTracing('insert-list', insertListImpl) + +const deleteListImpl = async (id: string) => { return db.delete(schema.lists).where(eq(schema.lists.id, id)).returning() } -export async function updateList( +export const deleteList = withDbTracing('delete-list', deleteListImpl) + +const updateListImpl = async ( id: string, userId: string, values: UpdateListValues -) { +) => { return db .update(schema.lists) .set(values) @@ -82,7 +89,9 @@ export async function updateList( .returning() } -export async function getListById(id: string, authenticatedUserId?: string) { +export const updateList = withDbTracing('update-list', updateListImpl) + +const getListByIdImpl = async (id: string, authenticatedUserId?: string) => { return db .select({ ...getTableColumns(schema.lists), @@ -109,14 +118,21 @@ export async function getListById(id: string, authenticatedUserId?: string) { .where(eq(schema.lists.id, id)) } -export async function updateListBanner({ +export const getListById = withDbTracing('get-list-by-id', getListByIdImpl) + +const updateListBannerImpl = async ({ listId, userId, bannerUrl, -}: UpdateListBannerInput) { +}: UpdateListBannerInput) => { return db .update(schema.lists) .set({ bannerUrl }) .where(and(eq(schema.lists.id, listId), eq(schema.lists.userId, userId))) .returning() } + +export const updateListBanner = withDbTracing( + 'update-list-banner', + updateListBannerImpl +) diff --git a/apps/backend/src/db/repositories/login-repository.ts b/apps/backend/src/db/repositories/login-repository.ts index 8530995c..72e502e6 100644 --- a/apps/backend/src/db/repositories/login-repository.ts +++ b/apps/backend/src/db/repositories/login-repository.ts @@ -1,8 +1,9 @@ import { or, sql } from 'drizzle-orm' import { schema } from '@/db/schema' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' -export async function findUserByEmailOrUsername(login?: string) { +const findUserByEmailOrUsernameImpl = async (login?: string) => { const [user] = await db .select() .from(schema.users) @@ -15,3 +16,8 @@ export async function findUserByEmailOrUsername(login?: string) { return user } + +export const findUserByEmailOrUsername = withDbTracing( + 'find-user-by-email-or-username', + findUserByEmailOrUsernameImpl +) diff --git a/apps/backend/src/db/repositories/magic-tokens.ts b/apps/backend/src/db/repositories/magic-tokens.ts index 100bbdd0..305d7888 100644 --- a/apps/backend/src/db/repositories/magic-tokens.ts +++ b/apps/backend/src/db/repositories/magic-tokens.ts @@ -1,22 +1,38 @@ import { eq } from 'drizzle-orm' import type { InsertMagicTokenModel } from '@/domain/entities/magic-token' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function insertMagicToken(values: InsertMagicTokenModel) { +const insertMagicTokenImpl = async (values: InsertMagicTokenModel) => { return db.insert(schema.magicTokens).values(values) } -export async function selectMagicToken(token: string) { +export const insertMagicToken = withDbTracing( + 'insert-magic-token', + insertMagicTokenImpl +) + +const selectMagicTokenImpl = async (token: string) => { return db .select() .from(schema.magicTokens) .where(eq(schema.magicTokens.token, token)) } -export async function invalidateMagicToken(token: string) { +export const selectMagicToken = withDbTracing( + 'select-magic-token', + selectMagicTokenImpl +) + +const invalidateMagicTokenImpl = async (token: string) => { return db .update(schema.magicTokens) .set({ used: true }) .where(eq(schema.magicTokens.token, token)) } + +export const invalidateMagicToken = withDbTracing( + 'invalidate-magic-token', + invalidateMagicTokenImpl +) diff --git a/apps/backend/src/db/repositories/review-replies-repository.ts b/apps/backend/src/db/repositories/review-replies-repository.ts index 08c34277..cd937c2c 100644 --- a/apps/backend/src/db/repositories/review-replies-repository.ts +++ b/apps/backend/src/db/repositories/review-replies-repository.ts @@ -2,26 +2,42 @@ import { and, asc, eq, getTableColumns, sql } from 'drizzle-orm' import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertReviewReplyModel } from '@/domain/entities/review-reply' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -export async function insertReviewReply(params: InsertReviewReplyModel) { +const insertReviewReplyImpl = async (params: InsertReviewReplyModel) => { return db.insert(schema.reviewReplies).values(params).returning() } -export async function deleteReviewReply(id: string) { +export const insertReviewReply = withDbTracing( + 'insert-review-reply', + insertReviewReplyImpl +) + +const deleteReviewReplyImpl = async (id: string) => { return db .delete(schema.reviewReplies) .where(and(eq(schema.reviewReplies.id, id))) .returning() } -export async function getReviewReplyById(id: string) { +export const deleteReviewReply = withDbTracing( + 'delete-review-reply', + deleteReviewReplyImpl +) + +const getReviewReplyByIdImpl = async (id: string) => { return db .select() .from(schema.reviewReplies) .where(eq(schema.reviewReplies.id, id)) } -export async function updateReviewReply(id: string, reply: string) { +export const getReviewReplyById = withDbTracing( + 'get-review-reply-by-id', + getReviewReplyByIdImpl +) + +const updateReviewReplyImpl = async (id: string, reply: string) => { return db .update(schema.reviewReplies) .set({ reply }) @@ -29,10 +45,15 @@ export async function updateReviewReply(id: string, reply: string) { .returning() } -export async function selectReviewReplies( +export const updateReviewReply = withDbTracing( + 'update-review-reply', + updateReviewReplyImpl +) + +const selectReviewRepliesImpl = async ( reviewId: string, authenticatedUserId?: string -) { +) => { return db .select({ ...getTableColumns(schema.reviewReplies), @@ -65,3 +86,8 @@ export async function selectReviewReplies( .leftJoin(schema.users, eq(schema.reviewReplies.userId, schema.users.id)) .orderBy(asc(schema.reviewReplies.createdAt)) } + +export const selectReviewReplies = withDbTracing( + 'select-review-replies', + selectReviewRepliesImpl +) diff --git a/apps/backend/src/db/repositories/reviews-repository.ts b/apps/backend/src/db/repositories/reviews-repository.ts index cca8e6c7..cc23b659 100644 --- a/apps/backend/src/db/repositories/reviews-repository.ts +++ b/apps/backend/src/db/repositories/reviews-repository.ts @@ -15,11 +15,14 @@ import type { InsertReviewModel } from '@/domain/entities/review' import type { GetReviewInput } from '@/domain/services/reviews/get-review' import type { GetReviewsServiceInput } from '@/domain/services/reviews/get-reviews' import type { UpdateReviewInput } from '@/domain/services/reviews/update-review' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -export async function insertReview(params: InsertReviewModel) { +const insertReviewImpl = async (params: InsertReviewModel) => { return db.insert(schema.reviews).values(params).returning() } +export const insertReview = withDbTracing('insert-review', insertReviewImpl) + function buildSeasonEpisodeFilter( seasonNumber?: number, episodeNumber?: number @@ -44,7 +47,7 @@ function buildSeasonEpisodeFilter( ) } -export async function selectReviews({ +const selectReviewsImpl = async ({ mediaType, tmdbId, userId, @@ -56,7 +59,7 @@ export async function selectReviews({ endDate, seasonNumber, episodeNumber, -}: GetReviewsServiceInput) { +}: GetReviewsServiceInput) => { const orderCriteria = [ orderBy === 'likeCount' ? desc( @@ -122,16 +125,23 @@ export async function selectReviews({ .offset(offset) } -export async function deleteReview(id: string) { +export const selectReviews = withDbTracing( + 'select-reviews', + selectReviewsImpl +) + +const deleteReviewImpl = async (id: string) => { return db.delete(schema.reviews).where(eq(schema.reviews.id, id)).returning() } -export async function updateReview({ +export const deleteReview = withDbTracing('delete-review', deleteReviewImpl) + +const updateReviewImpl = async ({ id, rating, review, hasSpoilers, -}: UpdateReviewInput) { +}: UpdateReviewInput) => { return db .update(schema.reviews) .set({ rating, review, hasSpoilers }) @@ -139,18 +149,30 @@ export async function updateReview({ .returning() } -export async function getReviewById(id: string) { +export const updateReview = withDbTracing('update-review', updateReviewImpl) + +const getReviewByIdImpl = async (id: string) => { return db.select().from(schema.reviews).where(eq(schema.reviews.id, id)) } -export async function selectReviewsCount(userId?: string) { +export const getReviewById = withDbTracing( + 'get-review-by-id', + getReviewByIdImpl +) + +const selectReviewsCountImpl = async (userId?: string) => { return db .select({ count: count() }) .from(schema.reviews) .where(userId ? eq(schema.reviews.userId, userId) : undefined) } -export async function selectBestReviews(userId: string, limit = 10) { +export const selectReviewsCount = withDbTracing( + 'select-reviews-count', + selectReviewsCountImpl +) + +const selectBestReviewsImpl = async (userId: string, limit = 10) => { return db .select() .from(schema.reviews) @@ -166,13 +188,18 @@ export async function selectBestReviews(userId: string, limit = 10) { .limit(limit) } -export async function selectReview({ +export const selectBestReviews = withDbTracing( + 'select-best-reviews', + selectBestReviewsImpl +) + +const selectReviewImpl = async ({ mediaType, tmdbId, userId, seasonNumber, episodeNumber, -}: GetReviewInput) { +}: GetReviewInput) => { return db .select() .from(schema.reviews) @@ -187,3 +214,5 @@ export async function selectReview({ ) ) } + +export const selectReview = withDbTracing('select-review', selectReviewImpl) diff --git a/apps/backend/src/db/repositories/social-links-repository.ts b/apps/backend/src/db/repositories/social-links-repository.ts index cf105fee..419d8ace 100644 --- a/apps/backend/src/db/repositories/social-links-repository.ts +++ b/apps/backend/src/db/repositories/social-links-repository.ts @@ -1,13 +1,14 @@ import { and, eq, sql } from 'drizzle-orm' import type { InsertSocialLink } from '@/domain/entities/social-link' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function insertSocialLink({ +const insertSocialLinkImpl = async ({ platform, url, userId, -}: InsertSocialLink) { +}: InsertSocialLink) => { return db.execute( sql` INSERT INTO ${schema.socialLinks} (user_id, platform, url) @@ -19,10 +20,15 @@ export async function insertSocialLink({ ) } -export async function deleteSocialLink( +export const insertSocialLink = withDbTracing( + 'insert-social-link', + insertSocialLinkImpl +) + +const deleteSocialLinkImpl = async ( userId: string, platform: InsertSocialLink['platform'] -) { +) => { return db .delete(schema.socialLinks) .where( @@ -33,9 +39,19 @@ export async function deleteSocialLink( ) } -export async function selectSocialLinks(userId: string) { +export const deleteSocialLink = withDbTracing( + 'delete-social-link', + deleteSocialLinkImpl +) + +const selectSocialLinksImpl = async (userId: string) => { return db .select() .from(schema.socialLinks) .where(eq(schema.socialLinks.userId, userId)) } + +export const selectSocialLinks = withDbTracing( + 'select-social-links', + selectSocialLinksImpl +) diff --git a/apps/backend/src/db/repositories/subscription-repository.ts b/apps/backend/src/db/repositories/subscription-repository.ts index 771b6444..ee79eecd 100644 --- a/apps/backend/src/db/repositories/subscription-repository.ts +++ b/apps/backend/src/db/repositories/subscription-repository.ts @@ -2,12 +2,18 @@ import { and, desc, eq, or } from 'drizzle-orm' import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertSubscriptionModel } from '@/domain/entities/subscription' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -export async function insertSubscription(params: InsertSubscriptionModel) { +const insertSubscriptionImpl = async (params: InsertSubscriptionModel) => { return db.insert(schema.subscriptions).values(params).returning() } -export async function getActiveSubscriptionByUserId(userId: string) { +export const insertSubscription = withDbTracing( + 'insert-subscription', + insertSubscriptionImpl +) + +const getActiveSubscriptionByUserIdImpl = async (userId: string) => { return db.query.subscriptions.findFirst({ where: and( eq(schema.subscriptions.userId, userId), @@ -16,12 +22,22 @@ export async function getActiveSubscriptionByUserId(userId: string) { }) } -export async function getSubscriptionById(id: string) { +export const getActiveSubscriptionByUserId = withDbTracing( + 'get-active-subscription-by-user-id', + getActiveSubscriptionByUserIdImpl +) + +const getSubscriptionByIdImpl = async (id: string) => { return db.query.subscriptions.findFirst({ where: eq(schema.subscriptions.id, id), }) } +export const getSubscriptionById = withDbTracing( + 'get-subscription-by-id', + getSubscriptionByIdImpl +) + export type CancelSubscriptionParams = { id: string userId: string @@ -30,7 +46,9 @@ export type CancelSubscriptionParams = { cancellationReason: string | undefined } -export async function cancelUserSubscription(params: CancelSubscriptionParams) { +const cancelUserSubscriptionImpl = async ( + params: CancelSubscriptionParams +) => { const [subscription] = await db .update(schema.subscriptions) .set({ @@ -49,10 +67,15 @@ export async function cancelUserSubscription(params: CancelSubscriptionParams) { return subscription } -export async function updateSubscription( +export const cancelUserSubscription = withDbTracing( + 'cancel-user-subscription', + cancelUserSubscriptionImpl +) + +const updateSubscriptionImpl = async ( userId: string, type: 'PRO' | 'MEMBER' -) { +) => { return db .update(schema.subscriptions) .set({ type }) @@ -60,7 +83,12 @@ export async function updateSubscription( .returning() } -export async function getLastestActiveSubscription(userId: string) { +export const updateSubscription = withDbTracing( + 'update-subscription', + updateSubscriptionImpl +) + +const getLastestActiveSubscriptionImpl = async (userId: string) => { const [subscription] = await db .select() .from(schema.subscriptions) @@ -82,3 +110,8 @@ export async function getLastestActiveSubscription(userId: string) { return subscription } + +export const getLastestActiveSubscription = withDbTracing( + 'get-lastest-active-subscription', + getLastestActiveSubscriptionImpl +) diff --git a/apps/backend/src/db/repositories/user-activities.ts b/apps/backend/src/db/repositories/user-activities.ts index 0b98070e..73b56518 100644 --- a/apps/backend/src/db/repositories/user-activities.ts +++ b/apps/backend/src/db/repositories/user-activities.ts @@ -6,18 +6,24 @@ import type { InsertUserActivity, SelectUserActivities, } from '@/domain/entities/user-activity' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function insertUserActivity(values: InsertUserActivity) { +const insertUserActivityImpl = async (values: InsertUserActivity) => { return db.insert(schema.userActivities).values(values) } -export async function selectUserActivities({ +export const insertUserActivity = withDbTracing( + 'insert-user-activity', + insertUserActivityImpl +) + +const selectUserActivitiesImpl = async ({ userIds, pageSize, cursor, -}: SelectUserActivities) { +}: SelectUserActivities) => { const additionalInfoCase = buildAdditionalInfoCase() const owner = alias(schema.users, 'owner') @@ -43,6 +49,11 @@ export async function selectUserActivities({ .leftJoin(owner, eq(schema.userActivities.userId, owner.id)) } +export const selectUserActivities = withDbTracing( + 'select-user-activities', + selectUserActivitiesImpl +) + function buildAdditionalInfoCase() { return sql` CASE @@ -179,12 +190,12 @@ function buildRepliesJoinCondition() { AND ${schema.userActivities.entityId} = ${schema.reviewReplies.id}` } -export async function deleteUserActivity({ +const deleteUserActivityImpl = async ({ activityType, entityId, entityType, userId, -}: DeleteUserActivity) { +}: DeleteUserActivity) => { return db .delete(schema.userActivities) .where( @@ -197,11 +208,16 @@ export async function deleteUserActivity({ ) } -export async function deleteFollowUserActivity({ +export const deleteUserActivity = withDbTracing( + 'delete-user-activity', + deleteUserActivityImpl +) + +const deleteFollowUserActivityImpl = async ({ followedId, followerId, userId, -}: DeleteFollowUserActivity) { +}: DeleteFollowUserActivity) => { return db .delete(schema.userActivities) .where( @@ -214,8 +230,18 @@ export async function deleteFollowUserActivity({ ) } -export async function deleteUserActivityById(activityId: string) { +export const deleteFollowUserActivity = withDbTracing( + 'delete-follow-user-activity', + deleteFollowUserActivityImpl +) + +const deleteUserActivityByIdImpl = async (activityId: string) => { return db .delete(schema.userActivities) .where(eq(schema.userActivities.id, activityId)) } + +export const deleteUserActivityById = withDbTracing( + 'delete-user-activity-by-id', + deleteUserActivityByIdImpl +) diff --git a/apps/backend/src/db/repositories/user-episode.ts b/apps/backend/src/db/repositories/user-episode.ts index e8b22b0c..f30ab528 100644 --- a/apps/backend/src/db/repositories/user-episode.ts +++ b/apps/backend/src/db/repositories/user-episode.ts @@ -1,10 +1,11 @@ import { and, count, desc, eq, inArray } from 'drizzle-orm' import type { InsertUserEpisode } from '@/domain/entities/user-episode' import type { GetUserEpisodesInput } from '@/domain/services/user-episodes/get-user-episodes' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function insertUserEpisodes(values: InsertUserEpisode[]) { +const insertUserEpisodesImpl = async (values: InsertUserEpisode[]) => { return db .insert(schema.userEpisodes) .values(values) @@ -12,10 +13,15 @@ export async function insertUserEpisodes(values: InsertUserEpisode[]) { .onConflictDoNothing() } -export async function selectUserEpisodes({ +export const insertUserEpisodes = withDbTracing( + 'insert-user-episodes', + insertUserEpisodesImpl +) + +const selectUserEpisodesImpl = async ({ userId, tmdbId, -}: GetUserEpisodesInput) { +}: GetUserEpisodesInput) => { return db .select() .from(schema.userEpisodes) @@ -28,13 +34,23 @@ export async function selectUserEpisodes({ .orderBy(schema.userEpisodes.episodeNumber) } -export async function deleteUserEpisodes(ids: string[]) { +export const selectUserEpisodes = withDbTracing( + 'select-user-episodes', + selectUserEpisodesImpl +) + +const deleteUserEpisodesImpl = async (ids: string[]) => { return db .delete(schema.userEpisodes) .where(inArray(schema.userEpisodes.id, ids)) } -export async function selectMostWatched(userId: string) { +export const deleteUserEpisodes = withDbTracing( + 'delete-user-episodes', + deleteUserEpisodesImpl +) + +const selectMostWatchedImpl = async (userId: string) => { return db .select({ count: count(), tmdbId: schema.userEpisodes.tmdbId }) .from(schema.userEpisodes) @@ -43,3 +59,8 @@ export async function selectMostWatched(userId: string) { .orderBy(desc(count())) .limit(3) } + +export const selectMostWatched = withDbTracing( + 'select-most-watched', + selectMostWatchedImpl +) diff --git a/apps/backend/src/db/repositories/user-item-repository.ts b/apps/backend/src/db/repositories/user-item-repository.ts index 67a397a1..734949f4 100644 --- a/apps/backend/src/db/repositories/user-item-repository.ts +++ b/apps/backend/src/db/repositories/user-item-repository.ts @@ -17,15 +17,16 @@ import type { SelectUserItems, } from '@/domain/entities/user-item' import type { GetUserItemInput } from '@/domain/services/user-items/get-user-item' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function upsertUserItem({ +const upsertUserItemImpl = async ({ mediaType, tmdbId, userId, status, -}: InsertUserItem) { +}: InsertUserItem) => { return db.execute( sql` INSERT INTO ${schema.userItems} (media_type, tmdb_id, user_id, status) @@ -39,7 +40,9 @@ export async function upsertUserItem({ ) } -export async function selectUserItems({ +export const upsertUserItem = withDbTracing('upsert-user-item', upsertUserItemImpl) + +const selectUserItemsImpl = async ({ userId, status, pageSize, @@ -49,7 +52,7 @@ export async function selectUserItems({ mediaType, rating, onlyItemsWithoutReview, -}: SelectUserItems) { +}: SelectUserItems) => { const whereConditions = [ eq(schema.userItems.userId, userId), inArray(schema.userItems.mediaType, mediaType), @@ -112,18 +115,28 @@ export async function selectUserItems({ .limit(pageSize + 1) } -export async function deleteUserItem(id: string) { +export const selectUserItems = withDbTracing( + 'select-user-items', + selectUserItemsImpl +) + +const deleteUserItemImpl = async (id: string) => { return db .delete(schema.userItems) .where(eq(schema.userItems.id, id)) .returning() } -export async function selectUserItem({ +export const deleteUserItem = withDbTracing( + 'delete-user-item', + deleteUserItemImpl +) + +const selectUserItemImpl = async ({ userId, mediaType, tmdbId, -}: GetUserItemInput) { +}: GetUserItemInput) => { return db .select() .from(schema.userItems) @@ -137,7 +150,12 @@ export async function selectUserItem({ .limit(1) } -export async function selectUserItemStatus(userId: string) { +export const selectUserItem = withDbTracing( + 'select-user-item', + selectUserItemImpl +) + +const selectUserItemStatusImpl = async (userId: string) => { return db .select({ status: schema.userItems.status, @@ -149,10 +167,15 @@ export async function selectUserItemStatus(userId: string) { .groupBy(schema.userItems.status) } -export async function selectAllUserItemsByStatus({ +export const selectUserItemStatus = withDbTracing( + 'select-user-item-status', + selectUserItemStatusImpl +) + +const selectAllUserItemsByStatusImpl = async ({ status, userId, -}: SelectAllUserItems) { +}: SelectAllUserItems) => { const { id, tmdbId, mediaType, position, updatedAt } = getTableColumns(schema.userItems) const whereConditions = [eq(schema.userItems.userId, userId)] @@ -173,11 +196,16 @@ export async function selectAllUserItemsByStatus({ .orderBy(asc(schema.userItems.position), desc(schema.userItems.updatedAt)) } -export async function reorderUserItems( +export const selectAllUserItemsByStatus = withDbTracing( + 'select-all-user-items-by-status', + selectAllUserItemsByStatusImpl +) + +const reorderUserItemsImpl = async ( userId: string, _status: string, orderedIds: string[] -) { +) => { // Update position for each item based on array order const updates = orderedIds.map((id, index) => db @@ -191,7 +219,12 @@ export async function reorderUserItems( await Promise.all(updates) } -export async function selectAllUserItems(userId: string) { +export const reorderUserItems = withDbTracing( + 'reorder-user-items', + reorderUserItemsImpl +) + +const selectAllUserItemsImpl = async (userId: string) => { return db .select({ id: schema.userItems.id, @@ -202,7 +235,12 @@ export async function selectAllUserItems(userId: string) { .where(eq(schema.userItems.userId, userId)) } -export async function selectUserItemsCount(userId: string) { +export const selectAllUserItems = withDbTracing( + 'select-all-user-items', + selectAllUserItemsImpl +) + +const selectUserItemsCountImpl = async (userId: string) => { const result = await db .select({ count: sql`COUNT(*)::int`, @@ -213,6 +251,11 @@ export async function selectUserItemsCount(userId: string) { return result[0]?.count ?? 0 } +export const selectUserItemsCount = withDbTracing( + 'select-user-items-count', + selectUserItemsCountImpl +) + function getOrderColumn(orderBy: string) { switch (orderBy) { case 'updatedAt': diff --git a/apps/backend/src/db/repositories/user-preferences.ts b/apps/backend/src/db/repositories/user-preferences.ts index a12a10bd..a16f6577 100644 --- a/apps/backend/src/db/repositories/user-preferences.ts +++ b/apps/backend/src/db/repositories/user-preferences.ts @@ -1,11 +1,12 @@ import { eq } from 'drizzle-orm' import { db } from '@/db' import type { UpdateUserPreferencesParams } from '@/domain/entities/user-preferences' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { userPreferences } from '../schema' -export async function updateUserPreferences( +const updateUserPreferencesImpl = async ( params: UpdateUserPreferencesParams -) { +) => { return await db .insert(userPreferences) .values(params) @@ -27,9 +28,19 @@ export async function updateUserPreferences( .returning() } -export async function selectUserPreferences(userId: string) { +export const updateUserPreferences = withDbTracing( + 'update-user-preferences', + updateUserPreferencesImpl +) + +const selectUserPreferencesImpl = async (userId: string) => { return await db .select() .from(userPreferences) .where(eq(userPreferences.userId, userId)) } + +export const selectUserPreferences = withDbTracing( + 'select-user-preferences', + selectUserPreferencesImpl +) diff --git a/apps/backend/src/db/repositories/user-repository.ts b/apps/backend/src/db/repositories/user-repository.ts index 7b449fac..a2b5eba0 100644 --- a/apps/backend/src/db/repositories/user-repository.ts +++ b/apps/backend/src/db/repositories/user-repository.ts @@ -3,15 +3,21 @@ import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertUserModel } from '@/domain/entities/user' import type { UpdateUserInput } from '@/domain/services/users/update-user' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -export async function getUserByEmail(email: string) { +const getUserByEmailImpl = async (email: string) => { return db .select() .from(schema.users) .where(sql`LOWER(${schema.users.email}) = LOWER(${email})`) } -export async function getUserById(id: string) { +export const getUserByEmail = withDbTracing( + 'get-user-by-email', + getUserByEmailImpl +) + +const getUserByIdImpl = async (id: string) => { return db .select({ id: schema.users.id, @@ -33,7 +39,9 @@ export async function getUserById(id: string) { .where(eq(schema.users.id, id)) } -export async function getUserByUsername(username: string) { +export const getUserById = withDbTracing('get-user-by-id', getUserByIdImpl) + +const getUserByUsernameImpl = async (username: string) => { return db .select({ id: schema.users.id, @@ -55,12 +63,17 @@ export async function getUserByUsername(username: string) { ) } -export async function insertUser({ +export const getUserByUsername = withDbTracing( + 'get-user-by-username', + getUserByUsernameImpl +) + +const insertUserImpl = async ({ email, password, username, displayName, -}: InsertUserModel) { +}: InsertUserModel) => { return db .insert(schema.users) .values({ @@ -72,7 +85,9 @@ export async function insertUser({ .returning() } -export async function updateUser(userId: string, data: UpdateUserInput) { +export const insertUser = withDbTracing('insert-user', insertUserImpl) + +const updateUserImpl = async (userId: string, data: UpdateUserInput) => { return db .update(schema.users) .set(data) @@ -80,7 +95,9 @@ export async function updateUser(userId: string, data: UpdateUserInput) { .returning() } -export async function updateUserPassword(userId: string, password: string) { +export const updateUser = withDbTracing('update-user', updateUserImpl) + +const updateUserPasswordImpl = async (userId: string, password: string) => { return db .update(schema.users) .set({ @@ -90,7 +107,12 @@ export async function updateUserPassword(userId: string, password: string) { .where(eq(schema.users.id, userId)) } -export async function getProUsersDetails() { +export const updateUserPassword = withDbTracing( + 'update-user-password', + updateUserPasswordImpl +) + +const getProUsersDetailsImpl = async () => { return db .select({ id: schema.users.id, @@ -123,7 +145,12 @@ export async function getProUsersDetails() { .groupBy(schema.users.id, schema.userPreferences.id) } -export async function listUsersByUsernameLike(username: string) { +export const getProUsersDetails = withDbTracing( + 'get-pro-users-details', + getProUsersDetailsImpl +) + +const listUsersByUsernameLikeImpl = async (username: string) => { return db .select({ id: schema.users.id, @@ -146,3 +173,8 @@ export async function listUsersByUsernameLike(username: string) { ) .limit(10) } + +export const listUsersByUsernameLike = withDbTracing( + 'list-users-by-username-like', + listUsersByUsernameLikeImpl +) diff --git a/apps/backend/src/db/repositories/user-stats.ts b/apps/backend/src/db/repositories/user-stats.ts index 8b785c89..48c6bddc 100644 --- a/apps/backend/src/db/repositories/user-stats.ts +++ b/apps/backend/src/db/repositories/user-stats.ts @@ -1,8 +1,9 @@ import { and, eq, sql } from 'drizzle-orm' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -export async function selectUserStats(userId: string) { +const selectUserStatsImpl = async (userId: string) => { // Run all independent queries in parallel instead of sequential transaction // This improves performance as these are all read-only operations const [ @@ -51,3 +52,8 @@ export async function selectUserStats(userId: string) { watchedSeriesCount, } } + +export const selectUserStats = withDbTracing( + 'select-user-stats', + selectUserStatsImpl +) diff --git a/apps/backend/src/db/repositories/user-watch-entries-repository.ts b/apps/backend/src/db/repositories/user-watch-entries-repository.ts index 741df0a7..87528c75 100644 --- a/apps/backend/src/db/repositories/user-watch-entries-repository.ts +++ b/apps/backend/src/db/repositories/user-watch-entries-repository.ts @@ -1,11 +1,12 @@ import { eq } from 'drizzle-orm' +import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { userWatchEntries } from '../schema' -export async function createWatchEntry(data: { +const createWatchEntryImpl = async (data: { userItemId: string watchedAt?: Date -}) { +}) => { const [entry] = await db .insert(userWatchEntries) .values({ @@ -17,7 +18,12 @@ export async function createWatchEntry(data: { return entry } -export async function getWatchEntriesByUserItemId(userItemId: string) { +export const createWatchEntry = withDbTracing( + 'create-watch-entry', + createWatchEntryImpl +) + +const getWatchEntriesByUserItemIdImpl = async (userItemId: string) => { return db .select() .from(userWatchEntries) @@ -25,7 +31,12 @@ export async function getWatchEntriesByUserItemId(userItemId: string) { .orderBy(userWatchEntries.watchedAt) } -export async function updateWatchEntry(id: string, watchedAt: Date) { +export const getWatchEntriesByUserItemId = withDbTracing( + 'get-watch-entries-by-user-item-id', + getWatchEntriesByUserItemIdImpl +) + +const updateWatchEntryImpl = async (id: string, watchedAt: Date) => { const [entry] = await db .update(userWatchEntries) .set({ watchedAt }) @@ -35,7 +46,12 @@ export async function updateWatchEntry(id: string, watchedAt: Date) { return entry } -export async function deleteWatchEntry(id: string) { +export const updateWatchEntry = withDbTracing( + 'update-watch-entry', + updateWatchEntryImpl +) + +const deleteWatchEntryImpl = async (id: string) => { const [entry] = await db .delete(userWatchEntries) .where(eq(userWatchEntries.id, id)) @@ -44,8 +60,18 @@ export async function deleteWatchEntry(id: string) { return entry } -export async function deleteWatchEntriesByUserItemId(userItemId: string) { +export const deleteWatchEntry = withDbTracing( + 'delete-watch-entry', + deleteWatchEntryImpl +) + +const deleteWatchEntriesByUserItemIdImpl = async (userItemId: string) => { await db .delete(userWatchEntries) .where(eq(userWatchEntries.userItemId, userItemId)) } + +export const deleteWatchEntriesByUserItemId = withDbTracing( + 'delete-watch-entries-by-user-item-id', + deleteWatchEntriesByUserItemIdImpl +) diff --git a/apps/backend/src/infra/telemetry/with-adapter-tracing.ts b/apps/backend/src/infra/telemetry/with-adapter-tracing.ts new file mode 100644 index 00000000..2f403893 --- /dev/null +++ b/apps/backend/src/infra/telemetry/with-adapter-tracing.ts @@ -0,0 +1,26 @@ +import { trace } from '@opentelemetry/api' + +// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any adapter signature +export function withAdapterTracing any>( + spanName: string, + fn: T +): T { + const tracer = trace.getTracer('plotwist-api', '0.1.0') + const fullSpanName = spanName.endsWith('-adapter') + ? spanName + : `${spanName}-adapter` + + return (async (...args: Parameters) => { + return tracer.startActiveSpan(fullSpanName, async span => { + try { + const result = await fn(...args) + return result + } catch (err) { + span.recordException(err as Error) + throw err + } finally { + span.end() + } + }) + }) as T +} diff --git a/apps/backend/src/infra/telemetry/with-db-tracing.ts b/apps/backend/src/infra/telemetry/with-db-tracing.ts new file mode 100644 index 00000000..83b94eaa --- /dev/null +++ b/apps/backend/src/infra/telemetry/with-db-tracing.ts @@ -0,0 +1,26 @@ +import { trace } from '@opentelemetry/api' + +// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any repository signature +export function withDbTracing any>( + spanName: string, + fn: T +): T { + const tracer = trace.getTracer('plotwist-api', '0.1.0') + const fullSpanName = spanName.endsWith('-repository') + ? spanName + : `${spanName}-repository` + + return (async (...args: Parameters) => { + return tracer.startActiveSpan(fullSpanName, async span => { + try { + const result = await fn(...args) + return result + } catch (err) { + span.recordException(err as Error) + throw err + } finally { + span.end() + } + }) + }) as T +} From ae9840c72ce6fd6e9db744b3040436e4cab7d68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 17 Feb 2026 23:15:38 -0300 Subject: [PATCH 07/26] feat: add HTTP request metrics and telemetry dashboard --- apps/backend/src/http/routes/healthcheck.ts | 6 +- apps/backend/src/http/server.ts | 3 + apps/backend/src/infra/telemetry/dash.json | 558 ++++++++++++++++++ .../infra/telemetry/http-request-metrics.ts | 26 + .../src/infra/telemetry/with-tracing.ts | 9 +- 5 files changed, 597 insertions(+), 5 deletions(-) create mode 100644 apps/backend/src/infra/telemetry/dash.json create mode 100644 apps/backend/src/infra/telemetry/http-request-metrics.ts diff --git a/apps/backend/src/http/routes/healthcheck.ts b/apps/backend/src/http/routes/healthcheck.ts index 69aa52bc..752bb74b 100644 --- a/apps/backend/src/http/routes/healthcheck.ts +++ b/apps/backend/src/http/routes/healthcheck.ts @@ -1,13 +1,11 @@ import type { FastifyInstance } from 'fastify' -import { withTracing } from '@/infra/telemetry/with-tracing' - export const healthCheck = (app: FastifyInstance) => app.route({ method: 'GET', url: '/health', config: { rateLimit: false }, - handler: withTracing('healthcheck', (_request, reply) => + handler: (_request, reply) => { reply.send({ alive: true }) - ), + }, }) diff --git a/apps/backend/src/http/server.ts b/apps/backend/src/http/server.ts index a81130a6..de3f5fde 100644 --- a/apps/backend/src/http/server.ts +++ b/apps/backend/src/http/server.ts @@ -8,6 +8,7 @@ import { import { ZodError } from 'zod' import { logger } from '@/adapters/logger' import { DomainError } from '@/domain/errors/domain-error' +import { registerHttpRequestMetrics } from '@/infra/telemetry/http-request-metrics' import { config } from '../config' import { routes } from './routes' import { transformSwaggerSchema } from './transform-schema' @@ -85,6 +86,8 @@ export function startServer() { return reply.status(500).send({ message: 'Internal server error.' }) }) + registerHttpRequestMetrics(app) + // TODO: Uncomment this when we have a client guard // registerClientGuard(app) routes(app) diff --git a/apps/backend/src/infra/telemetry/dash.json b/apps/backend/src/infra/telemetry/dash.json new file mode 100644 index 00000000..a98a42a3 --- /dev/null +++ b/apps/backend/src/infra/telemetry/dash.json @@ -0,0 +1,558 @@ +{ + "apiVersion": "dashboard.grafana.app/v2beta1", + "kind": "Dashboard", + "metadata": { + "name": "g6wwd8", + "namespace": "default", + "uid": "FgXXFvgSHts7LvpkvXUedqZPmfs50wJrIbU86KpQGa4X", + "resourceVersion": "1", + "generation": 9, + "creationTimestamp": "2026-02-18T01:36:07Z", + "labels": { + "grafana.app/deprecatedInternalID": "4" + }, + "annotations": { + "grafana.app/createdBy": "provisioning:", + "grafana.app/updatedBy": "provisioning:", + "grafana.app/updatedTimestamp": "2026-02-18T01:36:07Z", + "grafana.app/saved-from-ui": "Grafana v12.3.1 (0d1a5b4420)" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "query": { + "datasource": { + "name": "-- Grafana --" + }, + "group": "grafana", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "tempo" + }, + "group": "tempo", + "kind": "DataQuery", + "spec": { + "filters": [ + { + "id": "29f2879c", + "operator": "=", + "scope": "span" + }, + { + "id": "service-name", + "isCustomValue": false, + "operator": "=", + "scope": "resource", + "tag": "service.name", + "value": ["plotwist-api"], + "valueType": "string" + }, + { + "id": "status", + "isCustomValue": false, + "operator": "=", + "scope": "intrinsic", + "tag": "status", + "value": "error", + "valueType": "keyword" + } + ], + "limit": 20, + "metricsQueryType": "range", + "queryType": "traceqlSearch", + "serviceMapUseNativeHistograms": false, + "tableType": "traces" + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "All failed requests", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.3.1" + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "sum(rate(http_server_requests_total[5m]))", + "instant": false, + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "All Requests", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.3.1" + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "avg_over_time(sum(rate(http_server_requests_total[5m]))[$__range:5m]) * 1000", + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "Average requests time", + "vizConfig": { + "group": "gauge", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 3, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "#EAB839", + "value": 200 + }, + { + "color": "red", + "value": 400 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + } + }, + "version": "12.3.1" + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "sum(rate(http_server_requests_total[5m]))", + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "Requests rate per second", + "vizConfig": { + "group": "stat", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "hits/s" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + "version": "12.3.1" + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 8 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 8 + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "fiscalYearStartMonth": 0, + "from": "now-3h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "Plotwist", + "variables": [] + }, + "status": {} +} diff --git a/apps/backend/src/infra/telemetry/http-request-metrics.ts b/apps/backend/src/infra/telemetry/http-request-metrics.ts new file mode 100644 index 00000000..fe24745a --- /dev/null +++ b/apps/backend/src/infra/telemetry/http-request-metrics.ts @@ -0,0 +1,26 @@ +import { metrics } from '@opentelemetry/api' +import type { FastifyInstance } from 'fastify' + +function getStatusClass(statusCode: number): 'ok' | 'error' { + return statusCode >= 200 && statusCode < 400 ? 'ok' : 'error' +} + +export function registerHttpRequestMetrics(app: FastifyInstance) { + const meter = metrics.getMeter('plotwist-api', '0.1.0') + const requestCounter = meter.createCounter('http.server.requests', { + description: 'HTTP server request count by status', + unit: '1', + }) + + app.addHook('onResponse', (request, reply, done) => { + const statusCode = reply.statusCode + const statusClass = getStatusClass(statusCode) + requestCounter.add(1, { + 'http.status_code': statusCode, + 'http.response.status': statusClass, + 'http.method': request.method, + 'http.route': request.routeOptions?.url ?? request.url, + }) + done() + }) +} diff --git a/apps/backend/src/infra/telemetry/with-tracing.ts b/apps/backend/src/infra/telemetry/with-tracing.ts index 55e88254..9ce2c167 100644 --- a/apps/backend/src/infra/telemetry/with-tracing.ts +++ b/apps/backend/src/infra/telemetry/with-tracing.ts @@ -1,4 +1,4 @@ -import { trace } from '@opentelemetry/api' +import { SpanStatusCode, trace } from '@opentelemetry/api' import type { FastifyRequest } from 'fastify' // biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any handler signature @@ -25,11 +25,18 @@ export function withTracing any>( } const result = await handler(...args) + span.setStatus({ code: SpanStatusCode.OK }) span.setAttribute('http.status_code', 200) + span.setAttribute('http.response.status', 'ok') return result } catch (err) { span.recordException(err as Error) + span.setStatus({ + code: SpanStatusCode.ERROR, + message: (err as Error).message, + }) span.setAttribute('http.status_code', 500) + span.setAttribute('http.response.status', 'error') throw err } finally { span.end() From 5b14ce22925dfb9bdbd5911046677d5b6adbdf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Wed, 18 Feb 2026 23:14:46 -0300 Subject: [PATCH 08/26] feat: integrate OpenTelemetry for enhanced observability in backend services --- apps/backend/package.json | 2 + .../db/repositories/feedback-repository.ts | 5 +- .../db/repositories/followers-repository.ts | 28 ++---- .../src/db/repositories/likes-repository.ts | 13 +-- .../db/repositories/list-item-repository.ts | 35 ++----- .../src/db/repositories/list-repository.ts | 34 ++----- .../src/db/repositories/login-repository.ts | 8 +- .../src/db/repositories/magic-tokens.ts | 22 +---- .../repositories/social-links-repository.ts | 26 +---- .../src/db/repositories/user-repository.ts | 51 ++-------- apps/backend/src/http/routes/feedback.ts | 4 +- apps/backend/src/http/routes/follow.ts | 10 +- apps/backend/src/http/routes/images.ts | 4 +- apps/backend/src/http/routes/import.ts | 6 +- apps/backend/src/http/routes/likes.ts | 8 +- apps/backend/src/http/routes/list-item.ts | 12 +-- apps/backend/src/http/routes/lists.ts | 16 ++- apps/backend/src/http/routes/login.ts | 6 +- .../backend/src/http/routes/review-replies.ts | 10 +- apps/backend/src/http/routes/reviews.ts | 17 ++-- apps/backend/src/http/routes/social-auth.ts | 10 +- apps/backend/src/http/routes/social-links.ts | 6 +- apps/backend/src/http/routes/subscriptions.ts | 4 +- apps/backend/src/http/routes/tmdb-proxy.ts | 3 +- .../src/http/routes/user-activities.ts | 14 +-- apps/backend/src/http/routes/user-episodes.ts | 8 +- apps/backend/src/http/routes/user-items.ts | 25 ++--- apps/backend/src/http/routes/user-stats.ts | 41 +++----- apps/backend/src/http/routes/users.ts | 24 +++-- apps/backend/src/http/routes/watch-entries.ts | 10 +- apps/backend/src/http/routes/webhook.ts | 4 +- apps/backend/src/http/server.ts | 18 ++-- apps/backend/src/infra/telemetry/otel.ts | 7 ++ apps/backend/src/main.ts | 2 +- pnpm-lock.yaml | 99 +++++++++++++++++++ 35 files changed, 253 insertions(+), 339 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 831a0363..f9182ed8 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -32,6 +32,7 @@ "@fastify/cors": "^11.2.0", "@fastify/jwt": "^10.0.0", "@fastify/multipart": "^9.3.0", + "@fastify/otel": "^0.16.0", "@fastify/rate-limit": "^10.3.0", "@fastify/redis": "^7.1.0", "@fastify/swagger": "^9.6.1", @@ -39,6 +40,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.211.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.211.0", + "@opentelemetry/instrumentation-http": "^0.212.0", "@opentelemetry/resources": "^2.5.0", "@opentelemetry/sdk-metrics": "^2.5.0", "@opentelemetry/sdk-node": "^0.211.0", diff --git a/apps/backend/src/db/repositories/feedback-repository.ts b/apps/backend/src/db/repositories/feedback-repository.ts index 4be1d01f..fbe72f74 100644 --- a/apps/backend/src/db/repositories/feedback-repository.ts +++ b/apps/backend/src/db/repositories/feedback-repository.ts @@ -1,10 +1,7 @@ import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertFeedbackModel } from '@/domain/entities/feedback' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -const insertFeedbackImpl = async (params: InsertFeedbackModel) => { +export async function insertFeedback(params: InsertFeedbackModel) { return db.insert(schema.feedbacks).values(params).returning() } - -export const insertFeedback = withDbTracing('insert-feedback', insertFeedbackImpl) diff --git a/apps/backend/src/db/repositories/followers-repository.ts b/apps/backend/src/db/repositories/followers-repository.ts index bfc76531..e2d6e8c9 100644 --- a/apps/backend/src/db/repositories/followers-repository.ts +++ b/apps/backend/src/db/repositories/followers-repository.ts @@ -3,14 +3,13 @@ import type { CreateFollowServiceInput } from '@/domain/services/follows/create- import type { DeleteFollowServiceInput } from '@/domain/services/follows/delete-follow' import type { GetFollowServiceInput } from '@/domain/services/follows/get-follow' import type { GetFollowersInput } from '@/domain/services/follows/get-followers' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const insertFollowImpl = async ({ +export async function insertFollow({ followedId, followerId, -}: CreateFollowServiceInput) => { +}: CreateFollowServiceInput) { return db .insert(schema.followers) .values({ @@ -20,12 +19,10 @@ const insertFollowImpl = async ({ .returning() } -export const insertFollow = withDbTracing('insert-follow', insertFollowImpl) - -const getFollowImpl = async ({ +export async function getFollow({ followedId, followerId, -}: GetFollowServiceInput) => { +}: GetFollowServiceInput) { return db .select() .from(schema.followers) @@ -37,12 +34,10 @@ const getFollowImpl = async ({ ) } -export const getFollow = withDbTracing('get-follow', getFollowImpl) - -const deleteFollowImpl = async ({ +export async function deleteFollow({ followedId, followerId, -}: DeleteFollowServiceInput) => { +}: DeleteFollowServiceInput) { return db .delete(schema.followers) .where( @@ -54,14 +49,12 @@ const deleteFollowImpl = async ({ .returning() } -export const deleteFollow = withDbTracing('delete-follow', deleteFollowImpl) - -const selectFollowersImpl = async ({ +export async function selectFollowers({ followedId, followerId, cursor, pageSize, -}: GetFollowersInput) => { +}: GetFollowersInput) { return db .select({ ...getTableColumns(schema.followers), @@ -96,8 +89,3 @@ const selectFollowersImpl = async ({ ) .limit(pageSize + 1) } - -export const selectFollowers = withDbTracing( - 'select-followers', - selectFollowersImpl -) diff --git a/apps/backend/src/db/repositories/likes-repository.ts b/apps/backend/src/db/repositories/likes-repository.ts index 8a26fcbf..eaea4bc1 100644 --- a/apps/backend/src/db/repositories/likes-repository.ts +++ b/apps/backend/src/db/repositories/likes-repository.ts @@ -1,22 +1,17 @@ import { eq, getTableColumns, sql } from 'drizzle-orm' import type { InsertLike } from '@/domain/entities/likes' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const insertLikeImpl = async (values: InsertLike) => { +export async function insertLike(values: InsertLike) { return db.insert(schema.likes).values(values).returning() } -export const insertLike = withDbTracing('insert-like', insertLikeImpl) - -const deleteLikeImpl = async (id: string) => { +export async function deleteLike(id: string) { return db.delete(schema.likes).where(eq(schema.likes.id, id)).returning() } -export const deleteLike = withDbTracing('delete-like', deleteLikeImpl) - -const selectLikesImpl = async (entityId: string) => { +export async function selectLikes(entityId: string) { return db .select({ ...getTableColumns(schema.likes), @@ -35,5 +30,3 @@ const selectLikesImpl = async (entityId: string) => { eq(schema.users.id, schema.subscriptions.userId) ) } - -export const selectLikes = withDbTracing('select-likes', selectLikesImpl) diff --git a/apps/backend/src/db/repositories/list-item-repository.ts b/apps/backend/src/db/repositories/list-item-repository.ts index 1043f215..d53c8e83 100644 --- a/apps/backend/src/db/repositories/list-item-repository.ts +++ b/apps/backend/src/db/repositories/list-item-repository.ts @@ -1,20 +1,14 @@ import { eq } from 'drizzle-orm' import type { InsertListItem } from '@/domain/entities/list-item' import type { UpdateListItemsServiceInput } from '@/domain/services/list-item/update-list-items' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const insertListItemImpl = async (input: InsertListItem) => { +export async function insertListItem(input: InsertListItem) { return db.insert(schema.listItems).values(input).returning() } -export const insertListItem = withDbTracing( - 'insert-list-item', - insertListItemImpl -) - -const selectListItemsImpl = async (listId: string) => { +export async function selectListItems(listId: string) { return db .select() .from(schema.listItems) @@ -22,32 +16,20 @@ const selectListItemsImpl = async (listId: string) => { .orderBy(schema.listItems.position) } -export const selectListItems = withDbTracing( - 'select-list-items', - selectListItemsImpl -) - -const deleteListItemImpl = async (id: string) => { +export async function deleteListItem(id: string) { return db .delete(schema.listItems) .where(eq(schema.listItems.id, id)) .returning() } -export const deleteListItem = withDbTracing( - 'delete-list-item', - deleteListItemImpl -) - -const getListItemImpl = async (id: string) => { +export async function getListItem(id: string) { return db.select().from(schema.listItems).where(eq(schema.listItems.id, id)) } -export const getListItem = withDbTracing('get-list-item', getListItemImpl) - -const updateListItemsImpl = async ({ +export async function updateListItems({ listItems, -}: UpdateListItemsServiceInput) => { +}: UpdateListItemsServiceInput) { return db.transaction(async tx => { const promises = listItems.map(({ id, position }) => tx @@ -60,8 +42,3 @@ const updateListItemsImpl = async ({ return await Promise.all(promises) }) } - -export const updateListItems = withDbTracing( - 'update-list-items', - updateListItemsImpl -) diff --git a/apps/backend/src/db/repositories/list-repository.ts b/apps/backend/src/db/repositories/list-repository.ts index 83915d7f..4e47e054 100644 --- a/apps/backend/src/db/repositories/list-repository.ts +++ b/apps/backend/src/db/repositories/list-repository.ts @@ -3,17 +3,16 @@ import type { InsertListModel } from '@/domain/entities/lists' import type { GetListsInput } from '@/domain/services/lists/get-lists' import type { UpdateListValues } from '@/domain/services/lists/update-list' import type { UpdateListBannerInput } from '@/domain/services/lists/update-list-banner' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const selectListsImpl = ({ +export function selectLists({ userId, limit = 5, authenticatedUserId, visibility, hasBanner, -}: GetListsInput) => { +}: GetListsInput) { return db .select({ ...getTableColumns(schema.lists), @@ -60,28 +59,22 @@ const selectListsImpl = ({ .limit(limit) } -export const selectLists = withDbTracing('select-lists', selectListsImpl) - -const insertListImpl = async (input: InsertListModel) => { +export async function insertList(input: InsertListModel) { return db .insert(schema.lists) .values({ ...input }) .returning() } -export const insertList = withDbTracing('insert-list', insertListImpl) - -const deleteListImpl = async (id: string) => { +export async function deleteList(id: string) { return db.delete(schema.lists).where(eq(schema.lists.id, id)).returning() } -export const deleteList = withDbTracing('delete-list', deleteListImpl) - -const updateListImpl = async ( +export async function updateList( id: string, userId: string, values: UpdateListValues -) => { +) { return db .update(schema.lists) .set(values) @@ -89,9 +82,7 @@ const updateListImpl = async ( .returning() } -export const updateList = withDbTracing('update-list', updateListImpl) - -const getListByIdImpl = async (id: string, authenticatedUserId?: string) => { +export async function getListById(id: string, authenticatedUserId?: string) { return db .select({ ...getTableColumns(schema.lists), @@ -118,21 +109,14 @@ const getListByIdImpl = async (id: string, authenticatedUserId?: string) => { .where(eq(schema.lists.id, id)) } -export const getListById = withDbTracing('get-list-by-id', getListByIdImpl) - -const updateListBannerImpl = async ({ +export async function updateListBanner({ listId, userId, bannerUrl, -}: UpdateListBannerInput) => { +}: UpdateListBannerInput) { return db .update(schema.lists) .set({ bannerUrl }) .where(and(eq(schema.lists.id, listId), eq(schema.lists.userId, userId))) .returning() } - -export const updateListBanner = withDbTracing( - 'update-list-banner', - updateListBannerImpl -) diff --git a/apps/backend/src/db/repositories/login-repository.ts b/apps/backend/src/db/repositories/login-repository.ts index 72e502e6..8530995c 100644 --- a/apps/backend/src/db/repositories/login-repository.ts +++ b/apps/backend/src/db/repositories/login-repository.ts @@ -1,9 +1,8 @@ import { or, sql } from 'drizzle-orm' import { schema } from '@/db/schema' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' -const findUserByEmailOrUsernameImpl = async (login?: string) => { +export async function findUserByEmailOrUsername(login?: string) { const [user] = await db .select() .from(schema.users) @@ -16,8 +15,3 @@ const findUserByEmailOrUsernameImpl = async (login?: string) => { return user } - -export const findUserByEmailOrUsername = withDbTracing( - 'find-user-by-email-or-username', - findUserByEmailOrUsernameImpl -) diff --git a/apps/backend/src/db/repositories/magic-tokens.ts b/apps/backend/src/db/repositories/magic-tokens.ts index 305d7888..100bbdd0 100644 --- a/apps/backend/src/db/repositories/magic-tokens.ts +++ b/apps/backend/src/db/repositories/magic-tokens.ts @@ -1,38 +1,22 @@ import { eq } from 'drizzle-orm' import type { InsertMagicTokenModel } from '@/domain/entities/magic-token' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const insertMagicTokenImpl = async (values: InsertMagicTokenModel) => { +export async function insertMagicToken(values: InsertMagicTokenModel) { return db.insert(schema.magicTokens).values(values) } -export const insertMagicToken = withDbTracing( - 'insert-magic-token', - insertMagicTokenImpl -) - -const selectMagicTokenImpl = async (token: string) => { +export async function selectMagicToken(token: string) { return db .select() .from(schema.magicTokens) .where(eq(schema.magicTokens.token, token)) } -export const selectMagicToken = withDbTracing( - 'select-magic-token', - selectMagicTokenImpl -) - -const invalidateMagicTokenImpl = async (token: string) => { +export async function invalidateMagicToken(token: string) { return db .update(schema.magicTokens) .set({ used: true }) .where(eq(schema.magicTokens.token, token)) } - -export const invalidateMagicToken = withDbTracing( - 'invalidate-magic-token', - invalidateMagicTokenImpl -) diff --git a/apps/backend/src/db/repositories/social-links-repository.ts b/apps/backend/src/db/repositories/social-links-repository.ts index 419d8ace..cf105fee 100644 --- a/apps/backend/src/db/repositories/social-links-repository.ts +++ b/apps/backend/src/db/repositories/social-links-repository.ts @@ -1,14 +1,13 @@ import { and, eq, sql } from 'drizzle-orm' import type { InsertSocialLink } from '@/domain/entities/social-link' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const insertSocialLinkImpl = async ({ +export async function insertSocialLink({ platform, url, userId, -}: InsertSocialLink) => { +}: InsertSocialLink) { return db.execute( sql` INSERT INTO ${schema.socialLinks} (user_id, platform, url) @@ -20,15 +19,10 @@ const insertSocialLinkImpl = async ({ ) } -export const insertSocialLink = withDbTracing( - 'insert-social-link', - insertSocialLinkImpl -) - -const deleteSocialLinkImpl = async ( +export async function deleteSocialLink( userId: string, platform: InsertSocialLink['platform'] -) => { +) { return db .delete(schema.socialLinks) .where( @@ -39,19 +33,9 @@ const deleteSocialLinkImpl = async ( ) } -export const deleteSocialLink = withDbTracing( - 'delete-social-link', - deleteSocialLinkImpl -) - -const selectSocialLinksImpl = async (userId: string) => { +export async function selectSocialLinks(userId: string) { return db .select() .from(schema.socialLinks) .where(eq(schema.socialLinks.userId, userId)) } - -export const selectSocialLinks = withDbTracing( - 'select-social-links', - selectSocialLinksImpl -) diff --git a/apps/backend/src/db/repositories/user-repository.ts b/apps/backend/src/db/repositories/user-repository.ts index a2b5eba0..0c53e0fb 100644 --- a/apps/backend/src/db/repositories/user-repository.ts +++ b/apps/backend/src/db/repositories/user-repository.ts @@ -3,21 +3,14 @@ import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertUserModel } from '@/domain/entities/user' import type { UpdateUserInput } from '@/domain/services/users/update-user' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' - -const getUserByEmailImpl = async (email: string) => { +export async function getUserByEmail(email: string) { return db .select() .from(schema.users) .where(sql`LOWER(${schema.users.email}) = LOWER(${email})`) } -export const getUserByEmail = withDbTracing( - 'get-user-by-email', - getUserByEmailImpl -) - -const getUserByIdImpl = async (id: string) => { +export async function getUserById(id: string) { return db .select({ id: schema.users.id, @@ -39,9 +32,7 @@ const getUserByIdImpl = async (id: string) => { .where(eq(schema.users.id, id)) } -export const getUserById = withDbTracing('get-user-by-id', getUserByIdImpl) - -const getUserByUsernameImpl = async (username: string) => { +export async function getUserByUsername(username: string) { return db .select({ id: schema.users.id, @@ -63,17 +54,12 @@ const getUserByUsernameImpl = async (username: string) => { ) } -export const getUserByUsername = withDbTracing( - 'get-user-by-username', - getUserByUsernameImpl -) - -const insertUserImpl = async ({ +export async function insertUser({ email, password, username, displayName, -}: InsertUserModel) => { +}: InsertUserModel) { return db .insert(schema.users) .values({ @@ -85,9 +71,7 @@ const insertUserImpl = async ({ .returning() } -export const insertUser = withDbTracing('insert-user', insertUserImpl) - -const updateUserImpl = async (userId: string, data: UpdateUserInput) => { +export async function updateUser(userId: string, data: UpdateUserInput) { return db .update(schema.users) .set(data) @@ -95,9 +79,7 @@ const updateUserImpl = async (userId: string, data: UpdateUserInput) => { .returning() } -export const updateUser = withDbTracing('update-user', updateUserImpl) - -const updateUserPasswordImpl = async (userId: string, password: string) => { +export async function updateUserPassword(userId: string, password: string) { return db .update(schema.users) .set({ @@ -107,12 +89,7 @@ const updateUserPasswordImpl = async (userId: string, password: string) => { .where(eq(schema.users.id, userId)) } -export const updateUserPassword = withDbTracing( - 'update-user-password', - updateUserPasswordImpl -) - -const getProUsersDetailsImpl = async () => { +export async function getProUsersDetails() { return db .select({ id: schema.users.id, @@ -145,12 +122,7 @@ const getProUsersDetailsImpl = async () => { .groupBy(schema.users.id, schema.userPreferences.id) } -export const getProUsersDetails = withDbTracing( - 'get-pro-users-details', - getProUsersDetailsImpl -) - -const listUsersByUsernameLikeImpl = async (username: string) => { +export async function listUsersByUsernameLike(username: string) { return db .select({ id: schema.users.id, @@ -173,8 +145,3 @@ const listUsersByUsernameLikeImpl = async (username: string) => { ) .limit(10) } - -export const listUsersByUsernameLike = withDbTracing( - 'list-users-by-username-like', - listUsersByUsernameLikeImpl -) diff --git a/apps/backend/src/http/routes/feedback.ts b/apps/backend/src/http/routes/feedback.ts index c8745325..8c1ca2eb 100644 --- a/apps/backend/src/http/routes/feedback.ts +++ b/apps/backend/src/http/routes/feedback.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createFeedbackController } from '../controllers/feedback-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { @@ -25,7 +23,7 @@ export async function feedbackRoutes(app: FastifyInstance) { response: createFeedbackResponseSchema, security: [{ bearerAuth: [] }], }, - handler: withTracing('create-feedback', createFeedbackController), + handler: createFeedbackController, }) ) } diff --git a/apps/backend/src/http/routes/follow.ts b/apps/backend/src/http/routes/follow.ts index 57a75c58..ded596cd 100644 --- a/apps/backend/src/http/routes/follow.ts +++ b/apps/backend/src/http/routes/follow.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createFollowController, deleteFollowController, @@ -37,7 +35,7 @@ export async function followsRoutes(app: FastifyInstance) { ], body: createFollowBodySchema, }, - handler: withTracing('create-follow', createFollowController), + handler: createFollowController, }) ) @@ -57,7 +55,7 @@ export async function followsRoutes(app: FastifyInstance) { querystring: getFollowQuerySchema, response: getFollowResponseSchema, }, - handler: withTracing('get-follow', getFollowController), + handler: getFollowController, }) ) @@ -76,7 +74,7 @@ export async function followsRoutes(app: FastifyInstance) { ], body: deleteFollowBodySchema, }, - handler: withTracing('delete-follow', deleteFollowController), + handler: deleteFollowController, }) ) @@ -91,7 +89,7 @@ export async function followsRoutes(app: FastifyInstance) { response: getFollowersResponseSchema, operationId: 'getFollowers', }, - handler: withTracing('get-followers', getFollowersController), + handler: getFollowersController, }) ) } diff --git a/apps/backend/src/http/routes/images.ts b/apps/backend/src/http/routes/images.ts index baea3343..4f54e244 100644 --- a/apps/backend/src/http/routes/images.ts +++ b/apps/backend/src/http/routes/images.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createImageController } from '../controllers/images-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { @@ -28,7 +26,7 @@ export async function imagesRoutes(app: FastifyInstance) { querystring: createImageQuerySchema, consumes: ['multipart/form-data'], }, - handler: withTracing('create-image', createImageController), + handler: createImageController, }) ) } diff --git a/apps/backend/src/http/routes/import.ts b/apps/backend/src/http/routes/import.ts index 37984793..c9bcf1ac 100644 --- a/apps/backend/src/http/routes/import.ts +++ b/apps/backend/src/http/routes/import.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createImportController, getDetailedImportController, @@ -33,7 +31,7 @@ export async function importRoutes(app: FastifyInstance) { querystring: createImportRequestSchema, consumes: ['multipart/form-data'], }, - handler: withTracing('create-import', createImportController), + handler: createImportController, }) ) @@ -53,7 +51,7 @@ export async function importRoutes(app: FastifyInstance) { params: getDetailedImportRequestSchema, response: getDetailedImportResponseSchema, }, - handler: withTracing('get-detailed-import', getDetailedImportController), + handler: getDetailedImportController, }) ) } diff --git a/apps/backend/src/http/routes/likes.ts b/apps/backend/src/http/routes/likes.ts index d1a4e03d..4dcbdb40 100644 --- a/apps/backend/src/http/routes/likes.ts +++ b/apps/backend/src/http/routes/likes.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createLikeController, deleteLikeController, @@ -34,7 +32,7 @@ export async function likesRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('create-like', createLikeController), + handler: createLikeController, }) ) @@ -53,7 +51,7 @@ export async function likesRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('delete-like', deleteLikeController), + handler: deleteLikeController, }) ) @@ -67,7 +65,7 @@ export async function likesRoutes(app: FastifyInstance) { params: getLikesParamsSchema, response: getLikesResponseSchema, }, - handler: withTracing('get-likes', getLikesController), + handler: getLikesController, }) ) } diff --git a/apps/backend/src/http/routes/list-item.ts b/apps/backend/src/http/routes/list-item.ts index 14053c89..1f366500 100644 --- a/apps/backend/src/http/routes/list-item.ts +++ b/apps/backend/src/http/routes/list-item.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createListItemController, deleteListItemController, @@ -39,7 +37,7 @@ export async function listItemRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('create-list-item', createListItemController), + handler: createListItemController, }) ) @@ -54,9 +52,7 @@ export async function listItemRoute(app: FastifyInstance) { querystring: languageQuerySchema, response: getListItemsResponseSchema, }, - handler: withTracing('get-list-items', (req, reply) => - getListItemsController(req, reply, app.redis) - ), + handler: (req, reply) => getListItemsController(req, reply, app.redis), }) ) @@ -75,7 +71,7 @@ export async function listItemRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('delete-list-item', deleteListItemController), + handler: deleteListItemController, }) ) @@ -95,7 +91,7 @@ export async function listItemRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('update-list-item', updateListItemController), + handler: updateListItemController, }) ) } diff --git a/apps/backend/src/http/routes/lists.ts b/apps/backend/src/http/routes/lists.ts index 71fa1cae..37c71e8f 100644 --- a/apps/backend/src/http/routes/lists.ts +++ b/apps/backend/src/http/routes/lists.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createListController, deleteListController, @@ -48,7 +46,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('create-list', createListController), + handler: createListController, }) ) @@ -68,7 +66,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('get-lists', getListsController), + handler: getListsController, }) ) @@ -88,7 +86,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('delete-list', deleteListController), + handler: deleteListController, }) ) @@ -109,7 +107,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('update-list', updateListController), + handler: updateListController, }) ) @@ -129,7 +127,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('get-list', getListController), + handler: getListController, }) ) @@ -149,7 +147,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('update-list-banner', updateListBannerController), + handler: updateListBannerController, }) ) @@ -170,7 +168,7 @@ export async function listsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('get-list-progress', getListProgressController), + handler: getListProgressController, }) ) } diff --git a/apps/backend/src/http/routes/login.ts b/apps/backend/src/http/routes/login.ts index 25cd19f9..1ba77f02 100644 --- a/apps/backend/src/http/routes/login.ts +++ b/apps/backend/src/http/routes/login.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { loginController } from '../controllers/login-controller' import { loginBodySchema, loginResponseSchema } from '../schemas/login' @@ -16,8 +14,6 @@ export async function loginRoute(app: FastifyInstance) { body: loginBodySchema, response: loginResponseSchema, }, - handler: withTracing('login', (request, reply) => - loginController(request, reply, app) - ), + handler: (request, reply) => loginController(request, reply, app), }) } diff --git a/apps/backend/src/http/routes/review-replies.ts b/apps/backend/src/http/routes/review-replies.ts index 68b23aa3..d65e998c 100644 --- a/apps/backend/src/http/routes/review-replies.ts +++ b/apps/backend/src/http/routes/review-replies.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createReviewReplyController, deleteReviewReplyController, @@ -40,7 +38,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('create-review-reply', createReviewReplyController), + handler: createReviewReplyController, }) ) @@ -60,7 +58,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('get-review-replies', getReviewRepliesController), + handler: getReviewRepliesController, }) ) @@ -79,7 +77,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('delete-review-reply', deleteReviewReplyController), + handler: deleteReviewReplyController, }) ) @@ -100,7 +98,7 @@ export async function reviewRepliesRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('update-review-reply', updateReviewReplyController), + handler: updateReviewReplyController, }) ) } diff --git a/apps/backend/src/http/routes/reviews.ts b/apps/backend/src/http/routes/reviews.ts index 5479723d..f839b135 100644 --- a/apps/backend/src/http/routes/reviews.ts +++ b/apps/backend/src/http/routes/reviews.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createReviewController, deleteReviewController, @@ -45,7 +43,7 @@ export async function reviewsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('create-review', createReviewController), + handler: createReviewController, }) ) @@ -65,7 +63,7 @@ export async function reviewsRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('get-reviews', getReviewsController), + handler: getReviewsController, }) ) @@ -78,7 +76,7 @@ export async function reviewsRoute(app: FastifyInstance) { tags: [reviewsTag], params: reviewParamsSchema, }, - handler: withTracing('delete-review', deleteReviewController), + handler: deleteReviewController, }) ) @@ -93,7 +91,7 @@ export async function reviewsRoute(app: FastifyInstance) { body: updateReviewBodySchema, response: updateReviewResponse, }, - handler: withTracing('update-review', updateReviewController), + handler: updateReviewController, }) ) @@ -108,9 +106,8 @@ export async function reviewsRoute(app: FastifyInstance) { query: getReviewsQuerySchema, response: getDetailedReviewsResponseSchema, }, - handler: withTracing('get-detailed-reviews', (request, reply) => - getDetailedReviewsController(request, reply, app.redis) - ), + handler: (request, reply) => + getDetailedReviewsController(request, reply, app.redis), }) ) @@ -130,7 +127,7 @@ export async function reviewsRoute(app: FastifyInstance) { ], response: getReviewResponseSchema, }, - handler: withTracing('get-review', getReviewController), + handler: getReviewController, }) ) } diff --git a/apps/backend/src/http/routes/social-auth.ts b/apps/backend/src/http/routes/social-auth.ts index 1e51fc2f..233681e3 100644 --- a/apps/backend/src/http/routes/social-auth.ts +++ b/apps/backend/src/http/routes/social-auth.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { appleAuthController, googleAuthController, @@ -23,9 +21,7 @@ export async function socialAuthRoutes(app: FastifyInstance) { body: appleAuthBodySchema, response: socialAuthResponseSchema, }, - handler: withTracing('apple-auth', (request, reply) => - appleAuthController(request, reply, app) - ), + handler: (request, reply) => appleAuthController(request, reply, app), }) app.withTypeProvider().route({ @@ -37,8 +33,6 @@ export async function socialAuthRoutes(app: FastifyInstance) { body: googleAuthBodySchema, response: socialAuthResponseSchema, }, - handler: withTracing('google-auth', (request, reply) => - googleAuthController(request, reply, app) - ), + handler: (request, reply) => googleAuthController(request, reply, app), }) } diff --git a/apps/backend/src/http/routes/social-links.ts b/apps/backend/src/http/routes/social-links.ts index b66c61a9..4b0070ee 100644 --- a/apps/backend/src/http/routes/social-links.ts +++ b/apps/backend/src/http/routes/social-links.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { getSocialLinksController, upsertSocialLinksController, @@ -35,7 +33,7 @@ export async function socialLinksRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('upsert-social-links', upsertSocialLinksController), + handler: upsertSocialLinksController, }) ) @@ -49,7 +47,7 @@ export async function socialLinksRoute(app: FastifyInstance) { querystring: getSocialLinksQuerySchema, response: getSocialLinksResponseSchema, }, - handler: withTracing('get-social-links', getSocialLinksController), + handler: getSocialLinksController, }) ) } diff --git a/apps/backend/src/http/routes/subscriptions.ts b/apps/backend/src/http/routes/subscriptions.ts index 1bf771a8..d5d4bfae 100644 --- a/apps/backend/src/http/routes/subscriptions.ts +++ b/apps/backend/src/http/routes/subscriptions.ts @@ -1,7 +1,5 @@ import type { FastifyInstance } from 'fastify' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { deleteSubscriptionController } from '../controllers/subscriptions-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { @@ -27,5 +25,5 @@ export const subscriptionsRoutes = (app: FastifyInstance) => ], }, - handler: withTracing('delete-subscription', deleteSubscriptionController), + handler: deleteSubscriptionController, }) diff --git a/apps/backend/src/http/routes/tmdb-proxy.ts b/apps/backend/src/http/routes/tmdb-proxy.ts index f2ab4d9e..c06f5253 100644 --- a/apps/backend/src/http/routes/tmdb-proxy.ts +++ b/apps/backend/src/http/routes/tmdb-proxy.ts @@ -2,7 +2,6 @@ import type { FastifyInstance } from 'fastify' import https from 'https' import { config } from '@/config' -import { withTracing } from '@/infra/telemetry/with-tracing' const TMDB_BASE_URL = 'https://api.themoviedb.org/3' @@ -87,7 +86,7 @@ export async function tmdbProxyRoutes(app: FastifyInstance) { tags: TMDB_PROXY_TAGS, hide: config.app.APP_ENV === 'production', }, - handler: withTracing('tmdb-proxy', async (request, reply) => { + handler: async (request, reply) => { const tmdbPath = (request.params as { '*': string })['*'] if (!tmdbPath) { diff --git a/apps/backend/src/http/routes/user-activities.ts b/apps/backend/src/http/routes/user-activities.ts index 79c627d0..24aabf86 100644 --- a/apps/backend/src/http/routes/user-activities.ts +++ b/apps/backend/src/http/routes/user-activities.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { deleteUserActivityController, getUserActivitiesController, @@ -31,9 +29,8 @@ export async function userActivitiesRoutes(app: FastifyInstance) { params: getUserActivitiesParamsSchema, response: getUserActivitiesResponseSchema, }, - handler: withTracing('get-user-activities', (request, reply) => - getUserActivitiesController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserActivitiesController(request, reply, app.redis), }) ) @@ -47,7 +44,7 @@ export async function userActivitiesRoutes(app: FastifyInstance) { tags: TAGS, params: deleteUserActivityParamsSchema, }, - handler: withTracing('delete-user-activity', deleteUserActivityController), + handler: deleteUserActivityController, }) ) @@ -62,9 +59,8 @@ export async function userActivitiesRoutes(app: FastifyInstance) { querystring: getUserNetworkActivitiesQuerySchema, response: getUserActivitiesResponseSchema, }, - handler: withTracing('get-user-network-activities', (request, reply) => - getUserNetworkActivitiesController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserNetworkActivitiesController(request, reply, app.redis), }) }) } diff --git a/apps/backend/src/http/routes/user-episodes.ts b/apps/backend/src/http/routes/user-episodes.ts index 5b129e52..901f8df1 100644 --- a/apps/backend/src/http/routes/user-episodes.ts +++ b/apps/backend/src/http/routes/user-episodes.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createUserEpisodesController, deleteUserEpisodesController, @@ -37,7 +35,7 @@ export async function userEpisodesRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('create-user-episodes', createUserEpisodesController), + handler: createUserEpisodesController, }) ) @@ -57,7 +55,7 @@ export async function userEpisodesRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('get-user-episodes', getUserEpisodesController), + handler: getUserEpisodesController, }) ) @@ -77,7 +75,7 @@ export async function userEpisodesRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('delete-user-episodes', deleteUserEpisodesController), + handler: deleteUserEpisodesController, }) ) } diff --git a/apps/backend/src/http/routes/user-items.ts b/apps/backend/src/http/routes/user-items.ts index 95f31182..93c1afd8 100644 --- a/apps/backend/src/http/routes/user-items.ts +++ b/apps/backend/src/http/routes/user-items.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { deleteUserItemController, getAllUserItemsController, @@ -47,9 +45,8 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('upsert-user-item', (request, reply) => - upsertUserItemController(request, reply, app.redis) - ), + handler: (request, reply) => + upsertUserItemController(request, reply, app.redis), }) ) @@ -64,9 +61,8 @@ export async function userItemsRoutes(app: FastifyInstance) { response: getUserItemsResponseSchema, operationId: 'getUserItems', }, - handler: withTracing('get-user-items', (request, reply) => - getUserItemsController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserItemsController(request, reply, app.redis), }) ) @@ -85,9 +81,8 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('delete-user-item', (request, reply) => - deleteUserItemController(request, reply, app.redis) - ), + handler: (request, reply) => + deleteUserItemController(request, reply, app.redis), }) ) @@ -107,7 +102,7 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('get-user-item', getUserItemController), + handler: getUserItemController, }) ) @@ -122,7 +117,7 @@ export async function userItemsRoutes(app: FastifyInstance) { response: getAllUserItemsResponseSchema, operationId: 'getAllUserItems', }, - handler: withTracing('get-all-user-items', getAllUserItemsController), + handler: getAllUserItemsController, }) ) @@ -141,7 +136,7 @@ export async function userItemsRoutes(app: FastifyInstance) { }, ], }, - handler: withTracing('reorder-user-items', reorderUserItemsController), + handler: reorderUserItemsController, }) ) @@ -156,7 +151,7 @@ export async function userItemsRoutes(app: FastifyInstance) { response: getUserItemsCountResponseSchema, operationId: 'getUserItemsCount', }, - handler: withTracing('get-user-items-count', getUserItemsCountController), + handler: getUserItemsCountController, }) ) } diff --git a/apps/backend/src/http/routes/user-stats.ts b/apps/backend/src/http/routes/user-stats.ts index 009c0e2d..71afdf0f 100644 --- a/apps/backend/src/http/routes/user-stats.ts +++ b/apps/backend/src/http/routes/user-stats.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { getUserBestReviewsController, getUserItemsStatusController, @@ -44,7 +42,7 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserStatsResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-stats', getUserStatsController), + handler: getUserStatsController, }) ) @@ -58,9 +56,8 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserTotalHoursResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-total-hours', (request, reply) => - getUserTotalHoursController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserTotalHoursController(request, reply, app.redis), }) ) @@ -74,7 +71,7 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserReviewsCountResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-reviews-count', getUserReviewsCountController), + handler: getUserReviewsCountController, }) ) @@ -89,9 +86,8 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserMostWatchedSeriesResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-most-watched-series', (request, reply) => - getUserMostWatchedSeriesController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserMostWatchedSeriesController(request, reply, app.redis), }) ) @@ -106,9 +102,8 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserWatchedGenresResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-watched-genres', (request, reply) => - getUserWatchedGenresController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserWatchedGenresController(request, reply, app.redis), }) ) @@ -123,9 +118,8 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserWatchedCastResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-watched-cast', (request, reply) => - getUserWatchedCastController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserWatchedCastController(request, reply, app.redis), }) ) @@ -140,9 +134,8 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserWatchedCountriesResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-watched-countries', (request, reply) => - getUserWatchedCountriesController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserWatchedCountriesController(request, reply, app.redis), }) ) @@ -157,9 +150,8 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserBestReviewsResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-best-reviews', (request, reply) => - getUserBestReviewsController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserBestReviewsController(request, reply, app.redis), }) ) @@ -173,9 +165,8 @@ export async function userStatsRoutes(app: FastifyInstance) { response: getUserItemsStatusResponseSchema, tags: USER_STATS_TAG, }, - handler: withTracing('get-user-items-status', (request, reply) => - getUserItemsStatusController(request, reply, app.redis) - ), + handler: (request, reply) => + getUserItemsStatusController(request, reply, app.redis), }) ) } diff --git a/apps/backend/src/http/routes/users.ts b/apps/backend/src/http/routes/users.ts index 48f8e066..abb02702 100644 --- a/apps/backend/src/http/routes/users.ts +++ b/apps/backend/src/http/routes/users.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createUserController, getMeController, @@ -53,7 +51,7 @@ export async function usersRoute(app: FastifyInstance) { body: createUserBodySchema, response: createUserResponseSchema, }, - handler: withTracing('create-user', createUserController), + handler: createUserController, }) ) @@ -67,7 +65,7 @@ export async function usersRoute(app: FastifyInstance) { querystring: checkAvailableUsernameQuerySchema, response: checkAvailableUsernameResponseSchema, }, - handler: withTracing('is-username-available', isUsernameAvailableController), + handler: isUsernameAvailableController, }) ) @@ -81,7 +79,7 @@ export async function usersRoute(app: FastifyInstance) { querystring: isEmailAvailableQuerySchema, response: isEmailAvailableResponseSchema, }, - handler: withTracing('is-email-available', isEmailAvailableController), + handler: isEmailAvailableController, }) ) @@ -95,7 +93,7 @@ export async function usersRoute(app: FastifyInstance) { params: getUserByUsernameParamsSchema, response: getUserByUsernameResponseSchema, }, - handler: withTracing('get-user-by-username', getUserByUsernameController), + handler: getUserByUsernameController, }) ) @@ -109,7 +107,7 @@ export async function usersRoute(app: FastifyInstance) { params: getUserByIdParamsSchema, response: getUserByIdResponseSchema, }, - handler: withTracing('get-user-by-id', getUserByIdController), + handler: getUserByIdController, }) ) @@ -128,7 +126,7 @@ export async function usersRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('get-me', getMeController), + handler: getMeController, }) ) @@ -144,7 +142,7 @@ export async function usersRoute(app: FastifyInstance) { response: updateUserResponseSchema, security: [{ bearerAuth: [] }], }, - handler: withTracing('update-user', updateUserController), + handler: updateUserController, }) ) @@ -158,7 +156,7 @@ export async function usersRoute(app: FastifyInstance) { body: updateUserPasswordBodySchema, response: updateUserPasswordResponseSchema, }, - handler: withTracing('update-user-password', updateUserPasswordController), + handler: updateUserPasswordController, }) ) @@ -179,7 +177,7 @@ export async function usersRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('update-user-preferences', updateUserPreferencesController), + handler: updateUserPreferencesController, }) ) @@ -199,7 +197,7 @@ export async function usersRoute(app: FastifyInstance) { }, ], }, - handler: withTracing('get-user-preferences', getUserPreferencesController), + handler: getUserPreferencesController, }) ) @@ -213,7 +211,7 @@ export async function usersRoute(app: FastifyInstance) { querystring: searchUsersByUsernameQuerySchema, response: searchUsersByUsernameResponseSchema, }, - handler: withTracing('search-users-by-username', searchUsersByUsernameController), + handler: searchUsersByUsernameController, }) ) } diff --git a/apps/backend/src/http/routes/watch-entries.ts b/apps/backend/src/http/routes/watch-entries.ts index 1e45b0ff..536f8f46 100644 --- a/apps/backend/src/http/routes/watch-entries.ts +++ b/apps/backend/src/http/routes/watch-entries.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { createWatchEntryController, deleteWatchEntryController, @@ -36,7 +34,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { response: createWatchEntryResponseSchema, security: [{ bearerAuth: [] }], }, - handler: withTracing('create-watch-entry', createWatchEntryController), + handler: createWatchEntryController, }) ) @@ -52,7 +50,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { response: getWatchEntriesResponseSchema, security: [{ bearerAuth: [] }], }, - handler: withTracing('get-watch-entries', getWatchEntriesController), + handler: getWatchEntriesController, }) ) @@ -69,7 +67,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { response: updateWatchEntryResponseSchema, security: [{ bearerAuth: [] }], }, - handler: withTracing('update-watch-entry', updateWatchEntryController), + handler: updateWatchEntryController, }) ) @@ -84,7 +82,7 @@ export async function watchEntriesRoutes(app: FastifyInstance) { params: deleteWatchEntryParamsSchema, security: [{ bearerAuth: [] }], }, - handler: withTracing('delete-watch-entry', deleteWatchEntryController), + handler: deleteWatchEntryController, }) ) } diff --git a/apps/backend/src/http/routes/webhook.ts b/apps/backend/src/http/routes/webhook.ts index 2df08c63..05e2135c 100644 --- a/apps/backend/src/http/routes/webhook.ts +++ b/apps/backend/src/http/routes/webhook.ts @@ -1,8 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' -import { withTracing } from '@/infra/telemetry/with-tracing' - import { stripeWebhookController } from '../controllers/stripe-webhook-controller' export async function webhookRoutes(app: FastifyInstance) { @@ -29,7 +27,7 @@ export async function webhookRoutes(app: FastifyInstance) { description: 'Webhook route', tags: ['Webhook'], }, - handler: withTracing('stripe-webhook', stripeWebhookController), + handler: stripeWebhookController, }) ) } diff --git a/apps/backend/src/http/server.ts b/apps/backend/src/http/server.ts index de3f5fde..96320768 100644 --- a/apps/backend/src/http/server.ts +++ b/apps/backend/src/http/server.ts @@ -8,6 +8,7 @@ import { import { ZodError } from 'zod' import { logger } from '@/adapters/logger' import { DomainError } from '@/domain/errors/domain-error' +import { fastifyOtel } from '@/infra/telemetry/otel' import { registerHttpRequestMetrics } from '@/infra/telemetry/http-request-metrics' import { config } from '../config' import { routes } from './routes' @@ -15,7 +16,9 @@ import { transformSwaggerSchema } from './transform-schema' const app: FastifyInstance = fastify() -export function startServer() { +export async function startServer() { + await app.register(fastifyOtel.plugin()) + app.setValidatorCompiler(validatorCompiler) app.setSerializerCompiler(serializerCompiler) @@ -92,12 +95,9 @@ export function startServer() { // registerClientGuard(app) routes(app) - app - .listen({ - port: config.app.PORT, - host: '0.0.0.0', - }) - .then(() => { - logger.info(`HTTP server running at ${config.app.BASE_URL}`) - }) + await app.listen({ + port: config.app.PORT, + host: '0.0.0.0', + }) + logger.info(`HTTP server running at ${config.app.BASE_URL}`) } diff --git a/apps/backend/src/infra/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts index 8f4f8033..38d10850 100644 --- a/apps/backend/src/infra/telemetry/otel.ts +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -1,3 +1,5 @@ +import { trace } from '@opentelemetry/api' +import FastifyOtel from '@fastify/otel' import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' import { resourceFromAttributes } from '@opentelemetry/resources' @@ -28,3 +30,8 @@ const sdk = new NodeSDK({ console.log('Starting OTLP exporter') sdk.start() + +const fastifyOtel = new FastifyOtel() +fastifyOtel.setTracerProvider(trace.getTracerProvider()) + +export { fastifyOtel } diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index ec6101b6..9fb6fe7f 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -4,7 +4,7 @@ import { startWorkers } from './worker' async function main() { startWorkers() - startServer() + await startServer() } main().catch(err => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 361870eb..71191356 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@fastify/multipart': specifier: ^9.3.0 version: 9.4.0 + '@fastify/otel': + specifier: ^0.16.0 + version: 0.16.0(@opentelemetry/api@1.9.0) '@fastify/rate-limit': specifier: ^10.3.0 version: 10.3.0 @@ -65,6 +68,9 @@ importers: '@opentelemetry/exporter-trace-otlp-proto': specifier: ^0.211.0 version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.5.0 version: 2.5.0(@opentelemetry/api@1.9.0) @@ -2171,6 +2177,11 @@ packages: '@fastify/multipart@9.4.0': resolution: {integrity: sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==} + '@fastify/otel@0.16.0': + resolution: {integrity: sha512-2304BdM5Q/kUvQC9qJO1KZq3Zn1WWsw+WWkVmFEaj1UE2hEIiuFqrPeglQOwEtw/ftngisqfQ3v70TWMmwhhHA==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} @@ -2591,10 +2602,18 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.211.0': resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -2617,6 +2636,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0': resolution: {integrity: sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -2683,12 +2708,30 @@ packages: peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/instrumentation-http@0.212.0': + resolution: {integrity: sha512-t2nt16Uyv9irgR+tqnX96YeToOStc3X5js7Ljn3EKlI2b4Fe76VhMkTXtsTQ0aId6AsYgefrCRnXSCo/Fn/vww==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.208.0': + resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.211.0': resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@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} @@ -6006,6 +6049,9 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -10786,6 +10832,16 @@ snapshots: fastify-plugin: 5.1.0 secure-json-parse: 4.1.0 + '@fastify/otel@0.16.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + minimatch: 10.1.1 + transitivePeerDependencies: + - supports-color + '@fastify/proxy-addr@5.1.0': dependencies: '@fastify/forwarded': 3.0.1 @@ -11216,10 +11272,18 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.211.0': dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.212.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api@1.9.0': {} '@opentelemetry/configuration@0.211.0(@opentelemetry/api@1.9.0)': @@ -11237,6 +11301,11 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 @@ -11342,6 +11411,25 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/instrumentation-http@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -11351,6 +11439,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/otlp-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15077,6 +15174,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded-parse@2.1.2: {} + fraction.js@5.3.4: {} framer-motion@12.29.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): From f207bccccd9c810fb1c666614583acaa6df8d464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Wed, 18 Feb 2026 23:29:24 -0300 Subject: [PATCH 09/26] refactor: remove telemetry tracing from repository functions and expose core database operations directly --- .../repositories/review-replies-repository.ts | 38 ++-------- .../src/db/repositories/reviews-repository.ts | 53 ++++---------- .../repositories/subscription-repository.ts | 47 +++--------- .../src/db/repositories/user-activities.ts | 42 +++-------- .../src/db/repositories/user-episode.ts | 31 ++------ .../db/repositories/user-item-repository.ts | 72 ++++--------------- .../src/db/repositories/user-preferences.ts | 17 +---- .../backend/src/db/repositories/user-stats.ts | 10 +-- .../user-watch-entries-repository.ts | 38 ++-------- .../services/feedback/create-feedback.ts | 8 +-- .../domain/services/follows/create-follow.ts | 10 +-- .../domain/services/follows/delete-follow.ts | 10 +-- .../src/domain/services/follows/get-follow.ts | 10 +-- .../domain/services/follows/get-followers.ts | 8 +-- .../src/domain/services/likes/create-like.ts | 8 +-- .../src/domain/services/likes/delete-like.ts | 8 +-- .../src/domain/services/likes/get-likes.ts | 8 +-- .../services/list-item/create-list-item.ts | 10 +-- .../services/list-item/delete-list-item.ts | 10 +-- .../services/list-item/get-list-items.ts | 8 +-- .../services/list-item/update-list-items.ts | 10 +-- .../src/domain/services/lists/create-list.ts | 7 +- .../src/domain/services/lists/delete-list.ts | 8 +-- .../services/lists/get-list-progress.ts | 10 +-- .../src/domain/services/lists/get-list.ts | 7 +- .../src/domain/services/lists/get-lists.ts | 8 +-- .../services/lists/update-list-banner.ts | 8 +-- .../src/domain/services/lists/update-list.ts | 10 +-- .../src/domain/services/login/login.ts | 5 +- .../magic-link/generate-magic-link.ts | 8 +-- .../magic-link/send-magic-link-email.ts | 10 +-- .../review-replies/create-review-reply.ts | 10 +-- .../domain/services/reviews/create-review.ts | 8 +-- .../domain/services/reviews/delete-review.ts | 8 +-- .../src/domain/services/reviews/get-review.ts | 8 +-- .../domain/services/reviews/get-reviews.ts | 8 +-- .../domain/services/reviews/update-review.ts | 8 +-- .../services/social-links/get-social-links.ts | 8 +-- .../social-links/upsert-social-links.ts | 8 +-- .../src/domain/services/tmdb/get-tmdb-data.ts | 10 +-- .../user-activities/create-user-activity.ts | 8 +-- .../services/user-items/get-user-items.ts | 8 +-- .../services/user-items/upsert-user-item.ts | 8 +-- .../user-preferences/get-user-preferences.ts | 10 +-- .../update-user-preferences.ts | 10 +-- .../src/domain/services/users/create-user.ts | 7 +- .../src/domain/services/users/get-by-id.ts | 5 +- .../services/users/get-user-by-username.ts | 10 +-- .../services/users/is-email-available.ts | 10 +-- .../services/users/is-username-available.ts | 10 +-- .../users/search-users-by-username.ts | 8 +-- .../services/users/update-user-password.ts | 10 +-- .../src/domain/services/users/update-user.ts | 13 ++-- 53 files changed, 130 insertions(+), 600 deletions(-) diff --git a/apps/backend/src/db/repositories/review-replies-repository.ts b/apps/backend/src/db/repositories/review-replies-repository.ts index cd937c2c..08c34277 100644 --- a/apps/backend/src/db/repositories/review-replies-repository.ts +++ b/apps/backend/src/db/repositories/review-replies-repository.ts @@ -2,42 +2,26 @@ import { and, asc, eq, getTableColumns, sql } from 'drizzle-orm' import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertReviewReplyModel } from '@/domain/entities/review-reply' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -const insertReviewReplyImpl = async (params: InsertReviewReplyModel) => { +export async function insertReviewReply(params: InsertReviewReplyModel) { return db.insert(schema.reviewReplies).values(params).returning() } -export const insertReviewReply = withDbTracing( - 'insert-review-reply', - insertReviewReplyImpl -) - -const deleteReviewReplyImpl = async (id: string) => { +export async function deleteReviewReply(id: string) { return db .delete(schema.reviewReplies) .where(and(eq(schema.reviewReplies.id, id))) .returning() } -export const deleteReviewReply = withDbTracing( - 'delete-review-reply', - deleteReviewReplyImpl -) - -const getReviewReplyByIdImpl = async (id: string) => { +export async function getReviewReplyById(id: string) { return db .select() .from(schema.reviewReplies) .where(eq(schema.reviewReplies.id, id)) } -export const getReviewReplyById = withDbTracing( - 'get-review-reply-by-id', - getReviewReplyByIdImpl -) - -const updateReviewReplyImpl = async (id: string, reply: string) => { +export async function updateReviewReply(id: string, reply: string) { return db .update(schema.reviewReplies) .set({ reply }) @@ -45,15 +29,10 @@ const updateReviewReplyImpl = async (id: string, reply: string) => { .returning() } -export const updateReviewReply = withDbTracing( - 'update-review-reply', - updateReviewReplyImpl -) - -const selectReviewRepliesImpl = async ( +export async function selectReviewReplies( reviewId: string, authenticatedUserId?: string -) => { +) { return db .select({ ...getTableColumns(schema.reviewReplies), @@ -86,8 +65,3 @@ const selectReviewRepliesImpl = async ( .leftJoin(schema.users, eq(schema.reviewReplies.userId, schema.users.id)) .orderBy(asc(schema.reviewReplies.createdAt)) } - -export const selectReviewReplies = withDbTracing( - 'select-review-replies', - selectReviewRepliesImpl -) diff --git a/apps/backend/src/db/repositories/reviews-repository.ts b/apps/backend/src/db/repositories/reviews-repository.ts index cc23b659..36413538 100644 --- a/apps/backend/src/db/repositories/reviews-repository.ts +++ b/apps/backend/src/db/repositories/reviews-repository.ts @@ -15,14 +15,11 @@ import type { InsertReviewModel } from '@/domain/entities/review' import type { GetReviewInput } from '@/domain/services/reviews/get-review' import type { GetReviewsServiceInput } from '@/domain/services/reviews/get-reviews' import type { UpdateReviewInput } from '@/domain/services/reviews/update-review' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -const insertReviewImpl = async (params: InsertReviewModel) => { +export async function insertReview(params: InsertReviewModel) { return db.insert(schema.reviews).values(params).returning() } -export const insertReview = withDbTracing('insert-review', insertReviewImpl) - function buildSeasonEpisodeFilter( seasonNumber?: number, episodeNumber?: number @@ -47,7 +44,7 @@ function buildSeasonEpisodeFilter( ) } -const selectReviewsImpl = async ({ +export async function selectReviews({ mediaType, tmdbId, userId, @@ -59,7 +56,7 @@ const selectReviewsImpl = async ({ endDate, seasonNumber, episodeNumber, -}: GetReviewsServiceInput) => { +}: GetReviewsServiceInput) { const orderCriteria = [ orderBy === 'likeCount' ? desc( @@ -121,27 +118,20 @@ const selectReviewsImpl = async ({ ) .leftJoin(schema.users, eq(schema.reviews.userId, schema.users.id)) .orderBy(...orderCriteria) - .limit(limit + 1) // Fetch one extra to check if there are more + .limit(limit + 1) .offset(offset) } -export const selectReviews = withDbTracing( - 'select-reviews', - selectReviewsImpl -) - -const deleteReviewImpl = async (id: string) => { +export async function deleteReview(id: string) { return db.delete(schema.reviews).where(eq(schema.reviews.id, id)).returning() } -export const deleteReview = withDbTracing('delete-review', deleteReviewImpl) - -const updateReviewImpl = async ({ +export async function updateReview({ id, rating, review, hasSpoilers, -}: UpdateReviewInput) => { +}: UpdateReviewInput) { return db .update(schema.reviews) .set({ rating, review, hasSpoilers }) @@ -149,30 +139,18 @@ const updateReviewImpl = async ({ .returning() } -export const updateReview = withDbTracing('update-review', updateReviewImpl) - -const getReviewByIdImpl = async (id: string) => { +export async function getReviewById(id: string) { return db.select().from(schema.reviews).where(eq(schema.reviews.id, id)) } -export const getReviewById = withDbTracing( - 'get-review-by-id', - getReviewByIdImpl -) - -const selectReviewsCountImpl = async (userId?: string) => { +export async function selectReviewsCount(userId?: string) { return db .select({ count: count() }) .from(schema.reviews) .where(userId ? eq(schema.reviews.userId, userId) : undefined) } -export const selectReviewsCount = withDbTracing( - 'select-reviews-count', - selectReviewsCountImpl -) - -const selectBestReviewsImpl = async (userId: string, limit = 10) => { +export async function selectBestReviews(userId: string, limit = 10) { return db .select() .from(schema.reviews) @@ -188,18 +166,13 @@ const selectBestReviewsImpl = async (userId: string, limit = 10) => { .limit(limit) } -export const selectBestReviews = withDbTracing( - 'select-best-reviews', - selectBestReviewsImpl -) - -const selectReviewImpl = async ({ +export async function selectReview({ mediaType, tmdbId, userId, seasonNumber, episodeNumber, -}: GetReviewInput) => { +}: GetReviewInput) { return db .select() .from(schema.reviews) @@ -214,5 +187,3 @@ const selectReviewImpl = async ({ ) ) } - -export const selectReview = withDbTracing('select-review', selectReviewImpl) diff --git a/apps/backend/src/db/repositories/subscription-repository.ts b/apps/backend/src/db/repositories/subscription-repository.ts index ee79eecd..aa30029f 100644 --- a/apps/backend/src/db/repositories/subscription-repository.ts +++ b/apps/backend/src/db/repositories/subscription-repository.ts @@ -2,18 +2,12 @@ import { and, desc, eq, or } from 'drizzle-orm' import { db } from '@/db' import { schema } from '@/db/schema' import type { InsertSubscriptionModel } from '@/domain/entities/subscription' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' -const insertSubscriptionImpl = async (params: InsertSubscriptionModel) => { +export async function insertSubscription(params: InsertSubscriptionModel) { return db.insert(schema.subscriptions).values(params).returning() } -export const insertSubscription = withDbTracing( - 'insert-subscription', - insertSubscriptionImpl -) - -const getActiveSubscriptionByUserIdImpl = async (userId: string) => { +export async function getActiveSubscriptionByUserId(userId: string) { return db.query.subscriptions.findFirst({ where: and( eq(schema.subscriptions.userId, userId), @@ -22,22 +16,12 @@ const getActiveSubscriptionByUserIdImpl = async (userId: string) => { }) } -export const getActiveSubscriptionByUserId = withDbTracing( - 'get-active-subscription-by-user-id', - getActiveSubscriptionByUserIdImpl -) - -const getSubscriptionByIdImpl = async (id: string) => { +export async function getSubscriptionById(id: string) { return db.query.subscriptions.findFirst({ where: eq(schema.subscriptions.id, id), }) } -export const getSubscriptionById = withDbTracing( - 'get-subscription-by-id', - getSubscriptionByIdImpl -) - export type CancelSubscriptionParams = { id: string userId: string @@ -46,9 +30,9 @@ export type CancelSubscriptionParams = { cancellationReason: string | undefined } -const cancelUserSubscriptionImpl = async ( +export async function cancelUserSubscription( params: CancelSubscriptionParams -) => { +) { const [subscription] = await db .update(schema.subscriptions) .set({ @@ -67,15 +51,10 @@ const cancelUserSubscriptionImpl = async ( return subscription } -export const cancelUserSubscription = withDbTracing( - 'cancel-user-subscription', - cancelUserSubscriptionImpl -) - -const updateSubscriptionImpl = async ( +export async function updateSubscription( userId: string, type: 'PRO' | 'MEMBER' -) => { +) { return db .update(schema.subscriptions) .set({ type }) @@ -83,12 +62,7 @@ const updateSubscriptionImpl = async ( .returning() } -export const updateSubscription = withDbTracing( - 'update-subscription', - updateSubscriptionImpl -) - -const getLastestActiveSubscriptionImpl = async (userId: string) => { +export async function getLastestActiveSubscription(userId: string) { const [subscription] = await db .select() .from(schema.subscriptions) @@ -110,8 +84,3 @@ const getLastestActiveSubscriptionImpl = async (userId: string) => { return subscription } - -export const getLastestActiveSubscription = withDbTracing( - 'get-lastest-active-subscription', - getLastestActiveSubscriptionImpl -) diff --git a/apps/backend/src/db/repositories/user-activities.ts b/apps/backend/src/db/repositories/user-activities.ts index 73b56518..0b98070e 100644 --- a/apps/backend/src/db/repositories/user-activities.ts +++ b/apps/backend/src/db/repositories/user-activities.ts @@ -6,24 +6,18 @@ import type { InsertUserActivity, SelectUserActivities, } from '@/domain/entities/user-activity' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const insertUserActivityImpl = async (values: InsertUserActivity) => { +export async function insertUserActivity(values: InsertUserActivity) { return db.insert(schema.userActivities).values(values) } -export const insertUserActivity = withDbTracing( - 'insert-user-activity', - insertUserActivityImpl -) - -const selectUserActivitiesImpl = async ({ +export async function selectUserActivities({ userIds, pageSize, cursor, -}: SelectUserActivities) => { +}: SelectUserActivities) { const additionalInfoCase = buildAdditionalInfoCase() const owner = alias(schema.users, 'owner') @@ -49,11 +43,6 @@ const selectUserActivitiesImpl = async ({ .leftJoin(owner, eq(schema.userActivities.userId, owner.id)) } -export const selectUserActivities = withDbTracing( - 'select-user-activities', - selectUserActivitiesImpl -) - function buildAdditionalInfoCase() { return sql` CASE @@ -190,12 +179,12 @@ function buildRepliesJoinCondition() { AND ${schema.userActivities.entityId} = ${schema.reviewReplies.id}` } -const deleteUserActivityImpl = async ({ +export async function deleteUserActivity({ activityType, entityId, entityType, userId, -}: DeleteUserActivity) => { +}: DeleteUserActivity) { return db .delete(schema.userActivities) .where( @@ -208,16 +197,11 @@ const deleteUserActivityImpl = async ({ ) } -export const deleteUserActivity = withDbTracing( - 'delete-user-activity', - deleteUserActivityImpl -) - -const deleteFollowUserActivityImpl = async ({ +export async function deleteFollowUserActivity({ followedId, followerId, userId, -}: DeleteFollowUserActivity) => { +}: DeleteFollowUserActivity) { return db .delete(schema.userActivities) .where( @@ -230,18 +214,8 @@ const deleteFollowUserActivityImpl = async ({ ) } -export const deleteFollowUserActivity = withDbTracing( - 'delete-follow-user-activity', - deleteFollowUserActivityImpl -) - -const deleteUserActivityByIdImpl = async (activityId: string) => { +export async function deleteUserActivityById(activityId: string) { return db .delete(schema.userActivities) .where(eq(schema.userActivities.id, activityId)) } - -export const deleteUserActivityById = withDbTracing( - 'delete-user-activity-by-id', - deleteUserActivityByIdImpl -) diff --git a/apps/backend/src/db/repositories/user-episode.ts b/apps/backend/src/db/repositories/user-episode.ts index f30ab528..e8b22b0c 100644 --- a/apps/backend/src/db/repositories/user-episode.ts +++ b/apps/backend/src/db/repositories/user-episode.ts @@ -1,11 +1,10 @@ import { and, count, desc, eq, inArray } from 'drizzle-orm' import type { InsertUserEpisode } from '@/domain/entities/user-episode' import type { GetUserEpisodesInput } from '@/domain/services/user-episodes/get-user-episodes' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const insertUserEpisodesImpl = async (values: InsertUserEpisode[]) => { +export async function insertUserEpisodes(values: InsertUserEpisode[]) { return db .insert(schema.userEpisodes) .values(values) @@ -13,15 +12,10 @@ const insertUserEpisodesImpl = async (values: InsertUserEpisode[]) => { .onConflictDoNothing() } -export const insertUserEpisodes = withDbTracing( - 'insert-user-episodes', - insertUserEpisodesImpl -) - -const selectUserEpisodesImpl = async ({ +export async function selectUserEpisodes({ userId, tmdbId, -}: GetUserEpisodesInput) => { +}: GetUserEpisodesInput) { return db .select() .from(schema.userEpisodes) @@ -34,23 +28,13 @@ const selectUserEpisodesImpl = async ({ .orderBy(schema.userEpisodes.episodeNumber) } -export const selectUserEpisodes = withDbTracing( - 'select-user-episodes', - selectUserEpisodesImpl -) - -const deleteUserEpisodesImpl = async (ids: string[]) => { +export async function deleteUserEpisodes(ids: string[]) { return db .delete(schema.userEpisodes) .where(inArray(schema.userEpisodes.id, ids)) } -export const deleteUserEpisodes = withDbTracing( - 'delete-user-episodes', - deleteUserEpisodesImpl -) - -const selectMostWatchedImpl = async (userId: string) => { +export async function selectMostWatched(userId: string) { return db .select({ count: count(), tmdbId: schema.userEpisodes.tmdbId }) .from(schema.userEpisodes) @@ -59,8 +43,3 @@ const selectMostWatchedImpl = async (userId: string) => { .orderBy(desc(count())) .limit(3) } - -export const selectMostWatched = withDbTracing( - 'select-most-watched', - selectMostWatchedImpl -) diff --git a/apps/backend/src/db/repositories/user-item-repository.ts b/apps/backend/src/db/repositories/user-item-repository.ts index 734949f4..ddc3825b 100644 --- a/apps/backend/src/db/repositories/user-item-repository.ts +++ b/apps/backend/src/db/repositories/user-item-repository.ts @@ -17,16 +17,15 @@ import type { SelectUserItems, } from '@/domain/entities/user-item' import type { GetUserItemInput } from '@/domain/services/user-items/get-user-item' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const upsertUserItemImpl = async ({ +export async function upsertUserItem({ mediaType, tmdbId, userId, status, -}: InsertUserItem) => { +}: InsertUserItem) { return db.execute( sql` INSERT INTO ${schema.userItems} (media_type, tmdb_id, user_id, status) @@ -40,9 +39,7 @@ const upsertUserItemImpl = async ({ ) } -export const upsertUserItem = withDbTracing('upsert-user-item', upsertUserItemImpl) - -const selectUserItemsImpl = async ({ +export async function selectUserItems({ userId, status, pageSize, @@ -52,7 +49,7 @@ const selectUserItemsImpl = async ({ mediaType, rating, onlyItemsWithoutReview, -}: SelectUserItems) => { +}: SelectUserItems) { const whereConditions = [ eq(schema.userItems.userId, userId), inArray(schema.userItems.mediaType, mediaType), @@ -115,28 +112,18 @@ const selectUserItemsImpl = async ({ .limit(pageSize + 1) } -export const selectUserItems = withDbTracing( - 'select-user-items', - selectUserItemsImpl -) - -const deleteUserItemImpl = async (id: string) => { +export async function deleteUserItem(id: string) { return db .delete(schema.userItems) .where(eq(schema.userItems.id, id)) .returning() } -export const deleteUserItem = withDbTracing( - 'delete-user-item', - deleteUserItemImpl -) - -const selectUserItemImpl = async ({ +export async function selectUserItem({ userId, mediaType, tmdbId, -}: GetUserItemInput) => { +}: GetUserItemInput) { return db .select() .from(schema.userItems) @@ -150,12 +137,7 @@ const selectUserItemImpl = async ({ .limit(1) } -export const selectUserItem = withDbTracing( - 'select-user-item', - selectUserItemImpl -) - -const selectUserItemStatusImpl = async (userId: string) => { +export async function selectUserItemStatus(userId: string) { return db .select({ status: schema.userItems.status, @@ -167,15 +149,10 @@ const selectUserItemStatusImpl = async (userId: string) => { .groupBy(schema.userItems.status) } -export const selectUserItemStatus = withDbTracing( - 'select-user-item-status', - selectUserItemStatusImpl -) - -const selectAllUserItemsByStatusImpl = async ({ +export async function selectAllUserItemsByStatus({ status, userId, -}: SelectAllUserItems) => { +}: SelectAllUserItems) { const { id, tmdbId, mediaType, position, updatedAt } = getTableColumns(schema.userItems) const whereConditions = [eq(schema.userItems.userId, userId)] @@ -196,17 +173,11 @@ const selectAllUserItemsByStatusImpl = async ({ .orderBy(asc(schema.userItems.position), desc(schema.userItems.updatedAt)) } -export const selectAllUserItemsByStatus = withDbTracing( - 'select-all-user-items-by-status', - selectAllUserItemsByStatusImpl -) - -const reorderUserItemsImpl = async ( +export async function reorderUserItems( userId: string, _status: string, orderedIds: string[] -) => { - // Update position for each item based on array order +) { const updates = orderedIds.map((id, index) => db .update(schema.userItems) @@ -219,12 +190,7 @@ const reorderUserItemsImpl = async ( await Promise.all(updates) } -export const reorderUserItems = withDbTracing( - 'reorder-user-items', - reorderUserItemsImpl -) - -const selectAllUserItemsImpl = async (userId: string) => { +export async function selectAllUserItems(userId: string) { return db .select({ id: schema.userItems.id, @@ -235,12 +201,7 @@ const selectAllUserItemsImpl = async (userId: string) => { .where(eq(schema.userItems.userId, userId)) } -export const selectAllUserItems = withDbTracing( - 'select-all-user-items', - selectAllUserItemsImpl -) - -const selectUserItemsCountImpl = async (userId: string) => { +export async function selectUserItemsCount(userId: string) { const result = await db .select({ count: sql`COUNT(*)::int`, @@ -251,11 +212,6 @@ const selectUserItemsCountImpl = async (userId: string) => { return result[0]?.count ?? 0 } -export const selectUserItemsCount = withDbTracing( - 'select-user-items-count', - selectUserItemsCountImpl -) - function getOrderColumn(orderBy: string) { switch (orderBy) { case 'updatedAt': diff --git a/apps/backend/src/db/repositories/user-preferences.ts b/apps/backend/src/db/repositories/user-preferences.ts index a16f6577..a12a10bd 100644 --- a/apps/backend/src/db/repositories/user-preferences.ts +++ b/apps/backend/src/db/repositories/user-preferences.ts @@ -1,12 +1,11 @@ import { eq } from 'drizzle-orm' import { db } from '@/db' import type { UpdateUserPreferencesParams } from '@/domain/entities/user-preferences' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { userPreferences } from '../schema' -const updateUserPreferencesImpl = async ( +export async function updateUserPreferences( params: UpdateUserPreferencesParams -) => { +) { return await db .insert(userPreferences) .values(params) @@ -28,19 +27,9 @@ const updateUserPreferencesImpl = async ( .returning() } -export const updateUserPreferences = withDbTracing( - 'update-user-preferences', - updateUserPreferencesImpl -) - -const selectUserPreferencesImpl = async (userId: string) => { +export async function selectUserPreferences(userId: string) { return await db .select() .from(userPreferences) .where(eq(userPreferences.userId, userId)) } - -export const selectUserPreferences = withDbTracing( - 'select-user-preferences', - selectUserPreferencesImpl -) diff --git a/apps/backend/src/db/repositories/user-stats.ts b/apps/backend/src/db/repositories/user-stats.ts index 48c6bddc..8467c6bf 100644 --- a/apps/backend/src/db/repositories/user-stats.ts +++ b/apps/backend/src/db/repositories/user-stats.ts @@ -1,11 +1,8 @@ import { and, eq, sql } from 'drizzle-orm' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { schema } from '../schema' -const selectUserStatsImpl = async (userId: string) => { - // Run all independent queries in parallel instead of sequential transaction - // This improves performance as these are all read-only operations +export async function selectUserStats(userId: string) { const [ [{ count: followersCount }], [{ count: followingCount }], @@ -52,8 +49,3 @@ const selectUserStatsImpl = async (userId: string) => { watchedSeriesCount, } } - -export const selectUserStats = withDbTracing( - 'select-user-stats', - selectUserStatsImpl -) diff --git a/apps/backend/src/db/repositories/user-watch-entries-repository.ts b/apps/backend/src/db/repositories/user-watch-entries-repository.ts index 87528c75..741df0a7 100644 --- a/apps/backend/src/db/repositories/user-watch-entries-repository.ts +++ b/apps/backend/src/db/repositories/user-watch-entries-repository.ts @@ -1,12 +1,11 @@ import { eq } from 'drizzle-orm' -import { withDbTracing } from '@/infra/telemetry/with-db-tracing' import { db } from '..' import { userWatchEntries } from '../schema' -const createWatchEntryImpl = async (data: { +export async function createWatchEntry(data: { userItemId: string watchedAt?: Date -}) => { +}) { const [entry] = await db .insert(userWatchEntries) .values({ @@ -18,12 +17,7 @@ const createWatchEntryImpl = async (data: { return entry } -export const createWatchEntry = withDbTracing( - 'create-watch-entry', - createWatchEntryImpl -) - -const getWatchEntriesByUserItemIdImpl = async (userItemId: string) => { +export async function getWatchEntriesByUserItemId(userItemId: string) { return db .select() .from(userWatchEntries) @@ -31,12 +25,7 @@ const getWatchEntriesByUserItemIdImpl = async (userItemId: string) => { .orderBy(userWatchEntries.watchedAt) } -export const getWatchEntriesByUserItemId = withDbTracing( - 'get-watch-entries-by-user-item-id', - getWatchEntriesByUserItemIdImpl -) - -const updateWatchEntryImpl = async (id: string, watchedAt: Date) => { +export async function updateWatchEntry(id: string, watchedAt: Date) { const [entry] = await db .update(userWatchEntries) .set({ watchedAt }) @@ -46,12 +35,7 @@ const updateWatchEntryImpl = async (id: string, watchedAt: Date) => { return entry } -export const updateWatchEntry = withDbTracing( - 'update-watch-entry', - updateWatchEntryImpl -) - -const deleteWatchEntryImpl = async (id: string) => { +export async function deleteWatchEntry(id: string) { const [entry] = await db .delete(userWatchEntries) .where(eq(userWatchEntries.id, id)) @@ -60,18 +44,8 @@ const deleteWatchEntryImpl = async (id: string) => { return entry } -export const deleteWatchEntry = withDbTracing( - 'delete-watch-entry', - deleteWatchEntryImpl -) - -const deleteWatchEntriesByUserItemIdImpl = async (userItemId: string) => { +export async function deleteWatchEntriesByUserItemId(userItemId: string) { await db .delete(userWatchEntries) .where(eq(userWatchEntries.userItemId, userItemId)) } - -export const deleteWatchEntriesByUserItemId = withDbTracing( - 'delete-watch-entries-by-user-item-id', - deleteWatchEntriesByUserItemIdImpl -) diff --git a/apps/backend/src/domain/services/feedback/create-feedback.ts b/apps/backend/src/domain/services/feedback/create-feedback.ts index e63e83c5..54079d32 100644 --- a/apps/backend/src/domain/services/feedback/create-feedback.ts +++ b/apps/backend/src/domain/services/feedback/create-feedback.ts @@ -2,9 +2,8 @@ import { insertFeedback } from '@/db/repositories/feedback-repository' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import type { InsertFeedbackModel } from '@/domain/entities/feedback' import { UserNotFoundError } from '@/domain/errors/user-not-found' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -const createFeedbackServiceImpl = async (params: InsertFeedbackModel) => { +export async function createFeedbackService(params: InsertFeedbackModel) { try { const [feedback] = await insertFeedback(params) return { feedback } @@ -15,8 +14,3 @@ const createFeedbackServiceImpl = async (params: InsertFeedbackModel) => { throw error } } - -export const createFeedbackService = withServiceTracing( - 'create-feedback', - createFeedbackServiceImpl -) diff --git a/apps/backend/src/domain/services/follows/create-follow.ts b/apps/backend/src/domain/services/follows/create-follow.ts index b4deb5fd..a03e45b0 100644 --- a/apps/backend/src/domain/services/follows/create-follow.ts +++ b/apps/backend/src/domain/services/follows/create-follow.ts @@ -1,17 +1,16 @@ import { insertFollow } from '@/db/repositories/followers-repository' import { isUniqueViolation } from '@/db/utils/postgres-errors' import { FollowAlreadyRegisteredError } from '@/domain/errors/follow-already-registered' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type CreateFollowServiceInput = { followerId: string followedId: string } -const createFollowServiceImpl = async ({ +export async function createFollowService({ followedId, followerId, -}: CreateFollowServiceInput) => { +}: CreateFollowServiceInput) { try { const [follow] = await insertFollow({ followedId, followerId }) @@ -24,8 +23,3 @@ const createFollowServiceImpl = async ({ throw error } } - -export const createFollowService = withServiceTracing( - 'create-follow', - createFollowServiceImpl -) diff --git a/apps/backend/src/domain/services/follows/delete-follow.ts b/apps/backend/src/domain/services/follows/delete-follow.ts index bf098453..5130aeb3 100644 --- a/apps/backend/src/domain/services/follows/delete-follow.ts +++ b/apps/backend/src/domain/services/follows/delete-follow.ts @@ -1,21 +1,15 @@ import { deleteFollow } from '@/db/repositories/followers-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type DeleteFollowServiceInput = { followerId: string followedId: string } -const deleteFollowServiceImpl = async ({ +export async function deleteFollowService({ followedId, followerId, -}: DeleteFollowServiceInput) => { +}: DeleteFollowServiceInput) { const [deletedFollow] = await deleteFollow({ followedId, followerId }) return { follow: deletedFollow } } - -export const deleteFollowService = withServiceTracing( - 'delete-follow', - deleteFollowServiceImpl -) diff --git a/apps/backend/src/domain/services/follows/get-follow.ts b/apps/backend/src/domain/services/follows/get-follow.ts index 035a8421..8b0cd437 100644 --- a/apps/backend/src/domain/services/follows/get-follow.ts +++ b/apps/backend/src/domain/services/follows/get-follow.ts @@ -1,21 +1,15 @@ import { getFollow } from '@/db/repositories/followers-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetFollowServiceInput = { followerId: string followedId: string } -const getFollowServiceImpl = async ({ +export async function getFollowService({ followedId, followerId, -}: GetFollowServiceInput) => { +}: GetFollowServiceInput) { const [follow] = await getFollow({ followedId, followerId }) return { follow: follow || null } } - -export const getFollowService = withServiceTracing( - 'get-follow', - getFollowServiceImpl -) diff --git a/apps/backend/src/domain/services/follows/get-followers.ts b/apps/backend/src/domain/services/follows/get-followers.ts index 2c1183a0..45714eee 100644 --- a/apps/backend/src/domain/services/follows/get-followers.ts +++ b/apps/backend/src/domain/services/follows/get-followers.ts @@ -1,5 +1,4 @@ import { selectFollowers } from '@/db/repositories/followers-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetFollowersInput = { followedId?: string @@ -8,7 +7,7 @@ export type GetFollowersInput = { pageSize: number } -const getFollowersServiceImpl = async (input: GetFollowersInput) => { +export async function getFollowersService(input: GetFollowersInput) { const followers = await selectFollowers(input) return { @@ -16,8 +15,3 @@ const getFollowersServiceImpl = async (input: GetFollowersInput) => { nextCursor: followers[input.pageSize]?.createdAt.toISOString() || null, } } - -export const getFollowersService = withServiceTracing( - 'get-followers', - getFollowersServiceImpl -) diff --git a/apps/backend/src/domain/services/likes/create-like.ts b/apps/backend/src/domain/services/likes/create-like.ts index 7ef5fbd7..4acd6190 100644 --- a/apps/backend/src/domain/services/likes/create-like.ts +++ b/apps/backend/src/domain/services/likes/create-like.ts @@ -1,13 +1,7 @@ import { insertLike } from '@/db/repositories/likes-repository' import type { InsertLike } from '@/domain/entities/likes' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -const createLikeServiceImpl = async (values: InsertLike) => { +export async function createLikeService(values: InsertLike) { const [like] = await insertLike(values) return { like } } - -export const createLikeService = withServiceTracing( - 'create-like', - createLikeServiceImpl -) diff --git a/apps/backend/src/domain/services/likes/delete-like.ts b/apps/backend/src/domain/services/likes/delete-like.ts index 6e71a28d..7cecad15 100644 --- a/apps/backend/src/domain/services/likes/delete-like.ts +++ b/apps/backend/src/domain/services/likes/delete-like.ts @@ -1,13 +1,7 @@ import { deleteLike } from '@/db/repositories/likes-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -const deleteLikeServiceImpl = async (id: string) => { +export async function deleteLikeService(id: string) { const [like] = await deleteLike(id) return { like } } - -export const deleteLikeService = withServiceTracing( - 'delete-like', - deleteLikeServiceImpl -) diff --git a/apps/backend/src/domain/services/likes/get-likes.ts b/apps/backend/src/domain/services/likes/get-likes.ts index 0523f333..402ce86c 100644 --- a/apps/backend/src/domain/services/likes/get-likes.ts +++ b/apps/backend/src/domain/services/likes/get-likes.ts @@ -1,13 +1,7 @@ import { selectLikes } from '@/db/repositories/likes-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -const getLikesServiceImpl = async (entityId: string) => { +export async function getLikesService(entityId: string) { const likes = await selectLikes(entityId) return { likes } } - -export const getLikesService = withServiceTracing( - 'get-likes', - getLikesServiceImpl -) diff --git a/apps/backend/src/domain/services/list-item/create-list-item.ts b/apps/backend/src/domain/services/list-item/create-list-item.ts index f12cfa87..f65e645c 100644 --- a/apps/backend/src/domain/services/list-item/create-list-item.ts +++ b/apps/backend/src/domain/services/list-item/create-list-item.ts @@ -1,13 +1,12 @@ import { insertListItem } from '@/db/repositories/list-item-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import type { InsertListItem } from '../../entities/list-item' import { ListNotFoundError } from '../../errors/list-not-found-error' -const createListItemServiceImpl = async ( +export async function createListItemService( values: InsertListItem, _userId: string -) => { +) { try { const [listItem] = await insertListItem(values) @@ -20,8 +19,3 @@ const createListItemServiceImpl = async ( throw error } } - -export const createListItemService = withServiceTracing( - 'create-list-item', - createListItemServiceImpl -) diff --git a/apps/backend/src/domain/services/list-item/delete-list-item.ts b/apps/backend/src/domain/services/list-item/delete-list-item.ts index 22daf475..218f69e5 100644 --- a/apps/backend/src/domain/services/list-item/delete-list-item.ts +++ b/apps/backend/src/domain/services/list-item/delete-list-item.ts @@ -1,13 +1,12 @@ import { deleteListItem } from '@/db/repositories/list-item-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ListItemNotFoundError } from '@/domain/errors/list-item-not-found-error' type DeleteListItemInput = { id: string; userId: string } -const deleteListItemServiceImpl = async ({ +export async function deleteListItemService({ id, userId: _userId, -}: DeleteListItemInput) => { +}: DeleteListItemInput) { const [deletedListItem] = await deleteListItem(id) if (!deletedListItem) { @@ -16,8 +15,3 @@ const deleteListItemServiceImpl = async ({ return { deletedListItem } } - -export const deleteListItemService = withServiceTracing( - 'delete-list-item', - deleteListItemServiceImpl -) diff --git a/apps/backend/src/domain/services/list-item/get-list-items.ts b/apps/backend/src/domain/services/list-item/get-list-items.ts index e4ecb6b2..86825b2f 100644 --- a/apps/backend/src/domain/services/list-item/get-list-items.ts +++ b/apps/backend/src/domain/services/list-item/get-list-items.ts @@ -1,11 +1,10 @@ import { selectListItems } from '@/db/repositories/list-item-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { getListById } from '@/db/repositories/list-repository' import { ListNotFoundError } from '../../errors/list-not-found-error' type GetListItemsInput = { listId: string } -const getListItemsServiceImpl = async ({ listId }: GetListItemsInput) => { +export async function getListItemsService({ listId }: GetListItemsInput) { const [list] = await getListById(listId) if (!list) { @@ -16,8 +15,3 @@ const getListItemsServiceImpl = async ({ listId }: GetListItemsInput) => { return { listItems } } - -export const getListItemsService = withServiceTracing( - 'get-list-items', - getListItemsServiceImpl -) diff --git a/apps/backend/src/domain/services/list-item/update-list-items.ts b/apps/backend/src/domain/services/list-item/update-list-items.ts index 9ec32780..7b42ebbd 100644 --- a/apps/backend/src/domain/services/list-item/update-list-items.ts +++ b/apps/backend/src/domain/services/list-item/update-list-items.ts @@ -1,20 +1,14 @@ import { updateListItems } from '@/db/repositories/list-item-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type UpdateListItemsServiceInput = { listItems: Array<{ id: string; position: number }> } -const updateListItemsServiceImpl = async ( +export async function updateListItemsService( input: UpdateListItemsServiceInput -) => { +) { const result = await updateListItems(input) const listItems = result.flat() return { listItems } } - -export const updateListItemsService = withServiceTracing( - 'update-list-items', - updateListItemsServiceImpl -) diff --git a/apps/backend/src/domain/services/lists/create-list.ts b/apps/backend/src/domain/services/lists/create-list.ts index bfbd3be0..fe580f7d 100644 --- a/apps/backend/src/domain/services/lists/create-list.ts +++ b/apps/backend/src/domain/services/lists/create-list.ts @@ -1,18 +1,17 @@ import type { InferInsertModel } from 'drizzle-orm' import { insertList } from '@/db/repositories/list-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { schema } from '@/db/schema' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import { UserNotFoundError } from '../../errors/user-not-found' export type CreateListInput = InferInsertModel -const createListImpl = async ({ +export async function createList({ title, description, visibility = 'PUBLIC', userId, -}: CreateListInput) => { +}: CreateListInput) { try { const [list] = await insertList({ title, description, visibility, userId }) @@ -25,5 +24,3 @@ const createListImpl = async ({ throw error } } - -export const createList = withServiceTracing('create-list', createListImpl) diff --git a/apps/backend/src/domain/services/lists/delete-list.ts b/apps/backend/src/domain/services/lists/delete-list.ts index cfd2e49b..a226cd2b 100644 --- a/apps/backend/src/domain/services/lists/delete-list.ts +++ b/apps/backend/src/domain/services/lists/delete-list.ts @@ -1,15 +1,9 @@ import { deleteList } from '@/db/repositories/list-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' type DeleteListInput = { id: string; userId: string } -const deleteListServiceImpl = async ({ id }: DeleteListInput) => { +export async function deleteListService({ id }: DeleteListInput) { const [deletedList] = await deleteList(id) return deletedList } - -export const deleteListService = withServiceTracing( - 'delete-list', - deleteListServiceImpl -) diff --git a/apps/backend/src/domain/services/lists/get-list-progress.ts b/apps/backend/src/domain/services/lists/get-list-progress.ts index 885314a2..e7faeccf 100644 --- a/apps/backend/src/domain/services/lists/get-list-progress.ts +++ b/apps/backend/src/domain/services/lists/get-list-progress.ts @@ -1,5 +1,4 @@ import { selectListItems } from '@/db/repositories/list-item-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { selectAllUserItemsByStatus } from '@/db/repositories/user-item-repository' type GetListProgressServiceParams = { @@ -7,10 +6,10 @@ type GetListProgressServiceParams = { authenticatedUserId: string } -const getListProgressServiceImpl = async ({ +export async function getListProgressService({ id, authenticatedUserId, -}: GetListProgressServiceParams) => { +}: GetListProgressServiceParams) { const listItems = await selectListItems(id) if (listItems.length === 0) { return { @@ -41,8 +40,3 @@ const getListProgressServiceImpl = async ({ percentage, } } - -export const getListProgressService = withServiceTracing( - 'get-list-progress', - getListProgressServiceImpl -) diff --git a/apps/backend/src/domain/services/lists/get-list.ts b/apps/backend/src/domain/services/lists/get-list.ts index f750fb4c..fd3e10f8 100644 --- a/apps/backend/src/domain/services/lists/get-list.ts +++ b/apps/backend/src/domain/services/lists/get-list.ts @@ -1,5 +1,4 @@ import { getListById } from '@/db/repositories/list-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ListNotFoundError } from '../../errors/list-not-found-error' type GetListInput = { @@ -7,10 +6,10 @@ type GetListInput = { authenticatedUserId?: string } -const getListServiceImpl = async ({ +export async function getListService({ id, authenticatedUserId, -}: GetListInput) => { +}: GetListInput) { const [list] = await getListById(id, authenticatedUserId) if (!list) { @@ -19,5 +18,3 @@ const getListServiceImpl = async ({ return { list } } - -export const getListService = withServiceTracing('get-list', getListServiceImpl) diff --git a/apps/backend/src/domain/services/lists/get-lists.ts b/apps/backend/src/domain/services/lists/get-lists.ts index d7ed4d2d..3906d20b 100644 --- a/apps/backend/src/domain/services/lists/get-lists.ts +++ b/apps/backend/src/domain/services/lists/get-lists.ts @@ -1,5 +1,4 @@ import { selectLists } from '@/db/repositories/list-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetListsInput = { userId?: string @@ -9,13 +8,8 @@ export type GetListsInput = { hasBanner?: boolean } -const getListsServicesImpl = async (input: GetListsInput) => { +export async function getListsServices(input: GetListsInput) { const lists = await selectLists(input) return { lists } } - -export const getListsServices = withServiceTracing( - 'get-lists', - getListsServicesImpl -) diff --git a/apps/backend/src/domain/services/lists/update-list-banner.ts b/apps/backend/src/domain/services/lists/update-list-banner.ts index 2a16c5d9..89a9da6a 100644 --- a/apps/backend/src/domain/services/lists/update-list-banner.ts +++ b/apps/backend/src/domain/services/lists/update-list-banner.ts @@ -1,5 +1,4 @@ import { updateListBanner } from '@/db/repositories/list-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ListNotFoundError } from '@/domain/errors/list-not-found-error' export type UpdateListBannerInput = { @@ -8,7 +7,7 @@ export type UpdateListBannerInput = { userId: string } -const updateListBannerServiceImpl = async (input: UpdateListBannerInput) => { +export async function updateListBannerService(input: UpdateListBannerInput) { const [list] = await updateListBanner(input) if (!list) { @@ -17,8 +16,3 @@ const updateListBannerServiceImpl = async (input: UpdateListBannerInput) => { return { list } } - -export const updateListBannerService = withServiceTracing( - 'update-list-banner', - updateListBannerServiceImpl -) diff --git a/apps/backend/src/domain/services/lists/update-list.ts b/apps/backend/src/domain/services/lists/update-list.ts index a5b1bc35..24a42725 100644 --- a/apps/backend/src/domain/services/lists/update-list.ts +++ b/apps/backend/src/domain/services/lists/update-list.ts @@ -1,6 +1,5 @@ import type { InferInsertModel } from 'drizzle-orm' import { updateList } from '@/db/repositories/list-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { schema } from '@/db/schema' export type UpdateListValues = Omit< @@ -14,16 +13,11 @@ type UpdateListInput = { values: UpdateListValues } -const updateListServiceImpl = async ({ +export async function updateListService({ id, userId, values, -}: UpdateListInput) => { +}: UpdateListInput) { const [updatedList] = await updateList(id, userId, values) return { list: updatedList } } - -export const updateListService = withServiceTracing( - 'update-list', - updateListServiceImpl -) diff --git a/apps/backend/src/domain/services/login/login.ts b/apps/backend/src/domain/services/login/login.ts index 0d8a5a46..fce21756 100644 --- a/apps/backend/src/domain/services/login/login.ts +++ b/apps/backend/src/domain/services/login/login.ts @@ -1,6 +1,5 @@ import type { z } from 'zod' import { findUserByEmailOrUsername } from '@/db/repositories/login-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { InvalidPasswordError } from '@/domain/errors/invalid-password-error' import type { loginBodySchema } from '@/http/schemas/login' import { comparePassword } from '@/utils/password' @@ -10,7 +9,7 @@ import { sendMagicLinkEmailService } from '../magic-link/send-magic-link-email' type LoginInput = z.infer -const loginServiceImpl = async ({ login, password, url }: LoginInput) => { +export async function loginService({ login, password, url }: LoginInput) { const user = await findUserByEmailOrUsername(login) if (!user) { @@ -32,5 +31,3 @@ const loginServiceImpl = async ({ login, password, url }: LoginInput) => { const { password: removedPassword, ...formattedUser } = user return { user: formattedUser } } - -export const loginService = withServiceTracing('login', loginServiceImpl) diff --git a/apps/backend/src/domain/services/magic-link/generate-magic-link.ts b/apps/backend/src/domain/services/magic-link/generate-magic-link.ts index e055b792..953785ce 100644 --- a/apps/backend/src/domain/services/magic-link/generate-magic-link.ts +++ b/apps/backend/src/domain/services/magic-link/generate-magic-link.ts @@ -1,18 +1,12 @@ import { randomBytes } from 'node:crypto' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { insertMagicToken } from '@/db/repositories/magic-tokens' const FIFTEEN_MINUTES = new Date(Date.now() + 15 * 60000) -const generateMagicLinkTokenServiceImpl = async (userId: string) => { +export async function generateMagicLinkTokenService(userId: string) { const token = randomBytes(32).toString('hex') await insertMagicToken({ token, userId, expiresAt: FIFTEEN_MINUTES }) return { token } } - -export const generateMagicLinkTokenService = withServiceTracing( - 'generate-magic-link-token', - generateMagicLinkTokenServiceImpl -) diff --git a/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts b/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts index 89470243..df7292a1 100644 --- a/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts +++ b/apps/backend/src/domain/services/magic-link/send-magic-link-email.ts @@ -1,5 +1,4 @@ import { config } from '@/config' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { EmailMessage } from '@/domain/entities/email-message' import { emailServiceFactory } from '@/factories/resend-factory' @@ -9,11 +8,11 @@ type SendMagicLinkEmailServiceInput = { url?: string } -const sendMagicLinkEmailServiceImpl = async ({ +export async function sendMagicLinkEmailService({ email, token, url: _url, -}: SendMagicLinkEmailServiceInput) => { +}: SendMagicLinkEmailServiceInput) { const link = `${config.app.CLIENT_URL}/reset-password?token=${token}` const html = `Please use the following link to login and set your new password: Login` @@ -31,8 +30,3 @@ const sendMagicLinkEmailServiceImpl = async ({ await emailService.sendEmail(emailMessage) } - -export const sendMagicLinkEmailService = withServiceTracing( - 'send-magic-link-email', - sendMagicLinkEmailServiceImpl -) diff --git a/apps/backend/src/domain/services/review-replies/create-review-reply.ts b/apps/backend/src/domain/services/review-replies/create-review-reply.ts index 14952123..8b0f791c 100644 --- a/apps/backend/src/domain/services/review-replies/create-review-reply.ts +++ b/apps/backend/src/domain/services/review-replies/create-review-reply.ts @@ -1,5 +1,4 @@ import { insertReviewReply } from '@/db/repositories/review-replies-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { getPostgresError, isForeignKeyViolation, @@ -8,9 +7,9 @@ import type { InsertReviewReplyModel } from '@/domain/entities/review-reply' import { ReviewNotFoundError } from '@/domain/errors/review-not-found-error' import { UserNotFoundError } from '@/domain/errors/user-not-found' -const createReviewReplyServiceImpl = async ( +export async function createReviewReplyService( params: InsertReviewReplyModel -) => { +) { try { const [reviewReply] = await insertReviewReply(params) @@ -32,8 +31,3 @@ const createReviewReplyServiceImpl = async ( throw error } } - -export const createReviewReplyService = withServiceTracing( - 'create-review-reply', - createReviewReplyServiceImpl -) diff --git a/apps/backend/src/domain/services/reviews/create-review.ts b/apps/backend/src/domain/services/reviews/create-review.ts index b1fb18dc..aede592b 100644 --- a/apps/backend/src/domain/services/reviews/create-review.ts +++ b/apps/backend/src/domain/services/reviews/create-review.ts @@ -1,10 +1,9 @@ import { insertReview } from '@/db/repositories/reviews-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isForeignKeyViolation } from '@/db/utils/postgres-errors' import { UserNotFoundError } from '@/domain/errors/user-not-found' import type { InsertReviewModel } from '../../entities/review' -const createReviewServiceImpl = async (params: InsertReviewModel) => { +export async function createReviewService(params: InsertReviewModel) { try { const [review] = await insertReview(params) @@ -17,8 +16,3 @@ const createReviewServiceImpl = async (params: InsertReviewModel) => { throw error } } - -export const createReviewService = withServiceTracing( - 'create-review', - createReviewServiceImpl -) diff --git a/apps/backend/src/domain/services/reviews/delete-review.ts b/apps/backend/src/domain/services/reviews/delete-review.ts index 37a47678..8a1762ac 100644 --- a/apps/backend/src/domain/services/reviews/delete-review.ts +++ b/apps/backend/src/domain/services/reviews/delete-review.ts @@ -1,8 +1,7 @@ import { deleteReview } from '@/db/repositories/reviews-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { ReviewNotFoundError } from '@/domain/errors/review-not-found-error' -const deleteReviewServiceImpl = async (id: string) => { +export async function deleteReviewService(id: string) { const [review] = await deleteReview(id) if (!review) { @@ -11,8 +10,3 @@ const deleteReviewServiceImpl = async (id: string) => { return review } - -export const deleteReviewService = withServiceTracing( - 'delete-review', - deleteReviewServiceImpl -) diff --git a/apps/backend/src/domain/services/reviews/get-review.ts b/apps/backend/src/domain/services/reviews/get-review.ts index da031ef4..208f2ff3 100644 --- a/apps/backend/src/domain/services/reviews/get-review.ts +++ b/apps/backend/src/domain/services/reviews/get-review.ts @@ -1,5 +1,4 @@ import { selectReview } from '@/db/repositories/reviews-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { getReviewQuerySchema } from '@/http/schemas/reviews' export type GetReviewInput = { @@ -10,13 +9,8 @@ export type GetReviewInput = { episodeNumber?: number } -const getReviewServiceImpl = async (input: GetReviewInput) => { +export async function getReviewService(input: GetReviewInput) { const [review] = await selectReview(input) return { review: review || null } } - -export const getReviewService = withServiceTracing( - 'get-review', - getReviewServiceImpl -) diff --git a/apps/backend/src/domain/services/reviews/get-reviews.ts b/apps/backend/src/domain/services/reviews/get-reviews.ts index fb0cd293..d9c4a97b 100644 --- a/apps/backend/src/domain/services/reviews/get-reviews.ts +++ b/apps/backend/src/domain/services/reviews/get-reviews.ts @@ -7,7 +7,6 @@ import { startOfWeek, } from 'date-fns' import { selectReviews } from '@/db/repositories/reviews-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { getReviewsQuerySchema } from '@/http/schemas/reviews' export type GetReviewsServiceInput = Omit< @@ -54,14 +53,9 @@ function getIntervalDate(interval: GetReviewsServiceInput['interval']) { } } -const getReviewsServiceImpl = async (input: GetReviewsServiceInput) => { +export async function getReviewsService(input: GetReviewsServiceInput) { const { startDate, endDate } = getIntervalDate(input.interval) const reviews = await selectReviews({ ...input, startDate, endDate }) return { reviews } } - -export const getReviewsService = withServiceTracing( - 'get-reviews', - getReviewsServiceImpl -) diff --git a/apps/backend/src/domain/services/reviews/update-review.ts b/apps/backend/src/domain/services/reviews/update-review.ts index dfefa4db..adba9460 100644 --- a/apps/backend/src/domain/services/reviews/update-review.ts +++ b/apps/backend/src/domain/services/reviews/update-review.ts @@ -1,5 +1,4 @@ import { updateReview } from '@/db/repositories/reviews-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { reviewParamsSchema, updateReviewBodySchema, @@ -8,13 +7,8 @@ import type { export type UpdateReviewInput = typeof updateReviewBodySchema._type & typeof reviewParamsSchema._type -const updateReviewServiceImpl = async (input: UpdateReviewInput) => { +export async function updateReviewService(input: UpdateReviewInput) { const [review] = await updateReview(input) return { review } } - -export const updateReviewService = withServiceTracing( - 'update-review', - updateReviewServiceImpl -) diff --git a/apps/backend/src/domain/services/social-links/get-social-links.ts b/apps/backend/src/domain/services/social-links/get-social-links.ts index 2ff3b26f..6556e63a 100644 --- a/apps/backend/src/domain/services/social-links/get-social-links.ts +++ b/apps/backend/src/domain/services/social-links/get-social-links.ts @@ -1,17 +1,11 @@ import { selectSocialLinks } from '@/db/repositories/social-links-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' type Input = { userId: string } -const getSocialLinksServiceImpl = async ({ userId }: Input) => { +export async function getSocialLinksService({ userId }: Input) { const socialLinks = await selectSocialLinks(userId) return { socialLinks } } - -export const getSocialLinksService = withServiceTracing( - 'get-social-links', - getSocialLinksServiceImpl -) diff --git a/apps/backend/src/domain/services/social-links/upsert-social-links.ts b/apps/backend/src/domain/services/social-links/upsert-social-links.ts index 74bc0611..d47fa327 100644 --- a/apps/backend/src/domain/services/social-links/upsert-social-links.ts +++ b/apps/backend/src/domain/services/social-links/upsert-social-links.ts @@ -2,7 +2,6 @@ import { deleteSocialLink, insertSocialLink, } from '@/db/repositories/social-links-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { InsertSocialLink } from '@/domain/entities/social-link' import type { socialLinksBodySchema } from '@/http/schemas/social-links' @@ -11,7 +10,7 @@ type Input = { userId: string } -const upsertSocialLinksServiceImpl = async ({ userId, values }: Input) => { +export async function upsertSocialLinksService({ userId, values }: Input) { const updates = Object.entries(values).map(async ([platform, url]) => { if (url) { const teste = await insertSocialLink({ @@ -28,8 +27,3 @@ const upsertSocialLinksServiceImpl = async ({ userId, values }: Input) => { await Promise.all(updates) } - -export const upsertSocialLinksService = withServiceTracing( - 'upsert-social-links', - upsertSocialLinksServiceImpl -) diff --git a/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts b/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts index 85e90bcf..092e8944 100644 --- a/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts +++ b/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts @@ -1,6 +1,5 @@ import type { FastifyRedis } from '@fastify/redis' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { Language } from '@plotwist_app/tmdb' import { tmdb } from '@/adapters/tmdb' @@ -12,10 +11,10 @@ type GetTMDBDataServiceInput = { const THIRTY_DAYS_IN_SECONDS = 30 * 24 * 60 * 60 -const getTMDBDataServiceImpl = async ( +export async function getTMDBDataService( redis: FastifyRedis, input: GetTMDBDataServiceInput -) => { +) { const { mediaType, language, tmdbId } = input const cacheKey = `${mediaType}:${tmdbId}:${language}` @@ -66,8 +65,3 @@ const getTMDBDataServiceImpl = async ( backdropPath: data.backdrop_path, } } - -export const getTMDBDataService = withServiceTracing( - 'get-tmdb-data', - getTMDBDataServiceImpl -) diff --git a/apps/backend/src/domain/services/user-activities/create-user-activity.ts b/apps/backend/src/domain/services/user-activities/create-user-activity.ts index 78a1345c..3d6b29bc 100644 --- a/apps/backend/src/domain/services/user-activities/create-user-activity.ts +++ b/apps/backend/src/domain/services/user-activities/create-user-activity.ts @@ -1,12 +1,6 @@ import { insertUserActivity } from '@/db/repositories/user-activities' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { InsertUserActivity } from '@/domain/entities/user-activity' -const createUserActivityImpl = async (params: InsertUserActivity) => { +export async function createUserActivity(params: InsertUserActivity) { return await insertUserActivity(params) } - -export const createUserActivity = withServiceTracing( - 'create-user-activity', - createUserActivityImpl -) diff --git a/apps/backend/src/domain/services/user-items/get-user-items.ts b/apps/backend/src/domain/services/user-items/get-user-items.ts index 35f8d9c2..613a6693 100644 --- a/apps/backend/src/domain/services/user-items/get-user-items.ts +++ b/apps/backend/src/domain/services/user-items/get-user-items.ts @@ -1,8 +1,7 @@ import { selectUserItems } from '@/db/repositories/user-item-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { SelectUserItems } from '@/domain/entities/user-item' -const getUserItemsServiceImpl = async (input: SelectUserItems) => { +export async function getUserItemsService(input: SelectUserItems) { try { const userItems = await selectUserItems({ ...input }) @@ -17,8 +16,3 @@ const getUserItemsServiceImpl = async (input: SelectUserItems) => { throw error } } - -export const getUserItemsService = withServiceTracing( - 'get-user-items', - getUserItemsServiceImpl -) diff --git a/apps/backend/src/domain/services/user-items/upsert-user-item.ts b/apps/backend/src/domain/services/user-items/upsert-user-item.ts index 53c1bc4c..e82007f8 100644 --- a/apps/backend/src/domain/services/user-items/upsert-user-item.ts +++ b/apps/backend/src/domain/services/user-items/upsert-user-item.ts @@ -1,16 +1,10 @@ import * as changeKeys from 'change-case/keys' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { upsertUserItem } from '@/db/repositories/user-item-repository' import type { InsertUserItem, UserItem } from '@/domain/entities/user-item' -const upsertUserItemServiceImpl = async (values: InsertUserItem) => { +export async function upsertUserItemService(values: InsertUserItem) { const [userItem] = await upsertUserItem(values) return { userItem: changeKeys.camelCase(userItem) as UserItem } } - -export const upsertUserItemService = withServiceTracing( - 'upsert-user-item', - upsertUserItemServiceImpl -) diff --git a/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts b/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts index 5dc9da29..93e29b84 100644 --- a/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts +++ b/apps/backend/src/domain/services/user-preferences/get-user-preferences.ts @@ -1,19 +1,13 @@ import { selectUserPreferences } from '@/db/repositories/user-preferences' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' export type GetUserPreferencesParams = { userId: string } -const getUserPreferencesServiceImpl = async ({ +export async function getUserPreferencesService({ userId, -}: GetUserPreferencesParams) => { +}: GetUserPreferencesParams) { const [userPreferences] = await selectUserPreferences(userId) return { userPreferences: userPreferences ?? null } } - -export const getUserPreferencesService = withServiceTracing( - 'get-user-preferences', - getUserPreferencesServiceImpl -) diff --git a/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts b/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts index 4c2947a6..abf30236 100644 --- a/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts +++ b/apps/backend/src/domain/services/user-preferences/update-user-preferences.ts @@ -1,16 +1,10 @@ import { updateUserPreferences } from '@/db/repositories/user-preferences' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import type { UpdateUserPreferencesParams } from '@/domain/entities/user-preferences' -const updateUserPreferencesServiceImpl = async ( +export async function updateUserPreferencesService( params: UpdateUserPreferencesParams -) => { +) { const [userPreferences] = await updateUserPreferences(params) return { userPreferences } } - -export const updateUserPreferencesService = withServiceTracing( - 'update-user-preferences', - updateUserPreferencesServiceImpl -) diff --git a/apps/backend/src/domain/services/users/create-user.ts b/apps/backend/src/domain/services/users/create-user.ts index a7c667cd..6bfc39f9 100644 --- a/apps/backend/src/domain/services/users/create-user.ts +++ b/apps/backend/src/domain/services/users/create-user.ts @@ -1,5 +1,4 @@ import { insertUser } from '@/db/repositories/user-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isUniqueViolation } from '@/db/utils/postgres-errors' import { hashPassword } from '@/utils/password' import { EmailOrUsernameAlreadyRegisteredError } from '../../errors/email-or-username-already-registered-error' @@ -12,12 +11,12 @@ export type CreateUserInterface = { displayName?: string } -const createUserImpl = async ({ +export async function createUser({ username, email, password, displayName, -}: CreateUserInterface) => { +}: CreateUserInterface) { let hashedPassword: string try { @@ -45,5 +44,3 @@ const createUserImpl = async ({ throw error } } - -export const createUser = withServiceTracing('create-user', createUserImpl) diff --git a/apps/backend/src/domain/services/users/get-by-id.ts b/apps/backend/src/domain/services/users/get-by-id.ts index 859a4ebb..cdb44273 100644 --- a/apps/backend/src/domain/services/users/get-by-id.ts +++ b/apps/backend/src/domain/services/users/get-by-id.ts @@ -1,8 +1,7 @@ import { getUserById as getById } from '@/db/repositories/user-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { UserNotFoundError } from '../../errors/user-not-found' -const getUserByIdImpl = async (id: string) => { +export async function getUserById(id: string) { const [user] = await getById(id) if (!user) { @@ -11,5 +10,3 @@ const getUserByIdImpl = async (id: string) => { return { user } } - -export const getUserById = withServiceTracing('get-user-by-id', getUserByIdImpl) diff --git a/apps/backend/src/domain/services/users/get-user-by-username.ts b/apps/backend/src/domain/services/users/get-user-by-username.ts index 9df362df..fa9f8876 100644 --- a/apps/backend/src/domain/services/users/get-user-by-username.ts +++ b/apps/backend/src/domain/services/users/get-user-by-username.ts @@ -1,14 +1,13 @@ import { getUserByUsername as getByUsername } from '@/db/repositories/user-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { UserNotFoundError } from '../../errors/user-not-found' type GetUserByUsernameInput = { username: string } -const getUserByUsernameImpl = async ({ +export async function getUserByUsername({ username, -}: GetUserByUsernameInput) => { +}: GetUserByUsernameInput) { const [user] = await getByUsername(username) if (!user) { @@ -17,8 +16,3 @@ const getUserByUsernameImpl = async ({ return { user } } - -export const getUserByUsername = withServiceTracing( - 'get-user-by-username', - getUserByUsernameImpl -) diff --git a/apps/backend/src/domain/services/users/is-email-available.ts b/apps/backend/src/domain/services/users/is-email-available.ts index 45d5cee7..d245a4be 100644 --- a/apps/backend/src/domain/services/users/is-email-available.ts +++ b/apps/backend/src/domain/services/users/is-email-available.ts @@ -1,14 +1,13 @@ import { getUserByEmail } from '@/db/repositories/user-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { EmailAlreadyRegisteredError } from '../../errors/email-already-registered' type IsEmailAvailableInterface = { email: string } -const isEmailAvailableImpl = async ({ +export async function isEmailAvailable({ email, -}: IsEmailAvailableInterface) => { +}: IsEmailAvailableInterface) { const [user] = await getUserByEmail(email) if (user) { @@ -17,8 +16,3 @@ const isEmailAvailableImpl = async ({ return { available: true } } - -export const isEmailAvailable = withServiceTracing( - 'is-email-available', - isEmailAvailableImpl -) diff --git a/apps/backend/src/domain/services/users/is-username-available.ts b/apps/backend/src/domain/services/users/is-username-available.ts index e7a3255b..a71e7170 100644 --- a/apps/backend/src/domain/services/users/is-username-available.ts +++ b/apps/backend/src/domain/services/users/is-username-available.ts @@ -1,14 +1,13 @@ import { getUserByUsername } from '@/db/repositories/user-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { UsernameAlreadyRegisteredError } from '../../errors/username-already-registered' interface IsUsernameAvailableInterface { username: string } -const checkAvailableUsernameImpl = async ({ +export async function checkAvailableUsername({ username, -}: IsUsernameAvailableInterface) => { +}: IsUsernameAvailableInterface) { const [user] = await getUserByUsername(username) if (user) { @@ -17,8 +16,3 @@ const checkAvailableUsernameImpl = async ({ return { available: true } } - -export const checkAvailableUsername = withServiceTracing( - 'check-username-available', - checkAvailableUsernameImpl -) diff --git a/apps/backend/src/domain/services/users/search-users-by-username.ts b/apps/backend/src/domain/services/users/search-users-by-username.ts index 3dff1002..1de9526c 100644 --- a/apps/backend/src/domain/services/users/search-users-by-username.ts +++ b/apps/backend/src/domain/services/users/search-users-by-username.ts @@ -1,13 +1,7 @@ import { listUsersByUsernameLike } from '@/db/repositories/user-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' -const searchUsersByUsernameImpl = async (username: string) => { +export async function searchUsersByUsername(username: string) { const users = await listUsersByUsernameLike(username) return users } - -export const searchUsersByUsername = withServiceTracing( - 'search-users-by-username', - searchUsersByUsernameImpl -) diff --git a/apps/backend/src/domain/services/users/update-user-password.ts b/apps/backend/src/domain/services/users/update-user-password.ts index a9d3217d..c7853f37 100644 --- a/apps/backend/src/domain/services/users/update-user-password.ts +++ b/apps/backend/src/domain/services/users/update-user-password.ts @@ -2,7 +2,6 @@ import { invalidateMagicToken, selectMagicToken, } from '@/db/repositories/magic-tokens' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { updateUserPassword } from '@/db/repositories/user-repository' import { InvalidTokenError } from '@/domain/errors/invalid-token-error' import type { updateUserPasswordBodySchema } from '@/http/schemas/users' @@ -10,10 +9,10 @@ import { hashPassword } from '@/utils/password' type UpdatePasswordInput = typeof updateUserPasswordBodySchema._type -const updatePasswordServiceImpl = async ({ +export async function updatePasswordService({ password, token, -}: UpdatePasswordInput) => { +}: UpdatePasswordInput) { const [tokenRecord] = await selectMagicToken(token) if (!token) { @@ -26,8 +25,3 @@ const updatePasswordServiceImpl = async ({ return { status: 'password_set' } } - -export const updatePasswordService = withServiceTracing( - 'update-password', - updatePasswordServiceImpl -) diff --git a/apps/backend/src/domain/services/users/update-user.ts b/apps/backend/src/domain/services/users/update-user.ts index 6e735d91..57a1f073 100644 --- a/apps/backend/src/domain/services/users/update-user.ts +++ b/apps/backend/src/domain/services/users/update-user.ts @@ -1,19 +1,19 @@ +import type { z } from 'zod' import { getUserById, updateUser } from '@/db/repositories/user-repository' -import { withServiceTracing } from '@/infra/telemetry/with-service-tracing' import { isUniqueViolation } from '@/db/utils/postgres-errors' import { NoValidFieldsError } from '@/domain/errors/no-valid-fields' import { UserNotFoundError } from '@/domain/errors/user-not-found' import { UsernameAlreadyRegisteredError } from '@/domain/errors/username-already-registered' import type { updateUserBodySchema } from '@/http/schemas/users' -export type UpdateUserInput = typeof updateUserBodySchema._type +export type UpdateUserInput = z.infer -const updateUserServiceImpl = async ({ +export async function updateUserService({ userId, ...data }: UpdateUserInput & { userId: string -}) => { +}) { const validData = Object.fromEntries( Object.entries(data).filter(([_, value]) => value !== undefined) ) @@ -45,8 +45,3 @@ const updateUserServiceImpl = async ({ throw error } } - -export const updateUserService = withServiceTracing( - 'update-user', - updateUserServiceImpl -) From 1336294ab13a8e733f5348c4de81e99c3d87d1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Wed, 18 Feb 2026 23:31:02 -0300 Subject: [PATCH 10/26] refactor: remove telemetry tracing from adapter functions and expose core functionalities directly --- apps/backend/src/adapters/my-anime-list.ts | 11 +---- apps/backend/src/adapters/open-ai.ts | 3 +- apps/backend/src/adapters/r2-storage.ts | 5 +- apps/backend/src/adapters/resend.ts | 3 +- apps/backend/src/adapters/sqs.ts | 11 ++--- .../infra/telemetry/with-adapter-tracing.ts | 26 ----------- .../src/infra/telemetry/with-db-tracing.ts | 26 ----------- .../infra/telemetry/with-service-tracing.ts | 26 ----------- .../src/infra/telemetry/with-tracing.ts | 46 ------------------- 9 files changed, 10 insertions(+), 147 deletions(-) delete mode 100644 apps/backend/src/infra/telemetry/with-adapter-tracing.ts delete mode 100644 apps/backend/src/infra/telemetry/with-db-tracing.ts delete mode 100644 apps/backend/src/infra/telemetry/with-service-tracing.ts delete mode 100644 apps/backend/src/infra/telemetry/with-tracing.ts diff --git a/apps/backend/src/adapters/my-anime-list.ts b/apps/backend/src/adapters/my-anime-list.ts index efa9b870..a9524c35 100644 --- a/apps/backend/src/adapters/my-anime-list.ts +++ b/apps/backend/src/adapters/my-anime-list.ts @@ -1,11 +1,10 @@ import axios from 'axios' import type { AnimeDetails } from '@/@types/my-anime-list-request' import { config } from '@/config' -import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' const BASE_URL = 'https://api.myanimelist.net/v2' -async function searchAnimeImpl(query: string) { +export async function searchAnime(query: string) { try { const response = await axios.get(`${BASE_URL}/anime`, { params: { q: query, limit: 5 }, @@ -19,7 +18,7 @@ async function searchAnimeImpl(query: string) { } } -async function searchAnimeByIdImpl(animedbId: string) { +export async function searchAnimeById(animedbId: string) { try { const response = await axios.get(`${BASE_URL}/anime/${animedbId}`, { params: { @@ -37,9 +36,3 @@ async function searchAnimeByIdImpl(animedbId: string) { ) } } - -export const searchAnime = withAdapterTracing('mal-search-anime', searchAnimeImpl) -export const searchAnimeById = withAdapterTracing( - 'mal-search-anime-by-id', - searchAnimeByIdImpl -) diff --git a/apps/backend/src/adapters/open-ai.ts b/apps/backend/src/adapters/open-ai.ts index 90b233ff..e64ca971 100644 --- a/apps/backend/src/adapters/open-ai.ts +++ b/apps/backend/src/adapters/open-ai.ts @@ -1,6 +1,5 @@ import OpenAI from 'openai' import { config } from '@/config' -import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { AIService } from '@/ports/ai-service' const openai = new OpenAI({ @@ -28,7 +27,7 @@ async function generateMessage(prompt: string, content: string) { } const OpenAIService: AIService = { - generateMessage: withAdapterTracing('openai-generate-message', generateMessage), + generateMessage: (prefix, content) => generateMessage(prefix, content), } export { OpenAIService } diff --git a/apps/backend/src/adapters/r2-storage.ts b/apps/backend/src/adapters/r2-storage.ts index b6af0cff..d712c7a3 100644 --- a/apps/backend/src/adapters/r2-storage.ts +++ b/apps/backend/src/adapters/r2-storage.ts @@ -6,7 +6,6 @@ import { import { Upload } from '@aws-sdk/lib-storage' import type { UploadImageInput } from '@/@types/r2-storage' import { config } from '@/config' -import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { CloudStorage } from '@/ports/cloud-storage' const r2Storage = new S3Client({ @@ -76,8 +75,8 @@ async function uploadImage({ } const R2Storage: CloudStorage = { - deleteOldImages: withAdapterTracing('r2-delete-old-images', deleteOldImages), - uploadImage: withAdapterTracing('r2-upload-image', uploadImage), + deleteOldImages: prefix => deleteOldImages(prefix), + uploadImage: input => uploadImage(input), } export { R2Storage } diff --git a/apps/backend/src/adapters/resend.ts b/apps/backend/src/adapters/resend.ts index ad74a057..b91c7f2a 100644 --- a/apps/backend/src/adapters/resend.ts +++ b/apps/backend/src/adapters/resend.ts @@ -1,7 +1,6 @@ import { Resend } from 'resend' import { config } from '@/config' import type { EmailMessage } from '@/domain/entities/email-message' -import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { EmailService } from '@/ports/email-service' const resend = new Resend(config.services.RESEND_API_KEY) @@ -16,7 +15,7 @@ async function sendEmail(emailMessage: EmailMessage) { } const ResendAdapter: EmailService = { - sendEmail: withAdapterTracing('resend-send-email', sendEmail), + sendEmail: emailMessage => sendEmail(emailMessage), } export { ResendAdapter } diff --git a/apps/backend/src/adapters/sqs.ts b/apps/backend/src/adapters/sqs.ts index d5d3b902..b3d62926 100644 --- a/apps/backend/src/adapters/sqs.ts +++ b/apps/backend/src/adapters/sqs.ts @@ -8,7 +8,6 @@ import { } from '@aws-sdk/client-sqs' import { config } from '@/config' import type { QueueMessage } from '@/domain/entities/queue-message' -import { withAdapterTracing } from '@/infra/telemetry/with-adapter-tracing' import type { QueueService } from '@/ports/queue-service' import { logger } from './logger' @@ -119,12 +118,10 @@ async function deleteMessage(queueUrl: string, receiptHandle: string) { } const SQSAdapter: QueueService = { - publish: withAdapterTracing('sqs-publish', publish), - receiveMessage: withAdapterTracing('sqs-receive-message', receiveMessage), - initialize: withAdapterTracing('sqs-initialize', () => - initializeSQS(createSqsClient()) - ), - deleteMessage: withAdapterTracing('sqs-delete-message', deleteMessage), + publish: queueMessage => publish(queueMessage), + receiveMessage: queueUrl => receiveMessage(queueUrl), + initialize: () => initializeSQS(createSqsClient()), + deleteMessage: (queueUrl, receiptHandle) => deleteMessage(queueUrl, receiptHandle), } export { SQSAdapter } diff --git a/apps/backend/src/infra/telemetry/with-adapter-tracing.ts b/apps/backend/src/infra/telemetry/with-adapter-tracing.ts deleted file mode 100644 index 2f403893..00000000 --- a/apps/backend/src/infra/telemetry/with-adapter-tracing.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { trace } from '@opentelemetry/api' - -// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any adapter signature -export function withAdapterTracing any>( - spanName: string, - fn: T -): T { - const tracer = trace.getTracer('plotwist-api', '0.1.0') - const fullSpanName = spanName.endsWith('-adapter') - ? spanName - : `${spanName}-adapter` - - return (async (...args: Parameters) => { - return tracer.startActiveSpan(fullSpanName, async span => { - try { - const result = await fn(...args) - return result - } catch (err) { - span.recordException(err as Error) - throw err - } finally { - span.end() - } - }) - }) as T -} diff --git a/apps/backend/src/infra/telemetry/with-db-tracing.ts b/apps/backend/src/infra/telemetry/with-db-tracing.ts deleted file mode 100644 index 83b94eaa..00000000 --- a/apps/backend/src/infra/telemetry/with-db-tracing.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { trace } from '@opentelemetry/api' - -// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any repository signature -export function withDbTracing any>( - spanName: string, - fn: T -): T { - const tracer = trace.getTracer('plotwist-api', '0.1.0') - const fullSpanName = spanName.endsWith('-repository') - ? spanName - : `${spanName}-repository` - - return (async (...args: Parameters) => { - return tracer.startActiveSpan(fullSpanName, async span => { - try { - const result = await fn(...args) - return result - } catch (err) { - span.recordException(err as Error) - throw err - } finally { - span.end() - } - }) - }) as T -} diff --git a/apps/backend/src/infra/telemetry/with-service-tracing.ts b/apps/backend/src/infra/telemetry/with-service-tracing.ts deleted file mode 100644 index a236f25d..00000000 --- a/apps/backend/src/infra/telemetry/with-service-tracing.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { trace } from '@opentelemetry/api' - -// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any service signature -export function withServiceTracing any>( - spanName: string, - fn: T -): T { - const tracer = trace.getTracer('plotwist-api', '0.1.0') - const fullSpanName = spanName.endsWith('-service') - ? spanName - : `${spanName}-service` - - return (async (...args: Parameters) => { - return tracer.startActiveSpan(fullSpanName, async span => { - try { - const result = await fn(...args) - return result - } catch (err) { - span.recordException(err as Error) - throw err - } finally { - span.end() - } - }) - }) as T -} diff --git a/apps/backend/src/infra/telemetry/with-tracing.ts b/apps/backend/src/infra/telemetry/with-tracing.ts deleted file mode 100644 index 9ce2c167..00000000 --- a/apps/backend/src/infra/telemetry/with-tracing.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { SpanStatusCode, trace } from '@opentelemetry/api' -import type { FastifyRequest } from 'fastify' - -// biome-ignore lint/suspicious/noExplicitAny: generic HOF must accept any handler signature -export function withTracing any>( - spanName: string, - handler: T, - options?: { method?: string; url?: string } -): T { - const tracer = trace.getTracer('plotwist-api', '0.1.0') - const fullSpanName = spanName.endsWith('-controller') - ? spanName - : `${spanName}-controller` - - return (async (...args: Parameters) => { - return tracer.startActiveSpan(fullSpanName, async span => { - try { - const request = args[0] as FastifyRequest | undefined - if (request) { - span.setAttribute('http.method', options?.method ?? request.method) - span.setAttribute( - 'http.url', - options?.url ?? request.routeOptions?.url ?? request.url ?? '' - ) - } - - const result = await handler(...args) - span.setStatus({ code: SpanStatusCode.OK }) - span.setAttribute('http.status_code', 200) - span.setAttribute('http.response.status', 'ok') - return result - } catch (err) { - span.recordException(err as Error) - span.setStatus({ - code: SpanStatusCode.ERROR, - message: (err as Error).message, - }) - span.setAttribute('http.status_code', 500) - span.setAttribute('http.response.status', 'error') - throw err - } finally { - span.end() - } - }) - }) as T -} From 71c47fb0ea2bb1e005d1b45e0f915d05cfe6c7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Thu, 19 Feb 2026 00:12:45 -0300 Subject: [PATCH 11/26] feat: add @kubiks/otel-drizzle for enhanced OpenTelemetry integration in database operations --- apps/backend/package.json | 1 + apps/backend/src/db/index.ts | 3 +++ apps/backend/src/http/routes/tmdb-proxy.ts | 2 +- pnpm-lock.yaml | 15 +++++++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index f9182ed8..d4888bf5 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -37,6 +37,7 @@ "@fastify/redis": "^7.1.0", "@fastify/swagger": "^9.6.1", "@fastify/swagger-ui": "^5.2.4", + "@kubiks/otel-drizzle": "^2.1.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.211.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.211.0", diff --git a/apps/backend/src/db/index.ts b/apps/backend/src/db/index.ts index d809c4a9..a767a91e 100644 --- a/apps/backend/src/db/index.ts +++ b/apps/backend/src/db/index.ts @@ -1,3 +1,4 @@ +import { instrumentDrizzleClient } from '@kubiks/otel-drizzle' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import { config } from '@/config' @@ -6,3 +7,5 @@ import * as schema from './schema' export const client = postgres(config.db.DATABASE_URL) export const db = drizzle(client, { schema }) + +instrumentDrizzleClient(db, { dbSystem: 'postgresql' }) diff --git a/apps/backend/src/http/routes/tmdb-proxy.ts b/apps/backend/src/http/routes/tmdb-proxy.ts index c06f5253..457141cc 100644 --- a/apps/backend/src/http/routes/tmdb-proxy.ts +++ b/apps/backend/src/http/routes/tmdb-proxy.ts @@ -153,7 +153,7 @@ export async function tmdbProxyRoutes(app: FastifyInstance) { reply.header('X-Cache', 'MISS') reply.header('Content-Type', 'application/json') return reply.send(JSON.parse(tmdbResponse.body)) - }), + }, }) ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71191356..b743f71d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@fastify/swagger-ui': specifier: ^5.2.4 version: 5.2.5 + '@kubiks/otel-drizzle': + specifier: ^2.1.0 + version: 2.1.0(@opentelemetry/api@1.9.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8)) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 @@ -2485,6 +2488,13 @@ packages: peerDependencies: jsep: ^0.4.0||^1.0.0 + '@kubiks/otel-drizzle@2.1.0': + resolution: {integrity: sha512-9UHb0od3jwa6zTWMyEYPIZcUq5PDaziCmQLMLakSK2zeqy12SFZ3SAGWXJTgEr8valn/Wa+DKVs+Z3aqKQUpvg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <2.0.0' + drizzle-orm: '>=0.28.0' + '@lukeed/ms@2.0.2': resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} @@ -11150,6 +11160,11 @@ snapshots: dependencies: jsep: 1.4.0 + '@kubiks/otel-drizzle@2.1.0(@opentelemetry/api@1.9.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8))': + dependencies: + '@opentelemetry/api': 1.9.0 + drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(postgres@3.4.8) + '@lukeed/ms@2.0.2': {} '@mdx-js/loader@3.1.1': From 77ff92e8eed938e339f3f9ad03a2c079827dee45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Thu, 19 Feb 2026 00:13:28 -0300 Subject: [PATCH 12/26] style: format code for improved readability across multiple files --- apps/backend/src/adapters/sqs.ts | 3 ++- apps/backend/src/db/repositories/subscription-repository.ts | 4 +--- apps/backend/src/db/repositories/user-item-repository.ts | 4 +++- .../src/domain/services/review-replies/create-review-reply.ts | 4 +--- .../backend/src/domain/services/users/get-user-by-username.ts | 4 +--- apps/backend/src/domain/services/users/is-email-available.ts | 4 +--- apps/backend/src/http/server.ts | 2 +- apps/backend/src/infra/telemetry/otel.ts | 2 +- apps/web/next-env.d.ts | 2 +- 9 files changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/backend/src/adapters/sqs.ts b/apps/backend/src/adapters/sqs.ts index b3d62926..1e5aec46 100644 --- a/apps/backend/src/adapters/sqs.ts +++ b/apps/backend/src/adapters/sqs.ts @@ -121,7 +121,8 @@ const SQSAdapter: QueueService = { publish: queueMessage => publish(queueMessage), receiveMessage: queueUrl => receiveMessage(queueUrl), initialize: () => initializeSQS(createSqsClient()), - deleteMessage: (queueUrl, receiptHandle) => deleteMessage(queueUrl, receiptHandle), + deleteMessage: (queueUrl, receiptHandle) => + deleteMessage(queueUrl, receiptHandle), } export { SQSAdapter } diff --git a/apps/backend/src/db/repositories/subscription-repository.ts b/apps/backend/src/db/repositories/subscription-repository.ts index aa30029f..771b6444 100644 --- a/apps/backend/src/db/repositories/subscription-repository.ts +++ b/apps/backend/src/db/repositories/subscription-repository.ts @@ -30,9 +30,7 @@ export type CancelSubscriptionParams = { cancellationReason: string | undefined } -export async function cancelUserSubscription( - params: CancelSubscriptionParams -) { +export async function cancelUserSubscription(params: CancelSubscriptionParams) { const [subscription] = await db .update(schema.subscriptions) .set({ diff --git a/apps/backend/src/db/repositories/user-item-repository.ts b/apps/backend/src/db/repositories/user-item-repository.ts index ddc3825b..d7c65cba 100644 --- a/apps/backend/src/db/repositories/user-item-repository.ts +++ b/apps/backend/src/db/repositories/user-item-repository.ts @@ -153,7 +153,9 @@ export async function selectAllUserItemsByStatus({ status, userId, }: SelectAllUserItems) { - const { id, tmdbId, mediaType, position, updatedAt } = getTableColumns(schema.userItems) + const { id, tmdbId, mediaType, position, updatedAt } = getTableColumns( + schema.userItems + ) const whereConditions = [eq(schema.userItems.userId, userId)] diff --git a/apps/backend/src/domain/services/review-replies/create-review-reply.ts b/apps/backend/src/domain/services/review-replies/create-review-reply.ts index 8b0f791c..c9f09649 100644 --- a/apps/backend/src/domain/services/review-replies/create-review-reply.ts +++ b/apps/backend/src/domain/services/review-replies/create-review-reply.ts @@ -7,9 +7,7 @@ import type { InsertReviewReplyModel } from '@/domain/entities/review-reply' import { ReviewNotFoundError } from '@/domain/errors/review-not-found-error' import { UserNotFoundError } from '@/domain/errors/user-not-found' -export async function createReviewReplyService( - params: InsertReviewReplyModel -) { +export async function createReviewReplyService(params: InsertReviewReplyModel) { try { const [reviewReply] = await insertReviewReply(params) diff --git a/apps/backend/src/domain/services/users/get-user-by-username.ts b/apps/backend/src/domain/services/users/get-user-by-username.ts index fa9f8876..3afdc66b 100644 --- a/apps/backend/src/domain/services/users/get-user-by-username.ts +++ b/apps/backend/src/domain/services/users/get-user-by-username.ts @@ -5,9 +5,7 @@ type GetUserByUsernameInput = { username: string } -export async function getUserByUsername({ - username, -}: GetUserByUsernameInput) { +export async function getUserByUsername({ username }: GetUserByUsernameInput) { const [user] = await getByUsername(username) if (!user) { diff --git a/apps/backend/src/domain/services/users/is-email-available.ts b/apps/backend/src/domain/services/users/is-email-available.ts index d245a4be..4051ee8a 100644 --- a/apps/backend/src/domain/services/users/is-email-available.ts +++ b/apps/backend/src/domain/services/users/is-email-available.ts @@ -5,9 +5,7 @@ type IsEmailAvailableInterface = { email: string } -export async function isEmailAvailable({ - email, -}: IsEmailAvailableInterface) { +export async function isEmailAvailable({ email }: IsEmailAvailableInterface) { const [user] = await getUserByEmail(email) if (user) { diff --git a/apps/backend/src/http/server.ts b/apps/backend/src/http/server.ts index 96320768..0490cacb 100644 --- a/apps/backend/src/http/server.ts +++ b/apps/backend/src/http/server.ts @@ -8,8 +8,8 @@ import { import { ZodError } from 'zod' import { logger } from '@/adapters/logger' import { DomainError } from '@/domain/errors/domain-error' -import { fastifyOtel } from '@/infra/telemetry/otel' import { registerHttpRequestMetrics } from '@/infra/telemetry/http-request-metrics' +import { fastifyOtel } from '@/infra/telemetry/otel' import { config } from '../config' import { routes } from './routes' import { transformSwaggerSchema } from './transform-schema' diff --git a/apps/backend/src/infra/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts index 38d10850..496c4efe 100644 --- a/apps/backend/src/infra/telemetry/otel.ts +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -1,5 +1,5 @@ -import { trace } from '@opentelemetry/api' import FastifyOtel from '@fastify/otel' +import { trace } from '@opentelemetry/api' import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' import { resourceFromAttributes } from '@opentelemetry/resources' diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index c4b7818f..a3e4680c 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import './.next/dev/types/routes.d.ts' // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From 771f741f9bd7cf9f93cd4eb14a64df5ed52a4024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Thu, 19 Feb 2026 00:24:58 -0300 Subject: [PATCH 13/26] refactor: consolidate user repository imports in delete-user service --- apps/backend/src/domain/services/users/delete-user.ts | 6 ++++-- apps/backend/src/http/controllers/user-controller.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/domain/services/users/delete-user.ts b/apps/backend/src/domain/services/users/delete-user.ts index d584df38..1af54e6f 100644 --- a/apps/backend/src/domain/services/users/delete-user.ts +++ b/apps/backend/src/domain/services/users/delete-user.ts @@ -1,5 +1,7 @@ -import { deleteUser as deleteUserFromDb } from '@/db/repositories/user-repository' -import { getUserById } from '@/db/repositories/user-repository' +import { + deleteUser as deleteUserFromDb, + getUserById, +} from '@/db/repositories/user-repository' import { UserNotFoundError } from '@/domain/errors/user-not-found' export async function deleteUserService(userId: string) { diff --git a/apps/backend/src/http/controllers/user-controller.ts b/apps/backend/src/http/controllers/user-controller.ts index f9562b1e..dcccbe12 100644 --- a/apps/backend/src/http/controllers/user-controller.ts +++ b/apps/backend/src/http/controllers/user-controller.ts @@ -4,12 +4,12 @@ import { createUserActivity } from '@/domain/services/user-activities/create-use import { getUserPreferencesService } from '@/domain/services/user-preferences/get-user-preferences' import { updateUserPreferencesService } from '@/domain/services/user-preferences/update-user-preferences' import { createUser } from '@/domain/services/users/create-user' +import { deleteUserService } from '@/domain/services/users/delete-user' import { getUserById } from '@/domain/services/users/get-by-id' import { getUserByUsername } from '@/domain/services/users/get-user-by-username' import { isEmailAvailable } from '@/domain/services/users/is-email-available' import { checkAvailableUsername } from '@/domain/services/users/is-username-available' import { searchUsersByUsername } from '@/domain/services/users/search-users-by-username' -import { deleteUserService } from '@/domain/services/users/delete-user' import { updateUserService } from '@/domain/services/users/update-user' import { updatePasswordService } from '@/domain/services/users/update-user-password' import { From ab5fc880a6f672cdeca3953160c7462af43974e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Thu, 19 Feb 2026 00:38:43 -0300 Subject: [PATCH 14/26] test: update user total hours test to use matchObject for improved assertion accuracy --- .../src/domain/services/user-stats/get-user-total-hours.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/domain/services/user-stats/get-user-total-hours.spec.ts b/apps/backend/src/domain/services/user-stats/get-user-total-hours.spec.ts index e4ab1c2e..048b5211 100644 --- a/apps/backend/src/domain/services/user-stats/get-user-total-hours.spec.ts +++ b/apps/backend/src/domain/services/user-stats/get-user-total-hours.spec.ts @@ -126,7 +126,7 @@ describe('get user total hours count', () => { const sut = await getUserTotalHoursService(user.id, redisClient) - expect(sut).toEqual({ + expect(sut).toMatchObject({ totalHours: CHERNOBYL.runtime + INCEPTION.runtime, }) }) From 39521d67dfbec58a28a62841328d943d7244bcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Thu, 19 Feb 2026 17:27:07 -0300 Subject: [PATCH 15/26] chore: prepare to receive monitors --- apps/backend/src/main.ts | 8 ++++++-- apps/backend/src/monitors/monitor.ts | 3 +++ apps/backend/src/{ => workers}/worker.ts | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 apps/backend/src/monitors/monitor.ts rename apps/backend/src/{ => workers}/worker.ts (76%) diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index d73b357c..ee48746e 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,8 +1,12 @@ -import { startServer } from './infra/http/server' -import { startWorkers } from './worker' +import '@/infra/telemetry/otel' + +import { startServer } from '@/infra/http/server' +import { startWorkers } from '@/workers/worker' +import { startMonitors } from '@/monitors/monitor' async function main() { startWorkers() + startMonitors() await startServer() } diff --git a/apps/backend/src/monitors/monitor.ts b/apps/backend/src/monitors/monitor.ts new file mode 100644 index 00000000..111efee8 --- /dev/null +++ b/apps/backend/src/monitors/monitor.ts @@ -0,0 +1,3 @@ +export function startMonitors() { + +} \ No newline at end of file diff --git a/apps/backend/src/worker.ts b/apps/backend/src/workers/worker.ts similarity index 76% rename from apps/backend/src/worker.ts rename to apps/backend/src/workers/worker.ts index 10012f9c..4bf73a34 100644 --- a/apps/backend/src/worker.ts +++ b/apps/backend/src/workers/worker.ts @@ -1,7 +1,7 @@ -import { config } from './config' -import { createSqsClient, initializeSQS } from './infra/adapters/sqs' -import { startMovieConsumer } from './infra/consumers/movies-consumer' -import { startSeriesConsumer } from './infra/consumers/series-consumer' +import { config } from '@/config' +import { createSqsClient, initializeSQS } from '@/infra/adapters/sqs' +import { startMovieConsumer } from '@/infra/consumers/movies-consumer' +import { startSeriesConsumer } from '@/infra/consumers/series-consumer' export async function startWorkers() { startSQS() From f026279a2502704fce3820b47f093567742edf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Sun, 22 Feb 2026 13:44:28 -0300 Subject: [PATCH 16/26] feat: implement monitoring functionality with cron jobs --- apps/backend/src/config.ts | 11 +++++++++++ apps/backend/src/monitors/monitor.ts | 23 ++++++++++++++++++++--- apps/backend/src/monitors/total_users.ts | 11 +++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 apps/backend/src/monitors/total_users.ts diff --git a/apps/backend/src/config.ts b/apps/backend/src/config.ts index 0637da96..c8cef43e 100644 --- a/apps/backend/src/config.ts +++ b/apps/backend/src/config.ts @@ -12,6 +12,7 @@ export const config = { myAnimeList: loadMALEnvs(), openai: loadOpenAIEnvs(), google: loadGoogleEnvs(), + monitors: loadMonitorsEnvs(), } function loadRedisEnvs() { @@ -121,3 +122,13 @@ function loadGoogleEnvs() { return schema.parse(process.env) } + +function loadMonitorsEnvs() { + const schema = z.object({ + ENABLE_MONITORS: z.string().default('false'), + MONITOR_CRON_TIME: z.string().default('0 0 * * *'), + + }) + + return schema.parse(process.env) +} \ No newline at end of file diff --git a/apps/backend/src/monitors/monitor.ts b/apps/backend/src/monitors/monitor.ts index 111efee8..8b6add66 100644 --- a/apps/backend/src/monitors/monitor.ts +++ b/apps/backend/src/monitors/monitor.ts @@ -1,3 +1,20 @@ -export function startMonitors() { - -} \ No newline at end of file +import cron from 'node-cron' +import { config } from '@/config' +import { logger } from '@/infra/adapters/logger' +import { monitorTotalUsers } from './total_users' + +export function startMonitors() { + if (config.monitors.ENABLE_MONITORS === 'false') { + return + } + + const cronTime = config.monitors.MONITOR_CRON_TIME + logger.info('Monitors started') + + cron.schedule(cronTime, () => { + logger.info('Monitoring total users') + void monitorTotalUsers().catch(err => { + logger.error('Monitor total users failed:', err) + }) + }) +} diff --git a/apps/backend/src/monitors/total_users.ts b/apps/backend/src/monitors/total_users.ts new file mode 100644 index 00000000..e7463d7f --- /dev/null +++ b/apps/backend/src/monitors/total_users.ts @@ -0,0 +1,11 @@ +import { sql } from 'drizzle-orm' +import { db } from '@/infra/db' +import { schema } from '@/infra/db/schema' + +export async function monitorTotalUsers() { + const [[{ count: totalUsers }]] = await Promise.all([ + db.select({ count: sql`count(*)::int` }).from(schema.users), + ]) + + console.log(`Total users: ${totalUsers}`) +} From ddc5764f1c4edba8715b32cfc2a231693e1953a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Sun, 22 Feb 2026 14:01:53 -0300 Subject: [PATCH 17/26] feat: add new monitoring functions for user and subscription metrics --- apps/backend/src/monitors/monitor.ts | 26 ++++++++++++++++++- apps/backend/src/monitors/new-users.ts | 17 ++++++++++++ .../src/monitors/today-new-subscriptions.ts | 17 ++++++++++++ .../backend/src/monitors/total-items-added.ts | 12 +++++++++ .../src/monitors/total-subscriptions.ts | 12 +++++++++ .../{total_users.ts => total-users.ts} | 1 + 6 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 apps/backend/src/monitors/new-users.ts create mode 100644 apps/backend/src/monitors/today-new-subscriptions.ts create mode 100644 apps/backend/src/monitors/total-items-added.ts create mode 100644 apps/backend/src/monitors/total-subscriptions.ts rename apps/backend/src/monitors/{total_users.ts => total-users.ts} (94%) diff --git a/apps/backend/src/monitors/monitor.ts b/apps/backend/src/monitors/monitor.ts index 8b6add66..7577081c 100644 --- a/apps/backend/src/monitors/monitor.ts +++ b/apps/backend/src/monitors/monitor.ts @@ -1,7 +1,11 @@ import cron from 'node-cron' import { config } from '@/config' import { logger } from '@/infra/adapters/logger' -import { monitorTotalUsers } from './total_users' +import { monitorTodayNewUsers } from './new-users' +import { monitorTodayNewSubscriptions } from './today-new-subscriptions' +import { monitorTotalItemsAdded } from './total-items-added' +import { monitorTotalSubscriptions } from './total-subscriptions' +import { monitorTotalUsers } from './total-users' export function startMonitors() { if (config.monitors.ENABLE_MONITORS === 'false') { @@ -16,5 +20,25 @@ export function startMonitors() { void monitorTotalUsers().catch(err => { logger.error('Monitor total users failed:', err) }) + + logger.info('Monitoring total subscriptions') + void monitorTotalSubscriptions().catch(err => { + logger.error('Monitor total subscriptions failed:', err) + }) + + logger.info('Monitoring total items added') + void monitorTotalItemsAdded().catch(err => { + logger.error('Monitor total items added failed:', err) + }) + + logger.info('Monitoring today new users') + void monitorTodayNewUsers().catch(err => { + logger.error('Monitor today new users failed:', err) + }) + + logger.info('Monitoring today new subscriptions') + void monitorTodayNewSubscriptions().catch(err => { + logger.error('Monitor today new subscriptions failed:', err) + }) }) } diff --git a/apps/backend/src/monitors/new-users.ts b/apps/backend/src/monitors/new-users.ts new file mode 100644 index 00000000..71a42e5f --- /dev/null +++ b/apps/backend/src/monitors/new-users.ts @@ -0,0 +1,17 @@ +import { sql } from 'drizzle-orm' +import { db } from '@/infra/db' +import { schema } from '@/infra/db/schema' + +export async function monitorTodayNewUsers() { + const [[{ count: totalNewUsers }]] = await Promise.all([ + db + .select({ count: sql`count(*)::int` }) + .from(schema.users) + .where( + sql`${schema.users.createdAt} > (date_trunc('day', now() AT TIME ZONE 'America/Sao_Paulo') AT TIME ZONE 'America/Sao_Paulo')` + ), + ]) + + console.log(`Today new users: ${totalNewUsers}`) + return totalNewUsers +} diff --git a/apps/backend/src/monitors/today-new-subscriptions.ts b/apps/backend/src/monitors/today-new-subscriptions.ts new file mode 100644 index 00000000..0b92f6a9 --- /dev/null +++ b/apps/backend/src/monitors/today-new-subscriptions.ts @@ -0,0 +1,17 @@ +import { sql } from 'drizzle-orm' +import { db } from '@/infra/db' +import { schema } from '@/infra/db/schema' + +export async function monitorTodayNewSubscriptions() { + const [[{ count: totalNewSubscriptions }]] = await Promise.all([ + db + .select({ count: sql`count(*)::int` }) + .from(schema.subscriptions) + .where( + sql`${schema.subscriptions.createdAt} > (date_trunc('day', now() AT TIME ZONE 'America/Sao_Paulo') AT TIME ZONE 'America/Sao_Paulo')` + ), + ]) + + console.log(`Today new subscriptions: ${totalNewSubscriptions}`) + return totalNewSubscriptions +} diff --git a/apps/backend/src/monitors/total-items-added.ts b/apps/backend/src/monitors/total-items-added.ts new file mode 100644 index 00000000..dd96a08e --- /dev/null +++ b/apps/backend/src/monitors/total-items-added.ts @@ -0,0 +1,12 @@ +import { sql } from 'drizzle-orm' +import { db } from '@/infra/db' +import { schema } from '@/infra/db/schema' + +export async function monitorTotalItemsAdded() { + const [[{ count: totalItemsAdded }]] = await Promise.all([ + db.select({ count: sql`count(*)::int` }).from(schema.userItems), + ]) + + console.log(`Total items added: ${totalItemsAdded}`) + return totalItemsAdded +} diff --git a/apps/backend/src/monitors/total-subscriptions.ts b/apps/backend/src/monitors/total-subscriptions.ts new file mode 100644 index 00000000..56bbded4 --- /dev/null +++ b/apps/backend/src/monitors/total-subscriptions.ts @@ -0,0 +1,12 @@ +import { sql } from 'drizzle-orm' +import { db } from '@/infra/db' +import { schema } from '@/infra/db/schema' + +export async function monitorTotalSubscriptions() { + const [[{ count: totalSubscriptions }]] = await Promise.all([ + db.select({ count: sql`count(*)::int` }).from(schema.subscriptions), + ]) + + console.log(`Total subscriptions: ${totalSubscriptions}`) + return totalSubscriptions +} diff --git a/apps/backend/src/monitors/total_users.ts b/apps/backend/src/monitors/total-users.ts similarity index 94% rename from apps/backend/src/monitors/total_users.ts rename to apps/backend/src/monitors/total-users.ts index e7463d7f..5568891b 100644 --- a/apps/backend/src/monitors/total_users.ts +++ b/apps/backend/src/monitors/total-users.ts @@ -8,4 +8,5 @@ export async function monitorTotalUsers() { ]) console.log(`Total users: ${totalUsers}`) + return totalUsers } From 219e30dc71b910b6328a4d05bc26ace061dc99ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Sun, 22 Feb 2026 14:41:13 -0300 Subject: [PATCH 18/26] feat: implement telemetry metrics monitoring for users and subscriptions --- .../src/infra/telemetry/monitor-metrics.ts | 50 +++++++++++++++++++ apps/backend/src/monitors/monitor.ts | 48 ++++++++++++------ 2 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 apps/backend/src/infra/telemetry/monitor-metrics.ts diff --git a/apps/backend/src/infra/telemetry/monitor-metrics.ts b/apps/backend/src/infra/telemetry/monitor-metrics.ts new file mode 100644 index 00000000..0f267315 --- /dev/null +++ b/apps/backend/src/infra/telemetry/monitor-metrics.ts @@ -0,0 +1,50 @@ +import { metrics } from '@opentelemetry/api' + +const METER_NAME = 'plotwist-api' +const METER_VERSION = '0.1.0' +const GAUGE_NAME = 'plotwist_monitor' + +export const monitorMetricNames = { + totalUsers: 'total_users', + totalSubscriptions: 'total_subscriptions', + totalItemsAdded: 'total_items_added', + todayNewUsers: 'today_new_users', + todayNewSubscriptions: 'today_new_subscriptions', +} as const + +const store: Partial> = {} + +function getMeter() { + return metrics.getMeter(METER_NAME, METER_VERSION) +} + +function registerGauge() { + const meter = getMeter() + const gauge = meter.createObservableGauge(GAUGE_NAME, { + description: 'Monitor values (total users, subscriptions, etc.)', + unit: '1', + }) + gauge.addCallback(result => { + for (const [name, value] of Object.entries(store)) { + if (typeof value === 'number') { + result.observe(value, { monitor: name }) + } + } + }) +} + +let initialized = false +function ensureInitialized() { + if (!initialized) { + registerGauge() + initialized = true + } +} + +export function setMonitorMetric( + name: (typeof monitorMetricNames)[keyof typeof monitorMetricNames], + value: number +) { + ensureInitialized() + store[name] = value +} diff --git a/apps/backend/src/monitors/monitor.ts b/apps/backend/src/monitors/monitor.ts index 7577081c..7d88fce3 100644 --- a/apps/backend/src/monitors/monitor.ts +++ b/apps/backend/src/monitors/monitor.ts @@ -1,6 +1,10 @@ import cron from 'node-cron' import { config } from '@/config' import { logger } from '@/infra/adapters/logger' +import { + monitorMetricNames, + setMonitorMetric, +} from '@/infra/telemetry/monitor-metrics' import { monitorTodayNewUsers } from './new-users' import { monitorTodayNewSubscriptions } from './today-new-subscriptions' import { monitorTotalItemsAdded } from './total-items-added' @@ -17,28 +21,42 @@ export function startMonitors() { cron.schedule(cronTime, () => { logger.info('Monitoring total users') - void monitorTotalUsers().catch(err => { - logger.error('Monitor total users failed:', err) - }) + void monitorTotalUsers() + .then(v => { + if (v != null) setMonitorMetric(monitorMetricNames.totalUsers, v) + }) + .catch(err => logger.error('Monitor total users failed:', err)) logger.info('Monitoring total subscriptions') - void monitorTotalSubscriptions().catch(err => { - logger.error('Monitor total subscriptions failed:', err) - }) + void monitorTotalSubscriptions() + .then(v => { + if (v != null) + setMonitorMetric(monitorMetricNames.totalSubscriptions, v) + }) + .catch(err => logger.error('Monitor total subscriptions failed:', err)) logger.info('Monitoring total items added') - void monitorTotalItemsAdded().catch(err => { - logger.error('Monitor total items added failed:', err) - }) + void monitorTotalItemsAdded() + .then(v => { + if (v != null) setMonitorMetric(monitorMetricNames.totalItemsAdded, v) + }) + .catch(err => logger.error('Monitor total items added failed:', err)) logger.info('Monitoring today new users') - void monitorTodayNewUsers().catch(err => { - logger.error('Monitor today new users failed:', err) - }) + void monitorTodayNewUsers() + .then(v => { + if (v != null) setMonitorMetric(monitorMetricNames.todayNewUsers, v) + }) + .catch(err => logger.error('Monitor today new users failed:', err)) logger.info('Monitoring today new subscriptions') - void monitorTodayNewSubscriptions().catch(err => { - logger.error('Monitor today new subscriptions failed:', err) - }) + void monitorTodayNewSubscriptions() + .then(v => { + if (v != null) + setMonitorMetric(monitorMetricNames.todayNewSubscriptions, v) + }) + .catch(err => + logger.error('Monitor today new subscriptions failed:', err) + ) }) } From 0211cec548cead8230b9be389fc68008ffcc54a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Sun, 22 Feb 2026 15:09:53 -0300 Subject: [PATCH 19/26] feat: enhance telemetry dashboard with new panels for total subscriptions and total users --- apps/backend/src/infra/telemetry/dash.json | 452 ++++++++++++++++++--- 1 file changed, 402 insertions(+), 50 deletions(-) diff --git a/apps/backend/src/infra/telemetry/dash.json b/apps/backend/src/infra/telemetry/dash.json index a98a42a3..bd0b3545 100644 --- a/apps/backend/src/infra/telemetry/dash.json +++ b/apps/backend/src/infra/telemetry/dash.json @@ -5,8 +5,8 @@ "name": "g6wwd8", "namespace": "default", "uid": "FgXXFvgSHts7LvpkvXUedqZPmfs50wJrIbU86KpQGa4X", - "resourceVersion": "1", - "generation": 9, + "resourceVersion": "12", + "generation": 21, "creationTimestamp": "2026-02-18T01:36:07Z", "labels": { "grafana.app/deprecatedInternalID": "4" @@ -14,7 +14,7 @@ "annotations": { "grafana.app/createdBy": "provisioning:", "grafana.app/updatedBy": "provisioning:", - "grafana.app/updatedTimestamp": "2026-02-18T01:36:07Z", + "grafana.app/updatedTimestamp": "2026-02-22T17:09:23Z", "grafana.app/saved-from-ui": "Grafana v12.3.1 (0d1a5b4420)" } }, @@ -183,6 +183,168 @@ } } }, + "panel-10": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"total_subscriptions\"}", + "instant": false, + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 10, + "links": [], + "title": "Total subscriptions", + "vizConfig": { + "group": "stat", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + "version": "12.3.1" + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"total_users\"}", + "instant": false, + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 11, + "links": [], + "title": "Total Users", + "vizConfig": { + "group": "stat", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + "version": "12.3.1" + } + } + }, "panel-2": { "kind": "Panel", "spec": { @@ -313,7 +475,7 @@ "datasource": { "name": "prometheus" }, - "group": "prometheus", + "group": "grafana", "kind": "DataQuery", "spec": { "editorMode": "code", @@ -466,65 +628,255 @@ "version": "12.3.1" } } - } - }, - "layout": { - "kind": "GridLayout", - "spec": { - "items": [ - { - "kind": "GridLayoutItem", + }, + "panel-7": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-4" - }, - "height": 8, - "width": 12, - "x": 0, - "y": 0 + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"today_new_subscriptions\"}", + "instant": false, + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] } }, - { - "kind": "GridLayoutItem", + "description": "", + "id": 7, + "links": [], + "title": "Today new subscriptons", + "vizConfig": { + "group": "stat", + "kind": "VizConfig", "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-1" + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] }, - "height": 8, - "width": 12, - "x": 12, - "y": 0 + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + "version": "12.3.1" + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"total_items_added\"}", + "instant": false, + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] } }, - { - "kind": "GridLayoutItem", + "description": "", + "id": 8, + "links": [], + "title": "Total items added", + "vizConfig": { + "group": "stat", + "kind": "VizConfig", "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-3" + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] }, - "height": 8, - "width": 12, - "x": 0, - "y": 8 + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + "version": "12.3.1" + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"today_new_users\"}", + "instant": false, + "legendFormat": "__auto", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] } }, - { - "kind": "GridLayoutItem", + "description": "", + "id": 9, + "links": [], + "title": "New users", + "vizConfig": { + "group": "stat", + "kind": "VizConfig", "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-2" + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] }, - "height": 8, - "width": 12, - "x": 12, - "y": 8 - } + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + } + }, + "version": "12.3.1" } - ] + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] } }, "links": [], @@ -546,7 +898,7 @@ "1d" ], "fiscalYearStartMonth": 0, - "from": "now-3h", + "from": "now-5m", "hideTimepicker": false, "timezone": "browser", "to": "now" From b7d007f7d84ba08a8a372ae3e08a83e55ef6f5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Sun, 22 Feb 2026 23:37:10 -0300 Subject: [PATCH 20/26] feat: add host metrics monitoring and update telemetry dashboard configuration --- apps/backend/src/infra/telemetry/dash.json | 1709 ++++++++++---------- apps/backend/src/infra/telemetry/otel.ts | 16 +- pnpm-lock.yaml | 22 + 3 files changed, 905 insertions(+), 842 deletions(-) diff --git a/apps/backend/src/infra/telemetry/dash.json b/apps/backend/src/infra/telemetry/dash.json index bd0b3545..fa2f57e4 100644 --- a/apps/backend/src/infra/telemetry/dash.json +++ b/apps/backend/src/infra/telemetry/dash.json @@ -1,910 +1,937 @@ { - "apiVersion": "dashboard.grafana.app/v2beta1", - "kind": "Dashboard", - "metadata": { - "name": "g6wwd8", - "namespace": "default", - "uid": "FgXXFvgSHts7LvpkvXUedqZPmfs50wJrIbU86KpQGa4X", - "resourceVersion": "12", - "generation": 21, - "creationTimestamp": "2026-02-18T01:36:07Z", - "labels": { - "grafana.app/deprecatedInternalID": "4" - }, - "annotations": { - "grafana.app/createdBy": "provisioning:", - "grafana.app/updatedBy": "provisioning:", - "grafana.app/updatedTimestamp": "2026-02-22T17:09:23Z", - "grafana.app/saved-from-ui": "Grafana v12.3.1 (0d1a5b4420)" - } - }, - "spec": { - "annotations": [ + "annotations": { + "list": [ { - "kind": "AnnotationQuery", - "spec": { - "builtIn": true, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "query": { - "datasource": { - "name": "-- Grafana --" - }, - "group": "grafana", - "kind": "DataQuery", - "spec": {}, - "version": "v0" - } - } + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" } - ], - "cursorSync": "Off", - "editable": true, - "elements": { - "panel-1": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "tempo" - }, - "group": "tempo", - "kind": "DataQuery", - "spec": { - "filters": [ - { - "id": "29f2879c", - "operator": "=", - "scope": "span" - }, - { - "id": "service-name", - "isCustomValue": false, - "operator": "=", - "scope": "resource", - "tag": "service.name", - "value": ["plotwist-api"], - "valueType": "string" - }, - { - "id": "status", - "isCustomValue": false, - "operator": "=", - "scope": "intrinsic", - "tag": "status", - "value": "error", - "valueType": "keyword" - } - ], - "limit": 20, - "metricsQueryType": "range", - "queryType": "traceqlSearch", - "serviceMapUseNativeHistograms": false, - "tableType": "traces" - }, - "version": "v0" - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 0, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 13, + "panels": [], + "title": "Service status", + "type": "row" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } }, - "description": "", - "id": 1, - "links": [], - "title": "All failed requests", - "vizConfig": { - "group": "timeseries", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "showValues": false, - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } + { + "color": "red", + "value": 80 } - }, - "version": "12.3.1" + ] } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" } }, - "panel-10": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "prometheus", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "plotwist_monitor_ratio{monitor=\"total_subscriptions\"}", - "instant": false, - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] + "pluginVersion": "12.3.1", + "targets": [ + { + "refId": "A" + } + ], + "title": "New panel", + "type": "timeseries" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" } }, - "description": "", - "id": 10, - "links": [], - "title": "Total subscriptions", - "vizConfig": { - "group": "stat", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 }, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + { + "color": "red", + "value": 80 } - }, - "version": "12.3.1" + ] } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" } }, - "panel-11": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "prometheus", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "plotwist_monitor_ratio{monitor=\"total_users\"}", - "instant": false, - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" + "pluginVersion": "12.3.1", + "targets": [ + { + "refId": "A" + } + ], + "title": "New panel", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 5, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 } - } - ], - "queryOptions": {}, - "transformations": [] + ] + }, + "unit": "hits/s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "sum(rate(http_server_requests_total[5m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" } + ], + "title": "Requests rate per second", + "type": "stat" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" }, - "description": "", - "id": 11, - "links": [], - "title": "Total Users", - "vizConfig": { - "group": "stat", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] } }, - "version": "12.3.1" - } - } - }, - "panel-2": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "filters": [ { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "prometheus", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "sum(rate(http_server_requests_total[5m]))", - "instant": false, - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" - } + "id": "29f2879c", + "operator": "=", + "scope": "span" + }, + { + "id": "service-name", + "isCustomValue": false, + "operator": "=", + "scope": "resource", + "tag": "service.name", + "value": ["plotwist-api"], + "valueType": "string" + }, + { + "id": "status", + "isCustomValue": false, + "operator": "=", + "scope": "intrinsic", + "tag": "status", + "value": "error", + "valueType": "keyword" } ], - "queryOptions": {}, - "transformations": [] + "limit": 20, + "metricsQueryType": "range", + "queryType": "traceqlSearch", + "refId": "A", + "serviceMapUseNativeHistograms": false, + "tableType": "traces" } + ], + "title": "All failed requests", + "type": "timeseries" + }, + { + "datasource": { + "uid": "prometheus" }, - "description": "", - "id": 2, - "links": [], - "title": "All Requests", - "vizConfig": { - "group": "timeseries", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 3, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "showValues": false, - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } + { + "color": "#EAB839", + "value": 200 }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] + { + "color": "red", + "value": 400 } - }, - "overrides": [] + ] }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - } + "unit": "ms" }, - "version": "12.3.1" - } - } - }, - "panel-3": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "grafana", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "avg_over_time(sum(rate(http_server_requests_total[5m]))[$__range:5m]) * 1000", - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] - } + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 }, - "description": "", "id": 3, - "links": [], + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "editorMode": "code", + "expr": "avg_over_time(sum(rate(http_server_requests_total[5m]))[$__range:5m]) * 1000", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], "title": "Average requests time", - "vizConfig": { - "group": "gauge", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "decimals": 3, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "#EAB839", - "value": 200 - }, - { - "color": "red", - "value": 400 - } - ] - }, - "unit": "ms" - }, - "overrides": [] + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" }, - "options": { - "minVizHeight": 75, - "minVizWidth": 75, - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "sizing": "auto" + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] } }, - "version": "12.3.1" - } + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(http_server_requests_total[5m]))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "All Requests", + "type": "timeseries" } + ], + "title": "API data", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 }, - "panel-4": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "prometheus", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "sum(rate(http_server_requests_total[5m]))", - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" + "id": 6, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 } - } - ], - "queryOptions": {}, - "transformations": [] + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 9, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"today_new_users\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" } + ], + "title": "New users", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" }, - "description": "", - "id": 4, - "links": [], - "title": "Requests rate per second", - "vizConfig": { - "group": "stat", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 }, - "unit": "hits/s" - }, - "overrides": [] + { + "color": "red", + "value": 80 + } + ] }, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - } + "unit": "short" }, - "version": "12.3.1" - } - } - }, - "panel-7": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "prometheus", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "plotwist_monitor_ratio{monitor=\"today_new_subscriptions\"}", - "instant": false, - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "id": 11, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"total_users\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" } + ], + "title": "Total Users", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" }, - "description": "", - "id": 7, - "links": [], - "title": "Today new subscriptons", - "vizConfig": { - "group": "stat", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 }, - "unit": "short" - }, - "overrides": [] + { + "color": "red", + "value": 80 + } + ] }, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - } + "unit": "short" }, - "version": "12.3.1" - } - } - }, - "panel-8": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "prometheus", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "plotwist_monitor_ratio{monitor=\"total_items_added\"}", - "instant": false, - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] - } + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 26 }, - "description": "", "id": 8, - "links": [], + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"total_items_added\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], "title": "Total items added", - "vizConfig": { - "group": "stat", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 }, - "unit": "short" - }, - "overrides": [] + { + "color": "red", + "value": 80 + } + ] }, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - } + "unit": "short" }, - "version": "12.3.1" - } - } - }, - "panel-9": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "query": { - "datasource": { - "name": "prometheus" - }, - "group": "prometheus", - "kind": "DataQuery", - "spec": { - "editorMode": "code", - "expr": "plotwist_monitor_ratio{monitor=\"today_new_users\"}", - "instant": false, - "legendFormat": "__auto", - "range": true - }, - "version": "v0" - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 26 + }, + "id": 10, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"total_subscriptions\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" } + ], + "title": "Total subscriptions", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" }, - "description": "", - "id": 9, - "links": [], - "title": "New users", - "vizConfig": { - "group": "stat", - "kind": "VizConfig", - "spec": { - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 }, - "unit": "short" - }, - "overrides": [] + { + "color": "red", + "value": 80 + } + ] }, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - } + "unit": "short" }, - "version": "12.3.1" - } + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 34 + }, + "id": 7, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "plotwist_monitor_ratio{monitor=\"today_new_subscriptions\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Today new subscriptons", + "type": "stat" } - } - }, - "layout": { - "kind": "GridLayout", - "spec": { - "items": [] - } - }, - "links": [], - "liveNow": false, - "preload": false, - "tags": [], - "timeSettings": { - "autoRefresh": "", - "autoRefreshIntervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" ], - "fiscalYearStartMonth": 0, - "from": "now-5m", - "hideTimepicker": false, - "timezone": "browser", - "to": "now" - }, - "title": "Plotwist", - "variables": [] + "title": "Metrics", + "type": "row" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" }, - "status": {} + "timepicker": {}, + "timezone": "browser", + "title": "Plotwist", + "uid": "g6wwd8", + "version": 24 } diff --git a/apps/backend/src/infra/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts index 496c4efe..f9db9e48 100644 --- a/apps/backend/src/infra/telemetry/otel.ts +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -1,7 +1,8 @@ import FastifyOtel from '@fastify/otel' -import { trace } from '@opentelemetry/api' +import { metrics, trace } from '@opentelemetry/api' import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' +import { HostMetrics } from '@opentelemetry/host-metrics' import { resourceFromAttributes } from '@opentelemetry/resources' import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' import { NodeSDK } from '@opentelemetry/sdk-node' @@ -31,6 +32,19 @@ console.log('Starting OTLP exporter') sdk.start() +const meterProvider = metrics.getMeterProvider() +const hostMetrics = new HostMetrics({ + meterProvider, + metricGroups: [ + 'process.cpu', + 'process.memory', + 'system.cpu', + 'system.memory', + 'system.network', + ], +}) +hostMetrics.start() + const fastifyOtel = new FastifyOtel() fastifyOtel.setTracerProvider(trace.getTracerProvider()) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b743f71d..6d5fd28d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: '@opentelemetry/exporter-trace-otlp-proto': specifier: ^0.211.0 version: 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/host-metrics': + specifier: ^0.38.2 + version: 0.38.2(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': specifier: ^0.212.0 version: 0.212.0(@opentelemetry/api@1.9.0) @@ -2718,6 +2721,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/host-metrics@0.38.2': + resolution: {integrity: sha512-XnMj6BiLFjRABvYy6njZjqmX+ABo1SjQpeZFCARz1sXJ+wlOrFjJ/TllaYpD193bZ+FCA30R3iRRz8EjbpsmHA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation-http@0.212.0': resolution: {integrity: sha512-t2nt16Uyv9irgR+tqnX96YeToOStc3X5js7Ljn3EKlI2b4Fe76VhMkTXtsTQ0aId6AsYgefrCRnXSCo/Fn/vww==} engines: {node: ^18.19.0 || >=20.6.0} @@ -8394,6 +8403,12 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + systeminformation@5.30.3: + resolution: {integrity: sha512-NgHJUpA+y7j4asLQa9jgBt+Eb2piyQIXQ+YjOyd2K0cHNwbNJ6I06F5afOqOiaCuV/wrEyGrb0olg4aFLlJD+A==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -11426,6 +11441,11 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/host-metrics@0.38.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + systeminformation: 5.30.3 + '@opentelemetry/instrumentation-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -18060,6 +18080,8 @@ snapshots: symbol-tree@3.2.4: {} + systeminformation@5.30.3: {} + tagged-tag@1.0.0: {} tailwind-merge@3.4.0: {} From 552d0fc1f268591113692c7645e318e01da857f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 23 Feb 2026 00:07:28 -0300 Subject: [PATCH 21/26] feat: update telemetry dashboard to include CPU and memory usage metrics with Prometheus datasource --- apps/backend/src/infra/telemetry/dash.json | 357 ++++++++++++------ .../infra/telemetry/http-request-metrics.ts | 24 ++ 2 files changed, 271 insertions(+), 110 deletions(-) diff --git a/apps/backend/src/infra/telemetry/dash.json b/apps/backend/src/infra/telemetry/dash.json index fa2f57e4..cb9ad50b 100644 --- a/apps/backend/src/infra/telemetry/dash.json +++ b/apps/backend/src/infra/telemetry/dash.json @@ -36,8 +36,8 @@ }, { "datasource": { - "type": "datasource", - "uid": "grafana" + "type": "prometheus", + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -48,170 +48,212 @@ "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", - "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, + "lineInterpolation": "smooth", + "lineWidth": 2, "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, + "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "showValues": false, "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } }, "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } + "max": 1, + "min": 0, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "unit": "percentunit" }, "overrides": [] }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 1 - }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 }, "id": 12, "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, - "pluginVersion": "12.3.1", "targets": [ { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", + "expr": "process_cpu_utilization{job=\"plotwist-api\"} or process_cpu_utilization", + "legendFormat": "__auto", + "range": true, "refId": "A" } ], - "title": "New panel", + "title": "Process CPU utilization", "type": "timeseries" }, { "datasource": { - "type": "datasource", - "uid": "grafana" + "type": "prometheus", + "uid": "prometheus" }, "fieldConfig": { "defaults": { - "color": { - "mode": "palette-classic" - }, + "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", - "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, + "lineInterpolation": "smooth", + "lineWidth": 2, "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, + "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "showValues": false, "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } }, "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "unit": "bytes" }, "overrides": [] }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 1 - }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 1 }, "id": 14, "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", + "expr": "process_memory_usage{job=\"plotwist-api\"} or process_memory_usage", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Process memory usage", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "unit": "percentunit" }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 9 }, + "id": 15, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", + "expr": "avg(system_cpu_utilization{job=\"plotwist-api\"}) or avg(system_cpu_utilization)", + "legendFormat": "__auto", + "range": true, + "refId": "A" } + ], + "title": "System CPU utilization", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 9 }, + "id": 16, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, - "pluginVersion": "12.3.1", "targets": [ { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", + "expr": "system_memory_utilization{job=\"plotwist-api\"} or system_memory_utilization", + "legendFormat": "__auto", + "range": true, "refId": "A" } ], - "title": "New panel", + "title": "System memory utilization", "type": "timeseries" }, { @@ -220,7 +262,7 @@ "h": 1, "w": 24, "x": 0, - "y": 9 + "y": 17 }, "id": 5, "panels": [ @@ -574,6 +616,101 @@ ], "title": "All Requests", "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 0.001 } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 }, + "id": 17, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", + "expr": "sum(rate(http_server_requests_total{http_status_code=~\"5..\"}[5m])) or sum(rate(http_server_requests_total{http_response_status=\"error\"}[5m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "5xx error rate", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "opacity", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 500 }] }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 }, + "id": 18, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le)) * 1000", + "legendFormat": "p95", + "range": true, + "refId": "A" + } + ], + "title": "Request latency p95", + "type": "timeseries" } ], "title": "API data", diff --git a/apps/backend/src/infra/telemetry/http-request-metrics.ts b/apps/backend/src/infra/telemetry/http-request-metrics.ts index fe24745a..ac29c4b3 100644 --- a/apps/backend/src/infra/telemetry/http-request-metrics.ts +++ b/apps/backend/src/infra/telemetry/http-request-metrics.ts @@ -5,22 +5,46 @@ function getStatusClass(statusCode: number): 'ok' | 'error' { return statusCode >= 200 && statusCode < 400 ? 'ok' : 'error' } +const DURATION_BOUNDARIES_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000] + export function registerHttpRequestMetrics(app: FastifyInstance) { const meter = metrics.getMeter('plotwist-api', '0.1.0') const requestCounter = meter.createCounter('http.server.requests', { description: 'HTTP server request count by status', unit: '1', }) + const requestDuration = meter.createHistogram( + 'http.server.request.duration', + { + description: 'HTTP server request duration', + unit: 's', + }, + DURATION_BOUNDARIES_MS.map(ms => ms / 1000) + ) + + app.addHook('onRequest', (request, _reply, done) => { + ;(request as { _startTime?: number })._startTime = Date.now() + done() + }) app.addHook('onResponse', (request, reply, done) => { const statusCode = reply.statusCode const statusClass = getStatusClass(statusCode) + const startTime = (request as { _startTime?: number })._startTime + const durationSec = + typeof startTime === 'number' ? (Date.now() - startTime) / 1000 : 0 + requestCounter.add(1, { 'http.status_code': statusCode, 'http.response.status': statusClass, 'http.method': request.method, 'http.route': request.routeOptions?.url ?? request.url, }) + requestDuration.record(durationSec, { + 'http.response.status': statusClass, + 'http.method': request.method, + 'http.route': request.routeOptions?.url ?? request.url, + }) done() }) } From f40582173859ab5c69179de098ee77424524788c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 23 Feb 2026 00:08:15 -0300 Subject: [PATCH 22/26] refactor: clean up config and telemetry files, improve formatting in JSON and TypeScript --- apps/backend/src/config.ts | 3 +- apps/backend/src/infra/http/server.ts | 4 +- apps/backend/src/infra/telemetry/dash.json | 81 ++++++++++++++++--- .../src/infra/telemetry/monitor-metrics.ts | 4 +- apps/backend/src/main.ts | 2 +- 5 files changed, 77 insertions(+), 17 deletions(-) diff --git a/apps/backend/src/config.ts b/apps/backend/src/config.ts index c8cef43e..02d355e7 100644 --- a/apps/backend/src/config.ts +++ b/apps/backend/src/config.ts @@ -127,8 +127,7 @@ function loadMonitorsEnvs() { const schema = z.object({ ENABLE_MONITORS: z.string().default('false'), MONITOR_CRON_TIME: z.string().default('0 0 * * *'), - }) return schema.parse(process.env) -} \ No newline at end of file +} diff --git a/apps/backend/src/infra/http/server.ts b/apps/backend/src/infra/http/server.ts index 28d6fdf2..e901a0f2 100644 --- a/apps/backend/src/infra/http/server.ts +++ b/apps/backend/src/infra/http/server.ts @@ -6,11 +6,11 @@ import { validatorCompiler, } from 'fastify-type-provider-zod' import { ZodError } from 'zod' +import { config } from '@/config' import { DomainError } from '@/domain/errors/domain-error' +import { logger } from '@/infra/adapters/logger' import { registerHttpRequestMetrics } from '@/infra/telemetry/http-request-metrics' import { fastifyOtel } from '@/infra/telemetry/otel' -import { config } from '@/config' -import { logger } from '@/infra/adapters/logger' import { routes } from './routes' import { transformSwaggerSchema } from './transform-schema' diff --git a/apps/backend/src/infra/telemetry/dash.json b/apps/backend/src/infra/telemetry/dash.json index cb9ad50b..210c3900 100644 --- a/apps/backend/src/infra/telemetry/dash.json +++ b/apps/backend/src/infra/telemetry/dash.json @@ -69,7 +69,13 @@ "mappings": [], "max": 1, "min": 0, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, "unit": "percentunit" }, "overrides": [] @@ -77,7 +83,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 }, "id": 12, "options": { - "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, "targets": [ @@ -124,7 +135,13 @@ "thresholdsStyle": { "mode": "off" } }, "mappings": [], - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, "unit": "bytes" }, "overrides": [] @@ -132,7 +149,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 1 }, "id": 14, "options": { - "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, "targets": [ @@ -178,7 +200,13 @@ "mappings": [], "max": 1, "min": 0, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, "unit": "percentunit" }, "overrides": [] @@ -186,7 +214,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 9 }, "id": 15, "options": { - "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, "targets": [ @@ -232,7 +265,13 @@ "mappings": [], "max": 1, "min": 0, - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 80 }] }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, "unit": "percentunit" }, "overrides": [] @@ -240,7 +279,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 9 }, "id": 16, "options": { - "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, "targets": [ @@ -642,7 +686,11 @@ "justifyMode": "auto", "orientation": "auto", "percentChangeColorMode": "standard", - "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, "showPercentChange": false, "textMode": "auto", "wideLayout": true @@ -688,7 +736,13 @@ "thresholdsStyle": { "mode": "off" } }, "mappings": [], - "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 500 }] }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 500 } + ] + }, "unit": "ms" }, "overrides": [] @@ -696,7 +750,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 }, "id": 18, "options": { - "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "hideZeros": false, "mode": "single", "sort": "none" } }, "targets": [ diff --git a/apps/backend/src/infra/telemetry/monitor-metrics.ts b/apps/backend/src/infra/telemetry/monitor-metrics.ts index 0f267315..78ce77f0 100644 --- a/apps/backend/src/infra/telemetry/monitor-metrics.ts +++ b/apps/backend/src/infra/telemetry/monitor-metrics.ts @@ -12,7 +12,9 @@ export const monitorMetricNames = { todayNewSubscriptions: 'today_new_subscriptions', } as const -const store: Partial> = {} +const store: Partial< + Record<(typeof monitorMetricNames)[keyof typeof monitorMetricNames], number> +> = {} function getMeter() { return metrics.getMeter(METER_NAME, METER_VERSION) diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index ee48746e..d07ac62b 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,8 +1,8 @@ import '@/infra/telemetry/otel' import { startServer } from '@/infra/http/server' -import { startWorkers } from '@/workers/worker' import { startMonitors } from '@/monitors/monitor' +import { startWorkers } from '@/workers/worker' async function main() { startWorkers() From 06f642366b03004ee47abd9aba0ec07b8354e463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 23 Feb 2026 00:11:00 -0300 Subject: [PATCH 23/26] feat: add @opentelemetry/host-metrics dependency for enhanced host monitoring --- apps/backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/package.json b/apps/backend/package.json index e62e20c0..0dc35119 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -41,6 +41,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.211.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.211.0", + "@opentelemetry/host-metrics": "^0.38.2", "@opentelemetry/instrumentation-http": "^0.212.0", "@opentelemetry/resources": "^2.5.0", "@opentelemetry/sdk-metrics": "^2.5.0", From f239913cb83e1804de1f14fad259d518ecb207cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 23 Feb 2026 00:29:25 -0300 Subject: [PATCH 24/26] feat: add telemetry configuration and monitoring support with OpenTelemetry --- apps/backend/.env.example | 8 +++++++ apps/backend/src/config.ts | 16 +++++++++++++ apps/backend/src/infra/telemetry/otel.ts | 29 ++++++++++++++++++++---- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 561e28c7..112d3537 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -47,3 +47,11 @@ ENABLE_CRON_JOBS=false # OpenAI OPENAI_API_KEY= + +# Monitors +ENABLE_MONITORS=true +MONITOR_CRON_TIME="*/30 * * * *" + +# Telemetry +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/metrics +OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20 diff --git a/apps/backend/src/config.ts b/apps/backend/src/config.ts index 02d355e7..fe004439 100644 --- a/apps/backend/src/config.ts +++ b/apps/backend/src/config.ts @@ -13,6 +13,7 @@ export const config = { openai: loadOpenAIEnvs(), google: loadGoogleEnvs(), monitors: loadMonitorsEnvs(), + telemetry: loadTelemetryEnvs(), } function loadRedisEnvs() { @@ -131,3 +132,18 @@ function loadMonitorsEnvs() { return schema.parse(process.env) } + +function loadTelemetryEnvs() { + const schema = z.object({ + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: z + .url() + .optional() + .default('http://localhost:4318/v1/metrics'), + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: z + .url() + .optional() + .default('http://localhost:4318/v1/traces'), + OTEL_EXPORTER_OTLP_HEADERS: z.string().optional(), + }) + return schema.parse(process.env) +} diff --git a/apps/backend/src/infra/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts index f9db9e48..fc957ae0 100644 --- a/apps/backend/src/infra/telemetry/otel.ts +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -10,6 +10,27 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions' +import { config } from '@/config' + +const otlpMetricsEndpoint = config.telemetry.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT +const otlpTracesEndpoint = config.telemetry.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + +const otlpHeaders = parseOtlpHeaders( + config.telemetry.OTEL_EXPORTER_OTLP_HEADERS +) + +function parseOtlpHeaders(raw: string | undefined): Record { + if (!raw?.trim()) return {} + const out: Record = {} + for (const part of raw.split(',')) { + const eq = part.indexOf('=') + if (eq === -1) continue + const key = part.slice(0, eq).trim() + const value = part.slice(eq + 1).trim() + if (key && value) out[key] = value + } + return out +} const sdk = new NodeSDK({ resource: resourceFromAttributes({ @@ -17,13 +38,13 @@ const sdk = new NodeSDK({ [ATTR_SERVICE_VERSION]: '0.1.0', }), traceExporter: new OTLPTraceExporter({ - url: 'http://localhost:4318/v1/traces', - headers: {}, + url: otlpTracesEndpoint, + headers: otlpHeaders, }), metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ - url: 'http://localhost:4318/v1/metrics', - headers: {}, + url: otlpMetricsEndpoint, + headers: otlpHeaders, }), }), }) From c2698d220445b5ede81714d0eb4f5f30c0bd3a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 23 Feb 2026 00:36:30 -0300 Subject: [PATCH 25/26] refactor: simplify HTTP request metrics configuration and enhance OTLP headers handling --- .../src/infra/telemetry/http-request-metrics.ts | 5 +---- apps/backend/src/infra/telemetry/otel.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/backend/src/infra/telemetry/http-request-metrics.ts b/apps/backend/src/infra/telemetry/http-request-metrics.ts index ac29c4b3..bb7e9cf9 100644 --- a/apps/backend/src/infra/telemetry/http-request-metrics.ts +++ b/apps/backend/src/infra/telemetry/http-request-metrics.ts @@ -5,8 +5,6 @@ function getStatusClass(statusCode: number): 'ok' | 'error' { return statusCode >= 200 && statusCode < 400 ? 'ok' : 'error' } -const DURATION_BOUNDARIES_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000] - export function registerHttpRequestMetrics(app: FastifyInstance) { const meter = metrics.getMeter('plotwist-api', '0.1.0') const requestCounter = meter.createCounter('http.server.requests', { @@ -18,8 +16,7 @@ export function registerHttpRequestMetrics(app: FastifyInstance) { { description: 'HTTP server request duration', unit: 's', - }, - DURATION_BOUNDARIES_MS.map(ms => ms / 1000) + } ) app.addHook('onRequest', (request, _reply, done) => { diff --git a/apps/backend/src/infra/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts index fc957ae0..33fd0a60 100644 --- a/apps/backend/src/infra/telemetry/otel.ts +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -11,13 +11,15 @@ import { ATTR_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions' import { config } from '@/config' +import { logger } from '../adapters/logger' const otlpMetricsEndpoint = config.telemetry.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT const otlpTracesEndpoint = config.telemetry.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT -const otlpHeaders = parseOtlpHeaders( - config.telemetry.OTEL_EXPORTER_OTLP_HEADERS -) +const otlpHeaders = + config.app.APP_ENV === 'production' + ? parseOtlpHeaders(config.telemetry.OTEL_EXPORTER_OTLP_HEADERS) + : {} function parseOtlpHeaders(raw: string | undefined): Record { if (!raw?.trim()) return {} @@ -49,7 +51,7 @@ const sdk = new NodeSDK({ }), }) -console.log('Starting OTLP exporter') +logger.info('Starting OTLP exporter') sdk.start() From d216c8b86c9003e7e5702b60cd0c01772a3b47ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 23 Feb 2026 01:58:11 -0300 Subject: [PATCH 26/26] refactor: update telemetry configuration to use simplified endpoint and header handling --- apps/backend/.env.example | 4 +- apps/backend/src/config.ts | 9 +--- apps/backend/src/infra/telemetry/otel.ts | 55 ++++++++++++++++-------- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 112d3537..2f552610 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -53,5 +53,5 @@ ENABLE_MONITORS=true MONITOR_CRON_TIME="*/30 * * * *" # Telemetry -OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/metrics -OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20 +OTEL_EXPORTER_OTLP_ENDPOINT=localhost +OTEL_EXPORTER_OTLP_HEADERS= diff --git a/apps/backend/src/config.ts b/apps/backend/src/config.ts index fe004439..8377f9d4 100644 --- a/apps/backend/src/config.ts +++ b/apps/backend/src/config.ts @@ -135,14 +135,7 @@ function loadMonitorsEnvs() { function loadTelemetryEnvs() { const schema = z.object({ - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: z - .url() - .optional() - .default('http://localhost:4318/v1/metrics'), - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: z - .url() - .optional() - .default('http://localhost:4318/v1/traces'), + OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(), OTEL_EXPORTER_OTLP_HEADERS: z.string().optional(), }) return schema.parse(process.env) diff --git a/apps/backend/src/infra/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts index 33fd0a60..a3264765 100644 --- a/apps/backend/src/infra/telemetry/otel.ts +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -13,41 +13,60 @@ import { import { config } from '@/config' import { logger } from '../adapters/logger' -const otlpMetricsEndpoint = config.telemetry.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT -const otlpTracesEndpoint = config.telemetry.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT +const LOCALHOST_OTLP = 'http://localhost:4318' -const otlpHeaders = - config.app.APP_ENV === 'production' - ? parseOtlpHeaders(config.telemetry.OTEL_EXPORTER_OTLP_HEADERS) - : {} +function getOtlpConfig() { + const isProduction = config.app.APP_ENV === 'production' + const base = + isProduction && config.telemetry.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() + ? resolveBaseUrl(config.telemetry.OTEL_EXPORTER_OTLP_ENDPOINT.trim()) + : LOCALHOST_OTLP + const isRemote = base !== LOCALHOST_OTLP + const headers = + isRemote && config.telemetry.OTEL_EXPORTER_OTLP_HEADERS?.trim() + ? parseOtlpHeaders(config.telemetry.OTEL_EXPORTER_OTLP_HEADERS) + : {} -function parseOtlpHeaders(raw: string | undefined): Record { - if (!raw?.trim()) return {} + return { + metricsUrl: `${base}/v1/metrics`, + tracesUrl: `${base}/v1/traces`, + headers, + } +} + +function resolveBaseUrl(endpoint: string): string { + if (endpoint === 'localhost' || endpoint === '127.0.0.1') + return LOCALHOST_OTLP + if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) + return endpoint.replace(/\/$/, '') + return `http://${endpoint}:4318` +} + +function parseOtlpHeaders(raw: string): Record { const out: Record = {} for (const part of raw.split(',')) { const eq = part.indexOf('=') if (eq === -1) continue const key = part.slice(0, eq).trim() - const value = part.slice(eq + 1).trim() - if (key && value) out[key] = value + const value = part + .slice(eq + 1) + .trim() + .replace(/%20/g, ' ') + if (key) out[key] = value } return out } +const { metricsUrl, tracesUrl, headers } = getOtlpConfig() + const sdk = new NodeSDK({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'plotwist-api', [ATTR_SERVICE_VERSION]: '0.1.0', }), - traceExporter: new OTLPTraceExporter({ - url: otlpTracesEndpoint, - headers: otlpHeaders, - }), + traceExporter: new OTLPTraceExporter({ url: tracesUrl, headers }), metricReader: new PeriodicExportingMetricReader({ - exporter: new OTLPMetricExporter({ - url: otlpMetricsEndpoint, - headers: otlpHeaders, - }), + exporter: new OTLPMetricExporter({ url: metricsUrl, headers }), }), })