Open dickfickling opened 3 years ago
I agree, this would give us much more visibility inside our API endpoints.
Currently, Prisma simply doesn't show up (since it uses a separate client to do the queries).
This should be fairly simple to implement manually using https://www.prisma.io/docs/concepts/components/prisma-client/middleware/logging-middleware
When time permits, I will attempt this and report results/code here.
@Cretezy Thanks for looking into this! The main difficulty with using these kinds of APIs is that they don't usually support integrating with existing traces. For example, how would a span that is created during Prisma execution set any span from that middleware as a parent? We've had the same issue with Apollo Tracing amongst others. It's possible it doesn't apply here, but definitely something to keep in mind to avoid going down a path that might not work.
@Cretezy Thanks for looking into this! The main difficulty with using these kinds of APIs is that they don't usually support integrating with existing traces. For example, how would a span that is created during Prisma execution set any span from that middleware as a parent? We've had the same issue with Apollo Tracing amongst others. It's possible it doesn't apply here, but definitely something to keep in mind to avoid going down a path that might not work.
We are using the above logging middleware to report queries to datadog, but as you mentioned those are not coupled to the proper parent trace. I just recently found https://github.com/Jeff-Lewis/cls-hooked for another usecase we had (tagged logs with request uid) and also saw in #1268 that dd-trace-js leverages async_hooks.
Forgive me if I don't see the obvious reason here (I'm quite new to nodejs), but can't we leverage this continuation local storage to pass through the parent? Is it caused by prisma leveraging a separate process for its query engine, would the new experimental n-api bindings improve the situation here?
@tak1n The way the logging middleware works, it does look like it should work since it runs when the query happens. This is mostly problematic when the callback is run later, or when all data is accumulated and batched to a single handler, which doesn't seem to be the case here at a glance.
It shouldn't be needed to use cls-hooked
at all for tracing purpose because the tracer automatically propagates the context, assuming that Prisma doesn't to anything internally that would lose it, in which case cls-hooked
wouldn't work either anyway.
I haven't used Prisma so I'm not exactly sure how this middleware works (or Prisma for that matter), but at a glance I think it would look something like this:
prisma.$use((params, next) => {
// You can also add any tag relevant for you, and change the service name if
// needed with the `service.name` tag. If you have this information anywhere,
// make sure to add `out.host` and `out.port` tags as the UI will be able to
// group your instances together.
const tags = {
// The resource name is how things are grouped in the UI. This would usually
// include the query if you have it, although you should first obfuscate it
// since our agent doesn't know how to obfuscate Prisma.
'resource.name': `${params.model}.${params.action}`,
'span.kind': 'client',
'prisma.model': params.model,
'prisma.action': params.action
}
// Using tracer.trace() means that the span will automatically be a child of
// any current span, errors will be handled, and any span created in `next`
// will be a child of this span.
return tracer.trace('prisma.query', { tags }, () => next(params))
})
If it uses a different process for its query engine, I'm not sure how it would interact with the above, but if it doesn't work let me know and it should be possible to work around the issue.
@rochdev ah sry for not being clear, yes we were able to properly trace the client side:
But the SQL queries which are generated by the ORM are only accessible through https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#on
this.$on('query' as any, async (e: any) => {
const date = performance.now();
const span = tracer.startSpan('query', { startTime: date - e.duration });
span?.setTag('resource.name', e.query);
span?.finish(date);
});
Which gives us the executed queries as separate operation. In a perfect world we would see the queries in the same flamegraph.
@tak1n What is the value of tracer.scope().active()
in the query event handler? If it's the span from the middleware, then you could replace its resource name directly. If it's not then you would need some other means to stitch them together, but I don't know enough about Prisma to offer a workaround. However, from my understanding of how I think the middleware system works, I think it should work.
As a sidenote, make sure that the query is obfuscated when passed to span?.setTag('resource.name', e.query);
. If the query is SQL, you can instruct the agent to obfuscate it for you with the 'span.type': 'sql'
tag.
Ah thx for the hint of setting span type to sql, Prisma maintainers just confirmed this does not work yet - https://prisma.slack.com/archives/CA491RJH0/p1623391815452400?thread_ts=1623345269.439900&cid=CA491RJH0
@tak1n Did you check the value of tracer.scope().active()
just to be sure? The way the above question was asked I'm not sure it was understood that you meant the asynchronous context from async_hooks
and not explicitly passing the request context around, so I still think it could work.
Also, do you have any documentation about the separate query engine I could look into? Maybe if I understand this part better I could provide another workaround if needed.
@rochdev sry did take me somewhat longer
tracer.scope().active()
Uncaught ReferenceError: tracer is not defined
Yes I meant passing the context through async_hooks
. Will clarify this on prisma side if I was misunderstood (still a nodejs noob 😅 )
Docu can be found at https://www.prisma.io/docs/concepts/components/prisma-engines/query-engine. Here is the source code of the query engine https://github.com/prisma/prisma-engines/tree/master/query-engine
EDIT:
Above error seems incorrect, at least the tracer should be defined. Need to figure out whats going on here.
Debugger tells me tracer is not defined, console.log does state otherwise, but tracer.scope().active()
is null
@tak1n I just tried it and it works for me. This is what I tried using the quick start code:
// tracer.ts
import { tracer } from 'dd-trace'
tracer.init()
export { tracer }
// script.ts
import { tracer } from './tracer'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
}
]
})
prisma.$use(async (params, next) => {
const tags = {
'span.kind': 'client',
'span.type': 'sql',
'prisma.model': params.model,
'prisma.action': params.action
}
return tracer.trace('prisma.query', { tags }, () => next(params))
})
prisma.$on('query' as any, async (e: any) => {
const span = tracer.scope().active() // the span from above
span?.setTag('resource.name', e.query)
});
async function main() {
await prisma.user.findMany({
include: { posts: true },
})
}
main()
.catch(e => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
Not sure if there could be a significant enough difference between query engines to cause some of them to work and not others.
Also someone who is very interested in tracing prisma queries. @rochdev Thanks for that example, that exact setup does seem to work and captures the query.
However when I tried to translate that to my NestJS app it was no longer able to find the active span in the query event handler. I created a simple example reproduction here. TLDR: when the prisma client is created and exported as a regular JS object like above, seems to work fine. When the prisma client is created as a NestJS module as show in their docs, somehow that breaks the propagation of the context.
@zlangbert @rochdev can confirm this, the standalone snippet works. Combined with NestJS and like above mentioned setup breaks it.
Hi! The solutions from @zlangbert and @rochdev do not work for me.
While tracer.scope().active()
will return a span in non-nestjs environments, that span is always the same. That span will be the one created by fist execution of the prisma.$use callback.
This can be seen by either logging the span and looking at the _startTime, or just comparing the spans returned by .active().
E.g. (with the example from @rochdev)
// ...
const spans = [];
prisma.$on('query' as any, async (e: any) => {
const span = tracer.scope().active()
span?.setTag('resource.name', e.query)
spans.push(span);
const spansAreAllSame = spans.every(a => spans.every(b => a === b));
console.log(spansAreAllSame); // will always log true
});
async function main() {
await prisma.user.findMany();
await prisma.user.findMany();
await prisma.user.findMany();
}
This will result in only the first trace capturing a sql query, while all subsequent traces do not. (they try to overwrite the first trace - but from what I could tell do not succeed)
So while nestjs does seem to have an impact on this issue (the active span being null in the $on callback), the dd-trace - prisma integration with sql queries does not work either.
@ThibaultGerrier It's possible that there is some kind of internal queueing happening, or that the way the event emitter is used it runs the listeners in the "wrong" context (right and wrong for event emitters is complicated). In any case, it doesn't seem like it would be possible to add the query this way then. Is there any other hook into the lifecycle of a query that could be used instead? Or is there any object that is shared between the middleware and the event handler that could be used to store the query and then set it from the middleware? Otherwise it seems like a completely different approach will be needed, for example patching all the methods on the Prisma client, which is definitely not ideal, at least not for manual instrumentation.
Tried to get a closer look/understanding into how prisma works: (I am very new to prisma, so I might be wrong...)
The sql queries themselves are generated inside the rust prisma-enginge based on requests coming from the (nodejs) prisma client. Also to note, one "prisma request" (e.g. one prisma.user.findMany({args}) call) can generate multiple queries.
From what I could tell the raw sql queries are returned from the rust engine to nodejs in two different manners, depending on if you are using the experimental napi feature of prisma or not (https://github.com/prisma/prisma/releases/tag/2.20.0 - Node-API).
With the node engine: (the default)
It basically listens to stdout of the rust process. If it encounters any queries it just sends them back to the $on eventlistener. A sample message from the stdout stream containing the sql query that will be passed onto the $on event listener: E.g.
{
"timestamp": "Jun 14 16:36:49.173",
"level": "TRACE",
"fields": {
"query": "SELECT \"public\".\"Post\".\"id\", \"public\".\"Post\".\"name\" FROM \"public\".\"Post\" WHERE 1=1 OFFSET $1",
"item_type": "query",
"is_query": true,
"params": "[0]",
"duration_ms": 1,
"result": "success"
},
"target": "quaint::connector::metrics",
"span": {
"sql": "\"SELECT \\\"public\\\".\\\"Post\\\".\\\"id\\\", \\\"public\\\".\\\"Post\\\".\\\"name\\\" FROM \\\"public\\\".\\\"Post\\\" WHERE 1=1 OFFSET $1\"",
"name": "query_raw"
},
"spans": [
{
"name": "execute_single_operation"
},
{
"aggr_selections": "[]",
"name": "get_many_records"
},
{
"name": "filter"
},
{
"name": "Filter read query"
},
{
"sql": "\"SELECT \\\"public\\\".\\\"Post\\\".\\\"id\\\", \\\"public\\\".\\\"Post\\\".\\\"name\\\" FROM \\\"public\\\".\\\"Post\\\" WHERE 1=1 OFFSET $1\"",
"name": "query_raw"
}
]
}
Nothing points back to the original "prisma request" from this message. To me it seems like it would be impossible to say "who" was responsible for this query.
With napi:
When initializing the engine, nodejs passes a callback to the engine that will be executed whenever a new sql query is executed (or for other "info" messages). E.g. (arguments passed to callback)
{
"level": "TRACE",
"module_path": "quaint::connector::metrics",
"query": "SELECT \"public\".\"Post\".\"id\", \"public\".\"Post\".\"name\" FROM \"public\".\"Post\" WHERE 1=1 OFFSET $1",
"item_type": "query",
"is_query": "true",
"params": "[0]",
"duration_ms": "1",
"result": "success"
}
Again, nothing points back to the original "prisma request".
I have very little understanding of how dd-trace and prisma internally work, but I don't see any way to solve this issue without interfering in the rust prisma engine. (E.g. by having the messages contain ids to reference the original request)
same issue + 1.
I would recommend opening an issue in Prisma. Unfortunately the library is written in a way that is not patchable externally, so we can't monkey-patch it as we do for most libraries. There is also no functionality provided so that APM vendors can hook into the lifecycle of a query to instrument it.
I would recommend opening an issue in Prisma
There are already multiple issues open on prisma side in this regard:
It is also already on the roadmap of prisma - https://www.notion.so/Support-Tracing-in-Prisma-Client-c0afd7599022435da2b8fc05c30cd1c0
Primsa v4.2.0 has implemented opentelemetry tracing https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing
@xzyfer Does this mean that they have fixed context propagation internally as well? Otherwise there would be no way to support any kind of tracing, so I assume this is the case.
Can't comment authoritatively but when we implemented it on our codebase it worked just fine with Datadog APM, so I'd presume so?
We got context propagation working on top of Prisma's tracing feature by writing a custom ContextManager
to expose the active Datadog span context to OpenTelemetry. You still need to initialize both tracing SDKs, but with this adapter the spans will be correctly nested in APM.
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import tracer from 'dd-trace';
class DatadogContextManager extends AsyncLocalStorageContextManager {
active(): api.Context {
const context = super.active();
const datadogActiveSpan = tracer.scope().active();
// Only use the Datadog context if an OpenTelemetry span is not active and there is an active Datadog span.
const shouldUseDatadogContext =
!api.trace.getSpanContext(context) && datadogActiveSpan;
if (!shouldUseDatadogContext) {
return context;
}
// Extract and convert Datadog trace/span IDs to OpenTelemetry format.
// See: https://docs.datadoghq.com/tracing/other_telemetry/connect_logs_and_traces/opentelemetry/
const datadogSpanContext = datadogActiveSpan.context();
const traceId = BigInt(datadogSpanContext.toTraceId())
.toString(16)
.padStart(32, '0');
const spanId = BigInt(datadogSpanContext.toSpanId())
.toString(16)
.padStart(16, '0');
return api.trace.setSpanContext(context, {
traceId,
spanId,
// Datadog APM uses tail sampling, so we set this flag to always record data.
traceFlags: api.TraceFlags.SAMPLED,
});
}
}
The context manager can then be passed to provider.register()
when initializing the OpenTelemetry SDK. You can use the OTLPTraceExporter
class to export traces to the Datadog Agent's OTLP endpoint after enabling it in the agent config.
const provider = new BasicTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter()));
provider.register({
contextManager: new DatadogContextManager(),
});
new PrismaInstrumentation().enable();
This approach only properly handles nesting of OpenTelemetry spans within Datadog spans and not the other way around, but this is enough for the Prisma tracing use case.
Can't comment authoritatively but when we implemented it on our codebase it worked just fine with Datadog APM, so I'd presume so?
What articles do you recommend to implement it? I am checking Datadog's article OTLP Trace Ingestion by the Datadog Agent and can't think of how to accomplish that based on the Prisma OpenTelemetry tracing docs.
Thank you : )
What articles do you recommend to implement it? I am checking Datadog's article OTLP Trace Ingestion by the Datadog Agent and can't think of how to accomplish that based on the Prisma OpenTelemetry tracing docs.
Thank you : )
See @zacharyliu'a comment above, he implemented it for us :)
This integration would definitely be very appreciated! Now that Prisma has opened up the possibility for tracing, is there a chance that someone at Datadog could take another stab at this?
Link to prisma announcement on supporting opentelemetry tracing https://www.prisma.io/blog/tracing-launch-announcement-pmk4rlpc0ll
Anyone here using @zacharyliu's beautiful implementation above and trying to get sampling to work consistently across both the Datadog parent span and the OTLP child span? I want to make sure that if the Datadog Tracer samples out a trace, then any nested OTLP spans will also be sampled out.
+1 from us on this one as an official feature - would be great to have better tracing from Next.JS using prisma.
+1
+1
New Relic have this feature now, maybe that'll get this bumped up the priority list.
@tak1n I just tried it and it works for me. This is what I tried using the quick start code:
// tracer.ts import { tracer } from 'dd-trace' tracer.init() export { tracer }
// script.ts import { tracer } from './tracer' import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient({ log: [ { emit: 'event', level: 'query', } ] }) prisma.$use(async (params, next) => { const tags = { 'span.kind': 'client', 'span.type': 'sql', 'prisma.model': params.model, 'prisma.action': params.action } return tracer.trace('prisma.query', { tags }, () => next(params)) }) prisma.$on('query' as any, async (e: any) => { const span = tracer.scope().active() // the span from above span?.setTag('resource.name', e.query) }); async function main() { await prisma.user.findMany({ include: { posts: true }, }) } main() .catch(e => { throw e }) .finally(async () => { await prisma.$disconnect() })
Not sure if there could be a significant enough difference between query engines to cause some of them to work and not others.
I attempted to implement this, but the resource is only showing up as prisma.query
and not including the actual query. I'm wondering if anyone else has run into this and how they resolved it?
Glad that there are some workarounds. What would be clutch here is an api such as described for the pg
library (https://docs.datadoghq.com/database_monitoring/guide/connect_dbm_and_apm/?tab=nodejs).
tracer.use('prisma', { dbmPropagationMode: 'full', service: 'my-db-service' });
Has anyone using @zacharyliu 's implementation above found that a subset of prisma spans are sometimes missing from traces? For example, I'll see several prisma operations in one "trace" within datadog, but some of those operations are missing the prisma:engine:db_query
span?
Has anyone using @zacharyliu 's implementation above found that a subset of prisma spans are sometimes missing from traces? For example, I'll see several prisma operations in one "trace" within datadog, but some of those operations are missing the
prisma:engine:db_query
span?
Yes, I’ve noticed this issue
With dd-trace-js
now supporting TracerProvider
is this as simple as?
import tracer from 'dd-trace'
import { registerInstrumentations } from '@opentelemetry/instrumentation'
import { PrismaInstrumentation } from '@prisma/instrumentation'
const { TracerProvider } = tracer.init()
const provider = new TracerProvider()
// Register your auto-instrumentors
registerInstrumentations({
tracerProvider: provider,
instrumentations: [new PrismaInstrumentation()],
})
// Register the provider globally
provider.register()
@kdawgwilk solution worked for me, I implemented a similar solution .
@tak1n I wondered same question to add the SQL query to the Trace's Spans. I know it's not perfect, but this worked for me:
Building upon @cdm6a solution, in the prisma.$on('query', (e) => { ...
function (at the bottom)
if (this.options.includeSqlInTraces) {
if (this.tracer) {
let span = this.tracer.scope().active()
let ourSpan = false
if (!span) {
span = this.tracer.startSpan('custom:prisma:db:query')
ourSpan = true
}
span.setTag('prisma.sqlQuery', e.query)
if (ourSpan) {
span.finish()
}
}
}
The snippet was controlled by an optional flag so I can enable/disable this feature. YMMV, so you can omit the IF if you like.
Hope it helps. 🙂
@kdawgwilk solution worked for me, I implemented a similar solution .
only by adding @kdawgwilk's implementation at tracer setup ? or you added something else?
@kdawgwilk solution worked for me, I implemented a similar solution .
only by adding @kdawgwilk's implementation at tracer setup ? or you added something else?
Same, nothing else @cegonya-prima .
@kdawgwilk's solution mostly worked for me but I'm missing the prisma:engine:*
spans. Has anyone else been able to get those working?
We've been successfully using @zacharyliu's implementation in https://github.com/DataDog/dd-trace-js/issues/1244#issuecomment-1212462924
Only issue I've been getting now is that I have a unique db.statement tag for every query span, because every db statement includes a comment at the end
I tried the TracerProvider solution but immediately had an issue:
"TypeError: Xqs is not a constructor",
" at Object.<anonymous> (/var/src/prismaTracing.ts:7:18)",
" at Module._compile (node:internal/modules/cjs/loader:1256:14)",
" at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)",
" at Module.load (node:internal/modules/cjs/loader:1119:32)",
" at Module._load (node:internal/modules/cjs/loader:960:12)",
" at Module.require (node:internal/modules/cjs/loader:1143:19)",
" at Hook.Module.require (/opt/nodejs/node_modules/dd-trace/packages/dd-trace/src/ritm.js:85:33)",
" at require (node:internal/modules/cjs/helpers:121:18)",
" at _tryRequireFile (/opt/nodejs/node_modules/datadog-lambda-js/runtime/user-function.js:107:44)",
" at /opt/nodejs/node_modules/datadog-lambda-js/runtime/user-function.js:180:36",
" at step (/opt/nodejs/node_modules/datadog-lambda-js/runtime/user-function.js:43:23)",
" at Object.next (/opt/nodejs/node_modules/datadog-lambda-js/runtime/user-function.js:24:53)",
" at /opt/nodejs/node_modules/datadog-lambda-js/runtime/user-function.js:18:71",
" at new Promise (<anonymous>)",
" at __awaiter (/opt/nodejs/node_modules/datadog-lambda-js/runtime/user-function.js:14:12)",
" at _tryRequire (/opt/nodejs/node_modules/datadog-lambda-js/runtime/user-function.js:168:12)"
Xqs is the TracerProvider which has been renamed by our bundler (esbuild). Could be an issue with the bundling or with updated dependencies but not sure.
UPDATE
Yep, seems like it was the bundler. We're running Lambdas with the datadog lambda layers, so when I configured dd-trace as an external dependency for bundling this error went away.
Unfortunately this didn't surface Prisma's traces in Datadog. After setting previewFeatures = ["tracing"]
on the client generator I then got another error which is identical the one being discussed in the related issue (https://github.com/prisma/prisma/pull/21076)
Since tried @zacharyliu's solution which also didn't work for me. No errors, just no traces so not sure if there's any additional configuration needed.
We really rely on Datadog's expertise in this area, so would be great to get a clear official solution/guide although do appreciate they cannot get involved in bespoke requests from every user! It is, however, the most commented GitHub issue for dd-trace!
I tried the solution provided by @kdawgwilk, and it was working for a while, but now I am encountering an error. It seems that updating @prisma/instrumentation and its dependencies is causing the issue, and it probably occurs in version 5.3.0 and above.
The error message is as follows:
TypeError: parentTracer.getSpanLimits is not a function
at new Span (/usr/src/app/node_modules/@opentelemetry/sdk-trace-base/src/Span.ts:121:37)
It appears that there is no corresponding method in @opentelemetry/sdk-trace-base for this: https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/Tracer.ts#L247-L250
I tried adding a getSpanLimits method arbitrarily to the Tracer class in dd-trace, and the error disappeared: https://github.com/DataDog/dd-trace-js/blob/master/packages/dd-trace/src/opentelemetry/tracer.js
However, I am unsure about what should be returned by this method for dd-trace, based on the expected return values here: https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/types.ts#L75-L88
So, I haven’t been able to submit a Pull Request. Does anyone know what should be returned?
I'm encountering the issue descrived by @maruware above (parentTracer.getSpanLimits is not a function) with prisma, @prisma/client, and @prisma/instrumentation at version 5.6.0 and all the OpenTelemetry libraries at the latest versions. Everything worked on version 5.3.1 of the prisma libraries.
I am getting the same getSpanLimits
exception as others in the thread while using @kdawgwilk 's solution. After an investigation, I submitted a bug to Prisma: https://github.com/prisma/prisma/issues/22450
If I understand everything correctly, the issue stems from PrismaInstrumentation assuming that the Tracer is a Tracer derived from @opentelemetry/sdk-trace-base
, but in case of Datadog, the tracer is built from scratch and only implements the Tracer interface from @opentelemetry/api
(which doesn't have getSpanLimits
).
We've been successfully using @zacharyliu's implementation in #1244 (comment)
Only issue I've been getting now is that I have a unique db.statement tag for every query span, because every db statement includes a comment at the end
@alandotcom What versions of prisma and dd-trace were you using? Can you share the schema.prisma
generator section and any setup code? You seem be the only one with proof that prisma:engine:*
spans appear.
@alandotcom What versions of prisma and dd-trace were you using? Can you share the
schema.prisma
generator section and any setup code? You seem be the only one with proof thatprisma:engine:*
spans appear.
@eddowh package versions we're using:
"dd-trace": "^5.2.0",
"prisma": "^5.9.1",
"@prisma/client": "^5.9.1",
"@prisma/instrumentation": "^5.9.1",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/context-async-hooks": "^1.21.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
"@opentelemetry/instrumentation": "^0.48.0",
"@opentelemetry/resources": "^1.21.0",
"@opentelemetry/sdk-trace-base": "^1.21.0",
"@opentelemetry/sdk-trace-node": "^1.21.0",
"@opentelemetry/semantic-conventions": "^1.21.0",
Generator section looks like,
generator client {
provider = "prisma-client-js"
previewFeatures = ["tracing", "metrics"]
binaryTargets = ["native", "linux-musl"]
}
I'll just share the exact code we're using:
import tracer, { TracerOptions } from 'dd-trace'
import * as api from '@opentelemetry/api'
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { registerInstrumentations } from '@opentelemetry/instrumentation'
import {
BasicTracerProvider,
BatchSpanProcessor
} from '@opentelemetry/sdk-trace-base'
import { PrismaInstrumentation } from '@prisma/instrumentation'
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import config from './config'
export default tracer
class DatadogContextManager extends AsyncLocalStorageContextManager {
active(): api.Context {
const context = super.active()
const datadogActiveSpan = tracer.scope().active()
// Only use the Datadog context if an OpenTelemetry span is not active and there is an active Datadog span.
const shouldUseDatadogContext =
api.trace.getSpanContext(context) != null && datadogActiveSpan != null
if (!shouldUseDatadogContext) {
return context
}
// Extract and convert Datadog trace/span IDs to OpenTelemetry format.
// See: https://docs.datadoghq.com/tracing/other_telemetry/connect_logs_and_traces/opentelemetry/
const datadogSpanContext = datadogActiveSpan.context()
const traceId = BigInt(datadogSpanContext.toTraceId())
.toString(16)
.padStart(32, '0')
const spanId = BigInt(datadogSpanContext.toSpanId())
.toString(16)
.padStart(16, '0')
return api.trace.setSpanContext(context, {
traceId,
spanId,
// Datadog APM uses tail sampling, so we set this flag to always record data.
traceFlags: api.TraceFlags.SAMPLED
})
}
}
const options: TracerOptions = {
dogstatsd: {
hostname: process.env.DD_STATSD_HOST
}
}
const provider = new BasicTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'db',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.DD_ENV,
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.DD_VERSION
})
})
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter()))
provider.register({
contextManager: new DatadogContextManager()
})
const prismaInstrumentation = new PrismaInstrumentation()
registerInstrumentations({
tracerProvider: provider,
instrumentations: [prismaInstrumentation]
})
prismaInstrumentation.enable()
tracer.init(options)
Also, in my original comment I mentioned we were getting unique db.statement tags for every query. We were able to remove that with an env configuration on the datadog agent
DD_APM_REPLACE_TAGS=[{"name": "db.statement", "pattern": " ?[/][*] traceparent.*$", "repl": ""}]
I'm using Prisma to manage database connections / queries. Really missing being able to track down slow MySQL queries in DD. Would be great to see support for prisma query tracing added