DataDog / dd-trace-js

JavaScript APM Tracer
https://docs.datadoghq.com/tracing/
Other
639 stars 302 forks source link

Request: Add support for Prisma query tracing #1244

Open dickfickling opened 3 years ago

dickfickling commented 3 years ago

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

Cretezy commented 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).

Cretezy commented 3 years ago

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.

rochdev commented 3 years ago

@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.

tak1n commented 3 years ago

@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?

rochdev commented 3 years ago

@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.

tak1n commented 3 years ago

@rochdev ah sry for not being clear, yes we were able to properly trace the client side:

tracing

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.

rochdev commented 3 years ago

@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.

tak1n commented 3 years ago

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

slack

rochdev commented 3 years ago

@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.

tak1n commented 3 years ago

@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.

tak1n commented 3 years ago

Debugger tells me tracer is not defined, console.log does state otherwise, but tracer.scope().active() is null

tracer

rochdev commented 3 years ago

@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.

zlangbert commented 3 years ago

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.

tak1n commented 3 years ago

@zlangbert @rochdev can confirm this, the standalone snippet works. Combined with NestJS and like above mentioned setup breaks it.

ThibaultGerrier commented 3 years ago

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.

rochdev commented 3 years ago

@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.

ThibaultGerrier commented 3 years ago

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)

https://github.com/prisma/prisma/blob/36dbb4a171c5c5ef5e1dff94687c0ad3727a3028/src/packages/engine-core/src/NodeEngine.ts#L629

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:

https://github.com/prisma/prisma/blob/24b835bfaf4ca48c74685829cde91bc68071eca1/src/packages/engine-core/src/NAPIEngine.ts#L200

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)

lake2 commented 2 years ago

same issue + 1.

rochdev commented 2 years ago

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.

tak1n commented 2 years ago

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

xzyfer commented 2 years ago

Primsa v4.2.0 has implemented opentelemetry tracing https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing

rochdev commented 2 years ago

@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.

casey-chow commented 2 years ago

Can't comment authoritatively but when we implemented it on our codebase it worked just fine with Datadog APM, so I'd presume so?

zacharyliu commented 2 years ago

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.

supalarry commented 2 years ago

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 : )

casey-chow commented 2 years ago

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 :)

ludwigbacklund commented 1 year ago

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?

kdawgwilk commented 1 year ago

Link to prisma announcement on supporting opentelemetry tracing https://www.prisma.io/blog/tracing-launch-announcement-pmk4rlpc0ll

akirillo commented 1 year ago

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.

mrmason commented 1 year ago

+1 from us on this one as an official feature - would be great to have better tracing from Next.JS using prisma.

fruwe commented 1 year ago

+1

BernalCarlos commented 1 year ago

+1

public commented 1 year ago

New Relic have this feature now, maybe that'll get this bumped up the priority list.

cdm6a commented 1 year ago

@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?

crice88 commented 1 year ago

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' });
shawnjones253 commented 1 year ago

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?

alandotcom commented 1 year ago

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

kdawgwilk commented 1 year ago

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()
davidcam-nwse commented 1 year ago

@kdawgwilk solution worked for me, I implemented a similar solution .

davidcam-nwse commented 1 year ago

@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. 🙂

cegonya-prima commented 1 year ago

@kdawgwilk solution worked for me, I implemented a similar solution .

only by adding @kdawgwilk's implementation at tracer setup ? or you added something else?

davidcam-nwse commented 1 year ago

@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 .

georgewitteman commented 1 year ago

@kdawgwilk's solution mostly worked for me but I'm missing the prisma:engine:* spans. Has anyone else been able to get those working?

alandotcom commented 1 year ago

We've been successfully using @zacharyliu's implementation in https://github.com/DataDog/dd-trace-js/issues/1244#issuecomment-1212462924

image

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

Screenshot 2023-08-24 at 8 00 33 AM
brainsiq commented 11 months ago

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!

maruware commented 10 months ago

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?

dchenk commented 9 months ago

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.

sergeyevstifeev commented 9 months ago

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).

eddowh commented 6 months ago

We've been successfully using @zacharyliu's implementation in #1244 (comment)

image

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

Screenshot 2023-08-24 at 8 00 33 AM

@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 commented 6 months ago

@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.

@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": ""}]
image