From 9451cd7cc0f6dd7f6a7ed33222752c33ce545904 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Thu, 2 Apr 2026 18:21:30 +0800 Subject: [PATCH] feat(otel): server instrumentation script --- apps/server/instrumentation.mjs | 31 +++++++++++++++++++++++++++++++ apps/server/package.json | 7 ++++--- apps/server/src/libs/db.ts | 8 ++++++-- apps/server/src/libs/otel.ts | 19 ++++--------------- pnpm-lock.yaml | 3 +++ 5 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 apps/server/instrumentation.mjs diff --git a/apps/server/instrumentation.mjs b/apps/server/instrumentation.mjs new file mode 100644 index 000000000..2cdc6e6de --- /dev/null +++ b/apps/server/instrumentation.mjs @@ -0,0 +1,31 @@ +/** + * OTEL instrumentation preload — loaded via `--import` BEFORE tsx processes + * any application module. This ensures @opentelemetry/instrumentation-pg can + * monkey-patch the CJS `pg` module before it is imported anywhere. + * + * Only instrumentations that patch third-party modules need to live here. + * The full SDK (exporters, metrics, log processors) is still configured in + * src/libs/otel.ts — the NodeSDK there will reuse the already-registered + * instrumentations. + * + * NOTICE: `pg` and `ioredis` are CJS packages. When ESM code does + * `import pg from 'pg'`, Node.js internally calls `require()` to load + * the CJS module, so `require-in-the-middle` hooks still intercept it. + */ + +import { registerInstrumentations } from '@opentelemetry/instrumentation' +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http' +import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis' +import { PgInstrumentation } from '@opentelemetry/instrumentation-pg' + +registerInstrumentations({ + instrumentations: [ + new HttpInstrumentation({ + ignoreIncomingRequestHook: req => req.url === '/health', + }), + new PgInstrumentation({ + enhancedDatabaseReporting: true, + }), + new IORedisInstrumentation(), + ], +}) diff --git a/apps/server/package.json b/apps/server/package.json index 62b4b252c..48cc5a202 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -6,9 +6,9 @@ "scripts": { "apply:env": "dotenvx run -f .env.local --overload --ignore=MISSING_ENV_FILE", "auth:generate": "pnpm run apply:env -- better-auth generate --config src/scripts/auth.ts --output src/schemas/accounts.ts -y", - "dev": "pnpm run apply:env -- tsx --watch src/bin/run.ts api", - "start": "pnpm run apply:env -- tsx src/bin/run.ts api", - "server": "pnpm run apply:env -- tsx src/bin/run.ts", + "dev": "pnpm run apply:env -- tsx --import ./instrumentation.mjs --watch src/bin/run.ts api", + "start": "pnpm run apply:env -- tsx --import ./instrumentation.mjs src/bin/run.ts api", + "server": "pnpm run apply:env -- tsx --import ./instrumentation.mjs src/bin/run.ts", "build": "tsc -b", "typecheck": "tsc --noEmit", "db:generate": "drizzle-kit generate", @@ -29,6 +29,7 @@ "@opentelemetry/exporter-logs-otlp-proto": "^0.214.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.214.0", + "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-http": "^0.214.0", "@opentelemetry/instrumentation-ioredis": "^0.62.0", "@opentelemetry/instrumentation-pg": "^0.66.0", diff --git a/apps/server/src/libs/db.ts b/apps/server/src/libs/db.ts index e9fd2de39..58c83a9e7 100644 --- a/apps/server/src/libs/db.ts +++ b/apps/server/src/libs/db.ts @@ -1,10 +1,11 @@ import type { Env } from './env' +import pg from 'pg' + import { useLogger } from '@guiiai/logg' import { migrate } from '@proj-airi/drizzle-orm-browser-migrator/pg' import { migrations } from '@proj-airi/server-schema' import { drizzle } from 'drizzle-orm/node-postgres' -import { Pool } from 'pg' import * as fullSchema from '../schemas' @@ -14,8 +15,11 @@ export type Database = ReturnType['db'] type DrizzleEnv = Pick +// NOTICE: pg is imported statically here. The OTEL instrumentation hooks are +// registered via --import ./instrumentation.mjs (preload) which runs before +// tsx loads application modules, allowing require-in-the-middle to patch pg. export function createDrizzle(env: DrizzleEnv) { - const pool = new Pool({ + const pool = new pg.Pool({ connectionString: env.DATABASE_URL, max: env.DB_POOL_MAX, idleTimeoutMillis: env.DB_POOL_IDLE_TIMEOUT_MS, diff --git a/apps/server/src/libs/otel.ts b/apps/server/src/libs/otel.ts index 8aca9efe9..675f9c21b 100644 --- a/apps/server/src/libs/otel.ts +++ b/apps/server/src/libs/otel.ts @@ -1,5 +1,3 @@ -import type { IncomingMessage } from 'node:http' - import type { Counter, Histogram, UpDownCounter } from '@opentelemetry/api' import type { Env } from './env' @@ -12,9 +10,6 @@ import { logs, SeverityNumber } from '@opentelemetry/api-logs' import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto' import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' -import { HttpInstrumentation } from '@opentelemetry/instrumentation-http' -import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis' -import { PgInstrumentation } from '@opentelemetry/instrumentation-pg' import { RuntimeNodeInstrumentation } from '@opentelemetry/instrumentation-runtime-node' import { resourceFromAttributes } from '@opentelemetry/resources' import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs' @@ -172,17 +167,11 @@ export function initOtel(env: Env): OtelInstance | undefined { exportTimeoutMillis: 10_000, })], logRecordProcessors: [new BatchLogRecordProcessor(logExporter)], + // NOTICE: HttpInstrumentation, PgInstrumentation, and IORedisInstrumentation + // are registered in instrumentation.cjs (loaded via --require) so that + // require-in-the-middle can patch the CJS modules before tsx's ESM loader + // imports them. Only non-patching instrumentations belong here. instrumentations: [ - new HttpInstrumentation({ - ignoreIncomingRequestHook: (req: IncomingMessage) => { - // Ignore health check requests to reduce noise - return req.url === '/health' - }, - }), - new PgInstrumentation({ - enhancedDatabaseReporting: true, - }), - new IORedisInstrumentation(), new RuntimeNodeInstrumentation(), ], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d32ca7da..2d1162db1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -563,6 +563,9 @@ importers: '@opentelemetry/exporter-trace-otlp-proto': specifier: ^0.214.0 version: 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': + specifier: ^0.214.0 + version: 0.214.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation-http': specifier: ^0.214.0 version: 0.214.0(@opentelemetry/api@1.9.1)