Skip to content

Commit d44d9ea

Browse files
authored
feat(instrumentation): add ability to filter span by PrismaLayerType (#20113)
Add ability to filter out some of the instrumentation span generated by prisma Related #14640 #14640 (comment)
1 parent 31381be commit d44d9ea

File tree

10 files changed

+152
-17
lines changed

10 files changed

+152
-17
lines changed

packages/client/tests/functional/tracing-disabled/tests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { context } from '@opentelemetry/api'
2-
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
2+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
33
import { Resource } from '@opentelemetry/resources'
44
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
55
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
@@ -13,7 +13,7 @@ declare let prisma: PrismaClient
1313
let inMemorySpanExporter: InMemorySpanExporter
1414

1515
beforeAll(() => {
16-
const contextManager = new AsyncHooksContextManager().enable()
16+
const contextManager = new AsyncLocalStorageContextManager().enable()
1717
context.setGlobalContextManager(contextManager)
1818

1919
inMemorySpanExporter = new InMemorySpanExporter()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { defineMatrix } from '../_utils/defineMatrix'
2+
import { allProviders } from '../_utils/providers'
3+
4+
export default defineMatrix(() => [allProviders])
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { idForProvider } from '../../_utils/idForProvider'
2+
import testMatrix from '../_matrix'
3+
4+
export default testMatrix.setupSchema(({ provider }) => {
5+
return /* Prisma */ `
6+
generator client {
7+
provider = "prisma-client-js"
8+
}
9+
10+
datasource db {
11+
provider = "${provider}"
12+
url = env("DATABASE_URI_${provider}")
13+
}
14+
15+
model User {
16+
id ${idForProvider(provider)}
17+
}
18+
`
19+
})
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { context } from '@opentelemetry/api'
2+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
3+
import { registerInstrumentations } from '@opentelemetry/instrumentation'
4+
import { Resource } from '@opentelemetry/resources'
5+
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
6+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
7+
import { PrismaInstrumentation } from '@prisma/instrumentation'
8+
9+
import { NewPrismaClient } from '../_utils/types'
10+
import testMatrix from './_matrix'
11+
// @ts-ignore
12+
import type { PrismaClient } from './node_modules/@prisma/client'
13+
14+
let prisma: PrismaClient<{ log: [{ emit: 'event'; level: 'query' }] }>
15+
declare let newPrismaClient: NewPrismaClient<typeof PrismaClient>
16+
17+
let inMemorySpanExporter: InMemorySpanExporter
18+
19+
beforeAll(() => {
20+
const contextManager = new AsyncLocalStorageContextManager().enable()
21+
context.setGlobalContextManager(contextManager)
22+
23+
inMemorySpanExporter = new InMemorySpanExporter()
24+
25+
const basicTracerProvider = new BasicTracerProvider({
26+
resource: new Resource({
27+
[ATTR_SERVICE_NAME]: 'test-name',
28+
[ATTR_SERVICE_VERSION]: '1.0.0',
29+
}),
30+
})
31+
32+
basicTracerProvider.addSpanProcessor(new SimpleSpanProcessor(inMemorySpanExporter))
33+
basicTracerProvider.register()
34+
35+
registerInstrumentations({
36+
instrumentations: [
37+
new PrismaInstrumentation({
38+
middleware: true,
39+
ignoreSpanTypes: ['prisma:engine:connection', /prisma:client:operat.*/],
40+
}),
41+
],
42+
})
43+
})
44+
45+
afterAll(() => {
46+
context.disable()
47+
})
48+
49+
testMatrix.setupTestSuite(
50+
({ clientRuntime, engineType }) => {
51+
beforeAll(() => {
52+
inMemorySpanExporter.reset()
53+
prisma = newPrismaClient({ log: [{ emit: 'event', level: 'query' }] })
54+
})
55+
56+
test('should filter out spans and their children based on name', async () => {
57+
await prisma.$connect()
58+
await prisma.user.findMany()
59+
60+
const spans = inMemorySpanExporter.getFinishedSpans()
61+
62+
const expectedSpans = [
63+
'prisma:client:detect_platform',
64+
'prisma:client:load_engine',
65+
// 'prisma:engine:connection', <-- Filtered out individually
66+
'prisma:engine:connect',
67+
'prisma:client:connect',
68+
'prisma:client:serialize',
69+
// 'prisma:engine:connection', <-- Child span of filtered out parent span 'prisma:engine:query'
70+
// 'prisma:engine:db_query', <-- Child span of filtered out parent span 'prisma:engine:query'
71+
// 'prisma:engine:serialize', <-- Child span of filtered out parent span 'prisma:engine:query'
72+
// 'prisma:engine:response_json_serialization', <-- Child span of filtered out parent span 'prisma:engine:query'
73+
// 'prisma:engine:query', <-- Child span of filtered out parent span 'prisma:client:operation'
74+
// 'prisma:client:operation', <-- Filtered out parent span (by regex)
75+
]
76+
77+
if (clientRuntime === 'wasm') {
78+
expectedSpans.shift() // With wasm we do not perform platform detection
79+
} else if (engineType === 'client') {
80+
expectedSpans.splice(0, 4) // Client engine performs no binary engine related spans
81+
}
82+
83+
expect(spans.map((span) => span.name)).toEqual(expectedSpans)
84+
})
85+
},
86+
{
87+
skipDefaultClientInstance: true,
88+
skipDataProxy: {
89+
runtimes: ['edge', 'node', 'wasm', 'client'],
90+
reason: 'Data proxy creates different traces',
91+
},
92+
},
93+
)

packages/client/tests/functional/tracing-no-sampling/tests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { context } from '@opentelemetry/api'
2-
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
2+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
33
import { registerInstrumentations } from '@opentelemetry/instrumentation'
44
import { Resource } from '@opentelemetry/resources'
55
import {
@@ -22,7 +22,7 @@ declare let newPrismaClient: NewPrismaClient<typeof PrismaClient>
2222
let inMemorySpanExporter: InMemorySpanExporter
2323

2424
beforeAll(() => {
25-
const contextManager = new AsyncHooksContextManager().enable()
25+
const contextManager = new AsyncLocalStorageContextManager().enable()
2626
context.setGlobalContextManager(contextManager)
2727

2828
inMemorySpanExporter = new InMemorySpanExporter()

packages/client/tests/functional/tracing/tests.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { faker } from '@faker-js/faker'
22
import { Attributes, context, SpanKind, trace } from '@opentelemetry/api'
3-
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
3+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
44
import { registerInstrumentations } from '@opentelemetry/instrumentation'
55
import { Resource } from '@opentelemetry/resources'
66
import {
@@ -64,7 +64,7 @@ let inMemorySpanExporter: InMemorySpanExporter
6464
let processor: SpanProcessor
6565

6666
beforeAll(() => {
67-
const contextManager = new AsyncHooksContextManager().enable()
67+
const contextManager = new AsyncLocalStorageContextManager().enable()
6868
context.setGlobalContextManager(contextManager)
6969

7070
inMemorySpanExporter = new InMemorySpanExporter()
@@ -111,9 +111,9 @@ testMatrix.setupTestSuite(
111111
const trees = rootSpans.map((rootSpan) => buildTree(rootSpan, spans))
112112

113113
if (Array.isArray(expectedTree)) {
114-
expect(expectedTree).toEqual(trees)
114+
expect(trees).toEqual(expectedTree)
115115
} else {
116-
expect(expectedTree).toEqual(trees[0])
116+
expect(trees[0]).toEqual(expectedTree)
117117
}
118118
})
119119
}

packages/instrumentation/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Exporting traces to [Jaeger Tracing](https://jaegertracing.io).
3838

3939
```ts
4040
import { context } from '@opentelemetry/api'
41-
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
41+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
4242
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
4343
import { registerInstrumentations } from '@opentelemetry/instrumentation'
4444
import { Resource } from '@opentelemetry/resources'
@@ -48,7 +48,7 @@ import { PrismaInstrumentation } from '@prisma/instrumentation'
4848

4949
import { PrismaClient } from '.prisma/client'
5050

51-
const contextManager = new AsyncHooksContextManager().enable()
51+
const contextManager = new AsyncLocalStorageContextManager().enable()
5252

5353
context.setGlobalContextManager(contextManager)
5454

packages/instrumentation/src/ActiveTracingHelper.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const nonSampledTraceParent = `00-10-10-00`
2222
type Options = {
2323
traceMiddleware: boolean
2424
tracerProvider: TracerProvider
25+
ignoreSpanTypes: (string | RegExp)[]
2526
}
2627

2728
function engineSpanKindToOtelSpanKind(engineSpanKind: EngineSpanKind): SpanKind {
@@ -35,12 +36,14 @@ function engineSpanKindToOtelSpanKind(engineSpanKind: EngineSpanKind): SpanKind
3536
}
3637

3738
export class ActiveTracingHelper implements TracingHelper {
38-
traceMiddleware: boolean
39-
tracerProvider: TracerProvider
39+
private traceMiddleware: boolean
40+
private tracerProvider: TracerProvider
41+
private ignoreSpanTypes: (string | RegExp)[]
4042

41-
constructor({ traceMiddleware, tracerProvider }: Options) {
43+
constructor({ traceMiddleware, tracerProvider, ignoreSpanTypes }: Options) {
4244
this.traceMiddleware = traceMiddleware
4345
this.tracerProvider = tracerProvider
46+
this.ignoreSpanTypes = ignoreSpanTypes
4447
}
4548

4649
isEnabled(): boolean {
@@ -61,7 +64,7 @@ export class ActiveTracingHelper implements TracingHelper {
6164
const roots = spans.filter((span) => span.parentId === null)
6265

6366
for (const root of roots) {
64-
dispatchEngineSpan(tracer, root, spans, linkIds)
67+
dispatchEngineSpan(tracer, root, spans, linkIds, this.ignoreSpanTypes)
6568
}
6669
}
6770

@@ -85,6 +88,11 @@ export class ActiveTracingHelper implements TracingHelper {
8588
const tracer = this.tracerProvider.getTracer('prisma')
8689
const context = options.context ?? this.getActiveContext()
8790
const name = `prisma:client:${options.name}`
91+
92+
if (shouldIgnoreSpan(name, this.ignoreSpanTypes)) {
93+
return callback()
94+
}
95+
8896
// these spans will not be nested by default even in recursive calls
8997
// it's useful for showing middleware sequentially instead of nested
9098
if (options.active === false) {
@@ -103,7 +111,10 @@ function dispatchEngineSpan(
103111
engineSpan: EngineSpan,
104112
allSpans: EngineSpan[],
105113
linkIds: Map<string, string>,
114+
ignoreSpanTypes: (string | RegExp)[],
106115
) {
116+
if (shouldIgnoreSpan(engineSpan.name, ignoreSpanTypes)) return
117+
107118
const spanOptions = {
108119
attributes: engineSpan.attributes as Attributes,
109120
kind: engineSpanKindToOtelSpanKind(engineSpan.kind),
@@ -133,7 +144,7 @@ function dispatchEngineSpan(
133144

134145
const children = allSpans.filter((s) => s.parentId === engineSpan.id)
135146
for (const child of children) {
136-
dispatchEngineSpan(tracer, child, allSpans, linkIds)
147+
dispatchEngineSpan(tracer, child, allSpans, linkIds, ignoreSpanTypes)
137148
}
138149

139150
span.end(engineSpan.endTime)
@@ -160,3 +171,9 @@ function endSpan<T>(span: Span, result: T): T {
160171
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
161172
return value != null && typeof value['then'] === 'function'
162173
}
174+
175+
function shouldIgnoreSpan(spanName: string, ignoreSpanTypes: (string | RegExp)[]): boolean {
176+
return ignoreSpanTypes.some((pattern) =>
177+
typeof pattern === 'string' ? pattern === spanName : pattern.test(spanName),
178+
)
179+
}

packages/instrumentation/src/PrismaInstrumentation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717

1818
export interface PrismaInstrumentationConfig {
1919
middleware?: boolean
20+
ignoreSpanTypes?: (string | RegExp)[]
2021
}
2122

2223
type Config = PrismaInstrumentationConfig & InstrumentationConfig
@@ -45,6 +46,7 @@ export class PrismaInstrumentation extends InstrumentationBase {
4546
helper: new ActiveTracingHelper({
4647
traceMiddleware: config.middleware ?? false,
4748
tracerProvider: this.tracerProvider ?? trace.getTracerProvider(),
49+
ignoreSpanTypes: config.ignoreSpanTypes ?? [],
4850
}),
4951
}
5052

sandbox/tracing/otelSetup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as api from '@opentelemetry/api'
2-
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'
2+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
33
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
44
import { registerInstrumentations } from '@opentelemetry/instrumentation'
55
import { Resource } from '@opentelemetry/resources'
@@ -15,7 +15,7 @@ import { PrismaInstrumentation } from '@prisma/instrumentation'
1515

1616
export function otelSetup() {
1717
// a context manager is required to propagate the context
18-
const contextManager = new AsyncHooksContextManager().enable()
18+
const contextManager = new AsyncLocalStorageContextManager().enable()
1919

2020
// it's for node.js for span nesting and ctx propagation
2121
api.context.setGlobalContextManager(contextManager)

0 commit comments

Comments
 (0)