n1ru4l / envelop

Envelop is a lightweight library allowing developers to easily develop, share, collaborate and extend their GraphQL execution layer. Envelop is the missing GraphQL plugin system.
https://envelop.dev
MIT License
785 stars 127 forks source link

OpenTelemetry plugin #2255

Open darren-west opened 3 months ago

darren-west commented 3 months ago

Summary

The current implementation of the OpenTelemetry plugin can lead to confusing spans because the execution span starts within the OpenTelemetry plugin but these do not wrap each other and instead run as part of an iteration over the plugins.

I think the correct way to handle OpenTelemetry traces within envelop is to wrap the GraphQL functions with spans, for example the executeFn and subscribeFn, this will effectively instrument the GraphQL phases.

A plugin that wants to add instrumentation can pick up the parent context and add a span which will appear alongside any other instrumented plugin in terms of the traces.

An example of wrapping the execute function would be like so:

      setExecuteFn(async () => {
        const span = tracer.startSpan(`${operationType}.${operationName}`, {}) // todo: fill in attrbutes.

        return await opentelemetry.context.with(api.context.active(), async () => {
          let result: ExecutionResult;
          try {
            result = await execute(args);
            if (isAsyncIterable(result)) {
              return mapAsyncIterator(result[Symbol.asyncIterator](), (next: ExecutionResult) => next, (error: GraphQLError) => markError(executionSpan, {errors: [error]}), () => span.end());
            }
          }finally {
            if (!isAsyncIterable(result)){
              span.end();
            }
          }
          return result;
        });
      });

This could be repeated for all phases of the GraphQL request and gives a more realistic view of how the process is executed.

Further to this we could create a Yoga Plugin that takes this further and wraps the request handler so that the parent context is set through all phases.

 onRequest: ({ request, setRequestHandler, requestHandler }) => {
    setRequestHandler((req, serverCtx) => {
      const span = tracer.startSpan("yoga.request");
      const result = api.context.with(api.trace.setSpan(api.context.active(), span), () => requestHandler(req, serverCtx));        
      span.end();
    })
  },

I think this would provide a much better instrumentation of GraphQL and how it is executed.