diff --git a/apps/processor/package.json b/apps/processor/package.json index 20963ef..b11796c 100644 --- a/apps/processor/package.json +++ b/apps/processor/package.json @@ -7,6 +7,14 @@ "start": "node dist/index.js" }, "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/auto-instrumentations-node": "^0.70.1", + "@opentelemetry/exporter-logs-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.212.0", + "@opentelemetry/sdk-logs": "^0.212.0", + "@opentelemetry/sdk-metrics": "^2.1.0", + "@opentelemetry/sdk-node": "^0.212.0", "@hono/node-server": "^1.19.9", "@pkg/db": "workspace:*", "@pkg/logger": "workspace:*", @@ -14,6 +22,7 @@ "@pkg/result": "workspace:*", "@pkg/settings": "workspace:*", "hono": "^4.11.1", + "import-in-the-middle": "^3.0.0", "valibot": "^1.2.0" }, "devDependencies": { diff --git a/apps/processor/src/index.ts b/apps/processor/src/index.ts index a9868a5..672db07 100644 --- a/apps/processor/src/index.ts +++ b/apps/processor/src/index.ts @@ -1,8 +1,12 @@ +import "./instrumentation.js"; + import { mobileRouter } from "./domains/mobile/router.js"; +import { httpTelemetryMiddleware } from "./telemetry/http.middleware.js"; import { serve } from "@hono/node-server"; import { Hono } from "hono"; const app = new Hono(); +app.use("*", httpTelemetryMiddleware); app.get("/health", (c) => { return c.json({ ok: true }); diff --git a/apps/processor/src/instrumentation.ts b/apps/processor/src/instrumentation.ts new file mode 100644 index 0000000..6b3ec48 --- /dev/null +++ b/apps/processor/src/instrumentation.ts @@ -0,0 +1,50 @@ +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; +import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; +import { createAddHookMessageChannel } from "import-in-the-middle"; +import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { settings } from "@pkg/settings"; +import { register } from "node:module"; + +const { registerOptions } = createAddHookMessageChannel(); +register("import-in-the-middle/hook.mjs", import.meta.url, registerOptions); + +const normalizedEndpoint = settings.otelExporterOtlpHttpEndpoint.startsWith( + "http", +) + ? settings.otelExporterOtlpHttpEndpoint + : `http://${settings.otelExporterOtlpHttpEndpoint}`; + +if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = normalizedEndpoint; +} + +const sdk = new NodeSDK({ + serviceName: `${settings.otelServiceName}-processor`, + traceExporter: new OTLPTraceExporter(), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + exportIntervalMillis: 10_000, + }), + logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())], + instrumentations: [ + getNodeAutoInstrumentations({ + "@opentelemetry/instrumentation-winston": { + // We add OpenTelemetryTransportV3 explicitly in @pkg/logger. + disableLogSending: true, + }, + }), + ], +}); + +sdk.start(); + +const shutdown = () => { + void sdk.shutdown(); +}; + +process.on("SIGTERM", shutdown); +process.on("SIGINT", shutdown); diff --git a/apps/processor/src/telemetry/http.middleware.ts b/apps/processor/src/telemetry/http.middleware.ts new file mode 100644 index 0000000..7c56c3a --- /dev/null +++ b/apps/processor/src/telemetry/http.middleware.ts @@ -0,0 +1,77 @@ +import { context, metrics, SpanStatusCode, trace } from "@opentelemetry/api"; +import type { MiddlewareHandler } from "hono"; + +const tracer = trace.getTracer("processor.http"); +const meter = metrics.getMeter("processor.http"); + +const requestCount = meter.createCounter("processor.http.server.requests", { + description: "Total number of processor HTTP requests", +}); + +const requestDuration = meter.createHistogram( + "processor.http.server.duration", + { + description: "Processor HTTP request duration", + unit: "ms", + }, +); + +const activeRequests = meter.createUpDownCounter( + "processor.http.server.active_requests", + { + description: "Number of in-flight processor HTTP requests", + }, +); + +export const httpTelemetryMiddleware: MiddlewareHandler = async (c, next) => { + const startedAt = performance.now(); + const method = c.req.method; + const route = c.req.path; + + const span = tracer.startSpan(`http ${method} ${route}`, { + attributes: { + "http.request.method": method, + "url.path": route, + }, + }); + + activeRequests.add(1, { + "http.request.method": method, + "url.path": route, + }); + + try { + await context.with(trace.setSpan(context.active(), span), next); + + span.setAttribute("http.response.status_code", c.res.status); + span.setStatus({ + code: + c.res.status >= 500 + ? SpanStatusCode.ERROR + : SpanStatusCode.OK, + }); + } catch (error) { + span.recordException(error as Error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: "Unhandled processor request error", + }); + throw error; + } finally { + const durationMs = performance.now() - startedAt; + const attrs = { + "http.request.method": method, + "http.response.status_code": c.res.status, + "url.path": route, + }; + + requestCount.add(1, attrs); + requestDuration.record(durationMs, attrs); + activeRequests.add(-1, { + "http.request.method": method, + "url.path": route, + }); + + span.end(); + } +}; diff --git a/packages/logic/package.json b/packages/logic/package.json index 0aeeca0..2744d43 100644 --- a/packages/logic/package.json +++ b/packages/logic/package.json @@ -1,5 +1,6 @@ { "name": "@pkg/logic", + "type": "module", "scripts": { "auth:schemagen": "pnpm dlx @better-auth/cli generate --config ./domains/auth/config.base.ts --output ../../packages/db/schema/better.auth.schema.ts" }, diff --git a/packages/result/package.json b/packages/result/package.json index 7132515..449a414 100644 --- a/packages/result/package.json +++ b/packages/result/package.json @@ -1,5 +1,7 @@ { "name": "@pkg/result", + "module": "index.ts", + "type": "module", "devDependencies": { "@types/bun": "latest" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9209a8..f5ed070 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,30 @@ importers: '@hono/node-server': specifier: ^1.19.9 version: 1.19.9(hono@4.12.3) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.70.1 + version: 0.70.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-logs-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.1.0 + version: 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@pkg/db': specifier: workspace:* version: link:../../packages/db @@ -213,6 +237,9 @@ importers: hono: specifier: ^4.11.1 version: 4.12.3 + import-in-the-middle: + specifier: ^3.0.0 + version: 3.0.0 valibot: specifier: ^1.2.0 version: 1.2.0(typescript@5.9.3)