|
6 | 6 | * - Vendored from: https://github.com/prisma/prisma/tree/b6feea5565ec577545a79547d24273ccdd11b4c7/packages/instrumentation |
7 | 7 | * - Upstream version: @prisma/instrumentation@7.8.0 |
8 | 8 | * - Replaced `@prisma/instrumentation-contract` imports with local vendored types |
9 | | - * - Minor TypeScript strictness adjustments for this repository's compiler settings |
| 9 | + * - Span creation uses Sentry's span APIs (`startSpanManual` / `startInactiveSpan`) instead of the OTel tracer |
| 10 | + * - Span creation sets the Sentry origin, renames `db_query` spans to their SQL text, and backfills |
| 11 | + * `db.system` for older Prisma versions |
10 | 12 | */ |
11 | | -/* eslint-disable */ |
12 | 13 |
|
| 14 | +import type { Span, SpanAttributes, SpanKindValue, SpanLink } from '@sentry/core'; |
13 | 15 | import { |
14 | | - Attributes, |
15 | | - Context, |
16 | | - context as _context, |
17 | | - Span, |
18 | | - SpanKind, |
19 | | - SpanOptions, |
20 | | - trace, |
21 | | - Tracer, |
22 | | - TracerProvider, |
23 | | -} from '@opentelemetry/api'; |
24 | | -import type { EngineSpan, EngineSpanKind, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types'; |
| 16 | + getActiveSpan, |
| 17 | + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, |
| 18 | + SPAN_KIND, |
| 19 | + startInactiveSpan, |
| 20 | + startSpanManual, |
| 21 | +} from '@sentry/core'; |
| 22 | +import type { EngineSpan, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types'; |
25 | 23 |
|
26 | 24 | const showAllTraces = process.env.PRISMA_SHOW_ALL_TRACES === 'true'; |
27 | 25 |
|
28 | 26 | const nonSampledTraceParent = `00-10-10-00`; |
29 | 27 |
|
| 28 | +const PRISMA_ORIGIN = 'auto.db.otel.prisma'; |
| 29 | + |
30 | 30 | type Options = { |
31 | | - tracerProvider: TracerProvider; |
32 | 31 | ignoreSpanTypes: (string | RegExp)[]; |
33 | 32 | }; |
34 | 33 |
|
35 | | -function engineSpanKindToOtelSpanKind(engineSpanKind: EngineSpanKind): SpanKind { |
36 | | - switch (engineSpanKind) { |
37 | | - case 'client': |
38 | | - return SpanKind.CLIENT; |
39 | | - case 'internal': |
40 | | - default: |
41 | | - return SpanKind.INTERNAL; |
| 34 | +/** |
| 35 | + * Older Prisma versions emit `prisma:engine:db_query` spans without a `db.system`, so it's backfilled here. |
| 36 | + */ |
| 37 | +function buildSpanAttributes(name: string, attributes: Record<string, unknown> | undefined): SpanAttributes { |
| 38 | + const merged: SpanAttributes = { |
| 39 | + ...(attributes as SpanAttributes | undefined), |
| 40 | + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN, |
| 41 | + }; |
| 42 | + |
| 43 | + if (name === 'prisma:engine:db_query' && merged['db.system'] == null) { |
| 44 | + merged['db.system'] = 'prisma'; |
| 45 | + } |
| 46 | + |
| 47 | + return merged; |
| 48 | +} |
| 49 | + |
| 50 | +/** |
| 51 | + * Db query spans are named after their SQL text (e.g. `SELECT * FROM "User"`) rather than the generic |
| 52 | + * engine name. v5/v6 emit `prisma:engine:db_query`; v7 inlined the engine and emits `prisma:client:db_query`. |
| 53 | + */ |
| 54 | +function buildSpanName(name: string, attributes: SpanAttributes): string { |
| 55 | + const queryText = attributes['db.query.text']; |
| 56 | + if ((name === 'prisma:engine:db_query' || name === 'prisma:client:db_query') && typeof queryText === 'string') { |
| 57 | + return queryText; |
42 | 58 | } |
| 59 | + return name; |
43 | 60 | } |
44 | 61 |
|
45 | 62 | export class ActiveTracingHelper implements TracingHelper { |
46 | | - private tracerProvider: TracerProvider; |
47 | 63 | private ignoreSpanTypes: (string | RegExp)[]; |
48 | 64 |
|
49 | | - constructor({ tracerProvider, ignoreSpanTypes }: Options) { |
50 | | - this.tracerProvider = tracerProvider; |
| 65 | + public constructor({ ignoreSpanTypes }: Options) { |
51 | 66 | this.ignoreSpanTypes = ignoreSpanTypes; |
52 | 67 | } |
53 | 68 |
|
54 | | - isEnabled(): boolean { |
| 69 | + public isEnabled(): boolean { |
55 | 70 | return true; |
56 | 71 | } |
57 | 72 |
|
58 | | - getTraceParent(context?: Context | undefined): string { |
59 | | - const span = trace.getSpanContext(context ?? _context.active()); |
60 | | - if (span) { |
61 | | - return `00-${span.traceId}-${span.spanId}-0${span.traceFlags}`; |
| 73 | + public getTraceParent(span?: Span): string { |
| 74 | + const spanContext = (span ?? getActiveSpan())?.spanContext(); |
| 75 | + if (spanContext) { |
| 76 | + return `00-${spanContext.traceId}-${spanContext.spanId}-0${spanContext.traceFlags}`; |
62 | 77 | } |
63 | 78 | return nonSampledTraceParent; |
64 | 79 | } |
65 | 80 |
|
66 | | - dispatchEngineSpans(spans: EngineSpan[]): void { |
67 | | - const tracer = this.tracerProvider.getTracer('prisma'); |
| 81 | + public dispatchEngineSpans(spans: EngineSpan[]): void { |
68 | 82 | const linkIds = new Map<string, string>(); |
69 | 83 | const roots = spans.filter(span => span.parentId === null); |
70 | 84 |
|
71 | 85 | for (const root of roots) { |
72 | | - dispatchEngineSpan(tracer, root, spans, linkIds, this.ignoreSpanTypes); |
| 86 | + dispatchEngineSpan(root, spans, linkIds, this.ignoreSpanTypes); |
73 | 87 | } |
74 | 88 | } |
75 | 89 |
|
76 | | - getActiveContext(): Context | undefined { |
77 | | - return _context.active(); |
| 90 | + public getActiveContext(): Span | undefined { |
| 91 | + return getActiveSpan(); |
78 | 92 | } |
79 | 93 |
|
80 | | - runInChildSpan<R>(options: string | ExtendedSpanOptions, callback: SpanCallback<R>): R { |
81 | | - if (typeof options === 'string') { |
82 | | - options = { name: options }; |
83 | | - } |
| 94 | + public runInChildSpan<R>(nameOrOptions: string | ExtendedSpanOptions, callback: SpanCallback<R>): R { |
| 95 | + const options: ExtendedSpanOptions = typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions; |
84 | 96 |
|
85 | 97 | if (options.internal && !showAllTraces) { |
86 | 98 | return callback(); |
87 | 99 | } |
88 | 100 |
|
89 | | - const tracer = this.tracerProvider.getTracer('prisma'); |
90 | | - const context = options.context ?? this.getActiveContext(); |
91 | 101 | const name = `prisma:client:${options.name}`; |
92 | 102 |
|
93 | 103 | if (shouldIgnoreSpan(name, this.ignoreSpanTypes)) { |
94 | 104 | return callback(); |
95 | 105 | } |
96 | 106 |
|
| 107 | + const parentSpan = getActiveSpan(); |
| 108 | + |
| 109 | + const attributes = buildSpanAttributes(name, options.attributes as Record<string, unknown> | undefined); |
| 110 | + const spanOptions = { |
| 111 | + name: buildSpanName(name, attributes), |
| 112 | + attributes, |
| 113 | + kind: options.kind as SpanKindValue | undefined, |
| 114 | + links: options.links as SpanLink[] | undefined, |
| 115 | + startTime: options.startTime, |
| 116 | + parentSpan, |
| 117 | + }; |
| 118 | + |
97 | 119 | if (options.active === false) { |
98 | | - const span = tracer.startSpan(name, options, context); |
99 | | - return endSpan(span, callback(span, context)); |
| 120 | + const span = startInactiveSpan(spanOptions); |
| 121 | + return endSpan(span, () => callback(span, parentSpan)); |
100 | 122 | } |
101 | 123 |
|
102 | | - return tracer.startActiveSpan(name, options, span => endSpan(span, callback(span, context))); |
| 124 | + return startSpanManual(spanOptions, span => endSpan(span, () => callback(span, parentSpan))); |
103 | 125 | } |
104 | 126 | } |
105 | 127 |
|
106 | 128 | function dispatchEngineSpan( |
107 | | - tracer: Tracer, |
108 | 129 | engineSpan: EngineSpan, |
109 | 130 | allSpans: EngineSpan[], |
110 | 131 | linkIds: Map<string, string>, |
111 | 132 | ignoreSpanTypes: (string | RegExp)[], |
112 | | -) { |
113 | | - if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) return; |
114 | | - |
115 | | - const spanOptions = { |
116 | | - attributes: engineSpan.attributes as Attributes, |
117 | | - kind: engineSpanKindToOtelSpanKind(engineSpan.kind), |
118 | | - startTime: engineSpan.startTime, |
119 | | - } satisfies SpanOptions; |
120 | | - |
121 | | - tracer.startActiveSpan(engineSpan.name, spanOptions, span => { |
122 | | - linkIds.set(engineSpan.id, span.spanContext().spanId); |
123 | | - |
124 | | - if (engineSpan.links) { |
125 | | - span.addLinks( |
126 | | - engineSpan.links.flatMap(link => { |
127 | | - const linkedId = linkIds.get(link); |
128 | | - if (!linkedId) { |
129 | | - return []; |
130 | | - } |
131 | | - return { |
132 | | - context: { |
133 | | - spanId: linkedId, |
134 | | - traceId: span.spanContext().traceId, |
135 | | - traceFlags: span.spanContext().traceFlags, |
136 | | - }, |
137 | | - }; |
138 | | - }), |
139 | | - ); |
140 | | - } |
141 | | - |
142 | | - const children = allSpans.filter(s => s.parentId === engineSpan.id); |
143 | | - for (const child of children) { |
144 | | - dispatchEngineSpan(tracer, child, allSpans, linkIds, ignoreSpanTypes); |
145 | | - } |
| 133 | +): void { |
| 134 | + if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) { |
| 135 | + return; |
| 136 | + } |
146 | 137 |
|
147 | | - span.end(engineSpan.endTime); |
148 | | - }); |
| 138 | + const attributes = buildSpanAttributes(engineSpan.name, engineSpan.attributes); |
| 139 | + |
| 140 | + startSpanManual( |
| 141 | + { |
| 142 | + name: buildSpanName(engineSpan.name, attributes), |
| 143 | + attributes, |
| 144 | + kind: engineSpan.kind === 'client' ? SPAN_KIND.CLIENT : SPAN_KIND.INTERNAL, |
| 145 | + startTime: engineSpan.startTime, |
| 146 | + }, |
| 147 | + span => { |
| 148 | + linkIds.set(engineSpan.id, span.spanContext().spanId); |
| 149 | + |
| 150 | + if (engineSpan.links) { |
| 151 | + span.addLinks( |
| 152 | + engineSpan.links.flatMap(link => { |
| 153 | + const linkedId = linkIds.get(link); |
| 154 | + if (!linkedId) { |
| 155 | + return []; |
| 156 | + } |
| 157 | + return { |
| 158 | + context: { |
| 159 | + spanId: linkedId, |
| 160 | + traceId: span.spanContext().traceId, |
| 161 | + traceFlags: span.spanContext().traceFlags, |
| 162 | + }, |
| 163 | + }; |
| 164 | + }), |
| 165 | + ); |
| 166 | + } |
| 167 | + |
| 168 | + const children = allSpans.filter(s => s.parentId === engineSpan.id); |
| 169 | + for (const child of children) { |
| 170 | + dispatchEngineSpan(child, allSpans, linkIds, ignoreSpanTypes); |
| 171 | + } |
| 172 | + |
| 173 | + span.end(engineSpan.endTime); |
| 174 | + }, |
| 175 | + ); |
149 | 176 | } |
150 | 177 |
|
151 | | -function endSpan<T>(span: Span, result: T): T { |
| 178 | +function endSpan<T>(span: Span, run: () => T): T { |
| 179 | + let result: T; |
| 180 | + try { |
| 181 | + result = run(); |
| 182 | + } catch (reason) { |
| 183 | + span.end(); |
| 184 | + throw reason; |
| 185 | + } |
| 186 | + |
152 | 187 | if (isPromiseLike(result)) { |
153 | 188 | return result.then( |
154 | 189 | value => { |
|
0 commit comments