diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 561e28c7..2f552610 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=localhost +OTEL_EXPORTER_OTLP_HEADERS= diff --git a/apps/backend/docker-compose.yml b/apps/backend/docker-compose.yml index 4ada7330..98125b87 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 b46a6950..0dc35119 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -32,10 +32,21 @@ "@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", "@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", + "@opentelemetry/host-metrics": "^0.38.2", + "@opentelemetry/instrumentation-http": "^0.212.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 +68,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/config.ts b/apps/backend/src/config.ts index 0637da96..8377f9d4 100644 --- a/apps/backend/src/config.ts +++ b/apps/backend/src/config.ts @@ -12,6 +12,8 @@ export const config = { myAnimeList: loadMALEnvs(), openai: loadOpenAIEnvs(), google: loadGoogleEnvs(), + monitors: loadMonitorsEnvs(), + telemetry: loadTelemetryEnvs(), } function loadRedisEnvs() { @@ -121,3 +123,20 @@ 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) +} + +function loadTelemetryEnvs() { + const schema = z.object({ + 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/domain/services/tmdb/get-tmdb-data.ts b/apps/backend/src/domain/services/tmdb/get-tmdb-data.ts index 54736ed3..caaba3f2 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,5 @@ import type { FastifyRedis } from '@fastify/redis' + import type { Language } from '@plotwist_app/tmdb' import { tmdb } from '@/infra/adapters/tmdb' diff --git a/apps/backend/src/domain/services/users/update-user.ts b/apps/backend/src/domain/services/users/update-user.ts index a5e2838f..9f6a3a69 100644 --- a/apps/backend/src/domain/services/users/update-user.ts +++ b/apps/backend/src/domain/services/users/update-user.ts @@ -1,3 +1,4 @@ +import type { z } from 'zod' import { NoValidFieldsError } from '@/domain/errors/no-valid-fields' import { UserNotFoundError } from '@/domain/errors/user-not-found' import { UsernameAlreadyRegisteredError } from '@/domain/errors/username-already-registered' @@ -8,7 +9,7 @@ import { import { isUniqueViolation } from '@/infra/db/utils/postgres-errors' import type { updateUserBodySchema } from '@/infra/http/schemas/users' -export type UpdateUserInput = typeof updateUserBodySchema._type +export type UpdateUserInput = z.infer export async function updateUserService({ userId, diff --git a/apps/backend/src/infra/adapters/r2-storage.ts b/apps/backend/src/infra/adapters/r2-storage.ts index 02e317a8..5c22f497 100644 --- a/apps/backend/src/infra/adapters/r2-storage.ts +++ b/apps/backend/src/infra/adapters/r2-storage.ts @@ -76,7 +76,7 @@ async function uploadImage({ const R2Storage: CloudStorage = { deleteOldImages: prefix => deleteOldImages(prefix), - uploadImage: uploadImageInput => uploadImage(uploadImageInput), + uploadImage: input => uploadImage(input), } export { R2Storage } diff --git a/apps/backend/src/infra/db/index.ts b/apps/backend/src/infra/db/index.ts index d809c4a9..a767a91e 100644 --- a/apps/backend/src/infra/db/index.ts +++ b/apps/backend/src/infra/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/infra/db/repositories/reviews-repository.ts b/apps/backend/src/infra/db/repositories/reviews-repository.ts index 19f29dcc..dc57bc6b 100644 --- a/apps/backend/src/infra/db/repositories/reviews-repository.ts +++ b/apps/backend/src/infra/db/repositories/reviews-repository.ts @@ -118,7 +118,7 @@ export async function selectReviews({ ) .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) } diff --git a/apps/backend/src/infra/db/repositories/user-item-repository.ts b/apps/backend/src/infra/db/repositories/user-item-repository.ts index e2a9b9ac..d7c65cba 100644 --- a/apps/backend/src/infra/db/repositories/user-item-repository.ts +++ b/apps/backend/src/infra/db/repositories/user-item-repository.ts @@ -180,7 +180,6 @@ export async function reorderUserItems( _status: string, orderedIds: string[] ) { - // Update position for each item based on array order const updates = orderedIds.map((id, index) => db .update(schema.userItems) diff --git a/apps/backend/src/infra/db/repositories/user-stats.ts b/apps/backend/src/infra/db/repositories/user-stats.ts index 8b785c89..8467c6bf 100644 --- a/apps/backend/src/infra/db/repositories/user-stats.ts +++ b/apps/backend/src/infra/db/repositories/user-stats.ts @@ -3,8 +3,6 @@ import { db } from '..' import { schema } from '../schema' export async function selectUserStats(userId: string) { - // Run all independent queries in parallel instead of sequential transaction - // This improves performance as these are all read-only operations const [ [{ count: followersCount }], [{ count: followingCount }], diff --git a/apps/backend/src/infra/http/routes/follow.ts b/apps/backend/src/infra/http/routes/follow.ts index 636e6e87..ded596cd 100644 --- a/apps/backend/src/infra/http/routes/follow.ts +++ b/apps/backend/src/infra/http/routes/follow.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createFollowController, deleteFollowController, diff --git a/apps/backend/src/infra/http/routes/images.ts b/apps/backend/src/infra/http/routes/images.ts index b45d4fbe..4f54e244 100644 --- a/apps/backend/src/infra/http/routes/images.ts +++ b/apps/backend/src/infra/http/routes/images.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createImageController } from '../controllers/images-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { diff --git a/apps/backend/src/infra/http/routes/import.ts b/apps/backend/src/infra/http/routes/import.ts index d01a45fc..c9bcf1ac 100644 --- a/apps/backend/src/infra/http/routes/import.ts +++ b/apps/backend/src/infra/http/routes/import.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createImportController, getDetailedImportController, diff --git a/apps/backend/src/infra/http/routes/likes.ts b/apps/backend/src/infra/http/routes/likes.ts index 82956ba3..4dcbdb40 100644 --- a/apps/backend/src/infra/http/routes/likes.ts +++ b/apps/backend/src/infra/http/routes/likes.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createLikeController, deleteLikeController, diff --git a/apps/backend/src/infra/http/routes/list-item.ts b/apps/backend/src/infra/http/routes/list-item.ts index 8d0cf504..1f366500 100644 --- a/apps/backend/src/infra/http/routes/list-item.ts +++ b/apps/backend/src/infra/http/routes/list-item.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createListItemController, deleteListItemController, diff --git a/apps/backend/src/infra/http/routes/lists.ts b/apps/backend/src/infra/http/routes/lists.ts index f1c73af6..37c71e8f 100644 --- a/apps/backend/src/infra/http/routes/lists.ts +++ b/apps/backend/src/infra/http/routes/lists.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createListController, deleteListController, diff --git a/apps/backend/src/infra/http/routes/login.ts b/apps/backend/src/infra/http/routes/login.ts index cb236832..1ba77f02 100644 --- a/apps/backend/src/infra/http/routes/login.ts +++ b/apps/backend/src/infra/http/routes/login.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { loginController } from '../controllers/login-controller' import { loginBodySchema, loginResponseSchema } from '../schemas/login' diff --git a/apps/backend/src/infra/http/routes/social-auth.ts b/apps/backend/src/infra/http/routes/social-auth.ts index ed10ceff..233681e3 100644 --- a/apps/backend/src/infra/http/routes/social-auth.ts +++ b/apps/backend/src/infra/http/routes/social-auth.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { appleAuthController, googleAuthController, diff --git a/apps/backend/src/infra/http/routes/social-links.ts b/apps/backend/src/infra/http/routes/social-links.ts index 2829b741..4b0070ee 100644 --- a/apps/backend/src/infra/http/routes/social-links.ts +++ b/apps/backend/src/infra/http/routes/social-links.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { getSocialLinksController, upsertSocialLinksController, diff --git a/apps/backend/src/infra/http/routes/subscriptions.ts b/apps/backend/src/infra/http/routes/subscriptions.ts index 70093408..d5d4bfae 100644 --- a/apps/backend/src/infra/http/routes/subscriptions.ts +++ b/apps/backend/src/infra/http/routes/subscriptions.ts @@ -1,4 +1,5 @@ import type { FastifyInstance } from 'fastify' + import { deleteSubscriptionController } from '../controllers/subscriptions-controller' import { verifyJwt } from '../middlewares/verify-jwt' import { diff --git a/apps/backend/src/infra/http/routes/tmdb-proxy.ts b/apps/backend/src/infra/http/routes/tmdb-proxy.ts index 98eb6a00..457141cc 100644 --- a/apps/backend/src/infra/http/routes/tmdb-proxy.ts +++ b/apps/backend/src/infra/http/routes/tmdb-proxy.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import https from 'https' + import { config } from '@/config' const TMDB_BASE_URL = 'https://api.themoviedb.org/3' diff --git a/apps/backend/src/infra/http/routes/user-activities.ts b/apps/backend/src/infra/http/routes/user-activities.ts index 7cbd54ab..24aabf86 100644 --- a/apps/backend/src/infra/http/routes/user-activities.ts +++ b/apps/backend/src/infra/http/routes/user-activities.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { deleteUserActivityController, getUserActivitiesController, diff --git a/apps/backend/src/infra/http/routes/user-episodes.ts b/apps/backend/src/infra/http/routes/user-episodes.ts index 6f066598..901f8df1 100644 --- a/apps/backend/src/infra/http/routes/user-episodes.ts +++ b/apps/backend/src/infra/http/routes/user-episodes.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createUserEpisodesController, deleteUserEpisodesController, diff --git a/apps/backend/src/infra/http/routes/user-items.ts b/apps/backend/src/infra/http/routes/user-items.ts index 3799cf2a..93c1afd8 100644 --- a/apps/backend/src/infra/http/routes/user-items.ts +++ b/apps/backend/src/infra/http/routes/user-items.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { deleteUserItemController, getAllUserItemsController, diff --git a/apps/backend/src/infra/http/routes/user-stats.ts b/apps/backend/src/infra/http/routes/user-stats.ts index 14c34060..71afdf0f 100644 --- a/apps/backend/src/infra/http/routes/user-stats.ts +++ b/apps/backend/src/infra/http/routes/user-stats.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { getUserBestReviewsController, getUserItemsStatusController, diff --git a/apps/backend/src/infra/http/routes/users.ts b/apps/backend/src/infra/http/routes/users.ts index cd394afe..d85fd83f 100644 --- a/apps/backend/src/infra/http/routes/users.ts +++ b/apps/backend/src/infra/http/routes/users.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createUserController, deleteUserController, diff --git a/apps/backend/src/infra/http/routes/watch-entries.ts b/apps/backend/src/infra/http/routes/watch-entries.ts index 4e8130cf..536f8f46 100644 --- a/apps/backend/src/infra/http/routes/watch-entries.ts +++ b/apps/backend/src/infra/http/routes/watch-entries.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { createWatchEntryController, deleteWatchEntryController, diff --git a/apps/backend/src/infra/http/routes/webhook.ts b/apps/backend/src/infra/http/routes/webhook.ts index a7ad56ba..05e2135c 100644 --- a/apps/backend/src/infra/http/routes/webhook.ts +++ b/apps/backend/src/infra/http/routes/webhook.ts @@ -1,5 +1,6 @@ import type { FastifyInstance } from 'fastify' import type { ZodTypeProvider } from 'fastify-type-provider-zod' + import { stripeWebhookController } from '../controllers/stripe-webhook-controller' export async function webhookRoutes(app: FastifyInstance) { diff --git a/apps/backend/src/infra/http/server.ts b/apps/backend/src/infra/http/server.ts index e20e782f..e901a0f2 100644 --- a/apps/backend/src/infra/http/server.ts +++ b/apps/backend/src/infra/http/server.ts @@ -6,15 +6,19 @@ 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 { config } from '../../config' +import { registerHttpRequestMetrics } from '@/infra/telemetry/http-request-metrics' +import { fastifyOtel } from '@/infra/telemetry/otel' import { routes } from './routes' 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) @@ -85,16 +89,15 @@ 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) - 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/http/transform-schema.ts b/apps/backend/src/infra/http/transform-schema.ts index b5ff85c4..db25f211 100644 --- a/apps/backend/src/infra/http/transform-schema.ts +++ b/apps/backend/src/infra/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/infra/telemetry/dash.json b/apps/backend/src/infra/telemetry/dash.json new file mode 100644 index 00000000..210c3900 --- /dev/null +++ b/apps/backend/src/infra/telemetry/dash.json @@ -0,0 +1,1133 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "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": "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": 0, "y": 1 }, + "id": 12, + "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": "process_cpu_utilization{job=\"plotwist-api\"} or process_cpu_utilization", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Process 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": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, + "unit": "bytes" + }, + "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" } + }, + "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" + }, + "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" } + }, + "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": "System memory utilization", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "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 + } + ] + }, + "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" + }, + "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" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "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": [ + { + "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", + "refId": "A", + "serviceMapUseNativeHistograms": false, + "tableType": "traces" + } + ], + "title": "All failed requests", + "type": "timeseries" + }, + { + "datasource": { + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 3, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "#EAB839", + "value": 200 + }, + { + "color": "red", + "value": 400 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 3, + "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", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "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" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "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" + }, + { + "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", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 6, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "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" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "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" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 8, + "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", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "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" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "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" + } + ], + "title": "Metrics", + "type": "row" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Plotwist", + "uid": "g6wwd8", + "version": 24 +} 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..bb7e9cf9 --- /dev/null +++ b/apps/backend/src/infra/telemetry/http-request-metrics.ts @@ -0,0 +1,47 @@ +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', + }) + const requestDuration = meter.createHistogram( + 'http.server.request.duration', + { + description: 'HTTP server request duration', + unit: 's', + } + ) + + 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() + }) +} 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..78ce77f0 --- /dev/null +++ b/apps/backend/src/infra/telemetry/monitor-metrics.ts @@ -0,0 +1,52 @@ +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< + Record<(typeof monitorMetricNames)[keyof typeof monitorMetricNames], number> +> = {} + +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/infra/telemetry/otel.ts b/apps/backend/src/infra/telemetry/otel.ts new file mode 100644 index 00000000..a3264765 --- /dev/null +++ b/apps/backend/src/infra/telemetry/otel.ts @@ -0,0 +1,93 @@ +import FastifyOtel from '@fastify/otel' +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' +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, +} from '@opentelemetry/semantic-conventions' +import { config } from '@/config' +import { logger } from '../adapters/logger' + +const LOCALHOST_OTLP = 'http://localhost:4318' + +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) + : {} + + 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() + .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: tracesUrl, headers }), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ url: metricsUrl, headers }), + }), +}) + +logger.info('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()) + +export { fastifyOtel } diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index d3411405..d07ac62b 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,9 +1,13 @@ -import { startServer } from './infra/http/server' -import { startWorkers } from './worker' +import '@/infra/telemetry/otel' + +import { startServer } from '@/infra/http/server' +import { startMonitors } from '@/monitors/monitor' +import { startWorkers } from '@/workers/worker' async function main() { startWorkers() - startServer() + startMonitors() + await startServer() } main().catch(err => { diff --git a/apps/backend/src/monitors/monitor.ts b/apps/backend/src/monitors/monitor.ts new file mode 100644 index 00000000..7d88fce3 --- /dev/null +++ b/apps/backend/src/monitors/monitor.ts @@ -0,0 +1,62 @@ +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' +import { monitorTotalSubscriptions } from './total-subscriptions' +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() + .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() + .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() + .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() + .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() + .then(v => { + if (v != null) + setMonitorMetric(monitorMetricNames.todayNewSubscriptions, v) + }) + .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 new file mode 100644 index 00000000..5568891b --- /dev/null +++ b/apps/backend/src/monitors/total-users.ts @@ -0,0 +1,12 @@ +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}`) + return totalUsers +} 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() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 288e4e77..6d5fd28d 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 @@ -56,6 +59,36 @@ 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 + '@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) + '@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) + '@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 +130,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 +254,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 +419,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 +567,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: {} @@ -2150,6 +2183,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==} @@ -2453,6 +2491,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'} @@ -2570,6 +2615,212 @@ 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'} + + '@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/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} + 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/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} + 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} + 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 +4707,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 +5143,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==} @@ -5809,6 +6068,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==} @@ -6090,6 +6352,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 +7103,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 +7651,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 +7955,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==} @@ -8127,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'} @@ -10575,6 +10857,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 @@ -10883,6 +11175,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': @@ -11005,6 +11302,287 @@ 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)': + 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/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 + '@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/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 + '@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 + '@opentelemetry/api-logs': 0.211.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + 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 + '@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 +13680,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 +13696,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 +13742,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 +13766,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 +14204,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 +14661,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: @@ -14624,6 +15209,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): @@ -14977,6 +15564,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 +16493,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 +16549,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 +16571,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 +16596,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 +16661,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 +17113,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 +17518,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)): @@ -17461,6 +18080,8 @@ snapshots: symbol-tree@3.2.4: {} + systeminformation@5.30.3: {} + tagged-tag@1.0.0: {} tailwind-merge@3.4.0: {} @@ -17987,7 +18608,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 +18631,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