opentelemetry-js icon indicating copy to clipboard operation
opentelemetry-js copied to clipboard

Support instrumentation of modules via bundler plugins

Open drewcorlin1 opened this issue 2 years ago • 9 comments
trafficstars

Is your feature request related to a problem? Please describe.

I am attempting to use the OTel auto instrumentation for Javascript, but non-built-in modules are not patched because I am bundling my code with esbuild before deploying it into a docker container (same scenario as https://github.com/open-telemetry/opentelemetry-js/issues/2708).

Describe the solution you'd like

Provide bundler (esbuild, webpack, rollup, etc.) plugins as a means of instrumenting third party modules, since the existing require()-based patching does not work if you are bundling your code. This is somewhat of an alternative to https://github.com/open-telemetry/opentelemetry-js/issues/2708, with a big difference I see being that the bundler plugin could more easily apply to modules only when they're present. The proposed solution in that link seems much more manual to maintain in a codebase where we have many microservices with varying dependencies. I don't want to force developers to know they need to pass 20 aws modules to the OTel instrumentation just because they added an aws-s3 dependency.

Describe alternatives you've considered

  1. https://github.com/open-telemetry/opentelemetry-js/issues/2708
  2. Writing this esbuild plugin myself (code below).

Additional context

An extremely hacky proof of concept with esbuild's onLoad hook to patch pino and fastify

// esbuild.ts
import { build, PluginBuild } from 'esbuild';
import { readFile } from 'fs/promises';

function wrapModule({
  originalSource,
  instrumentationPackage,
  instrumentationName,
  instrumentationConstructorArgs,
}: {
  originalSource: string;
  instrumentationPackage: string;
  instrumentationName: string;
  instrumentationConstructorArgs?: string;
}) {
  return `
(function() {
${originalSource}
})(...arguments);
{
let mod = module.exports;

const { ${instrumentationName} } = require('${instrumentationPackage}');
const instrumentations = new ${instrumentationName}(${instrumentationConstructorArgs ?? ''}).init();
// TODO: Get rid of this check, but also ensure it does what we want when there are multiple instrumentations
if (instrumentations.length > 1) throw new Error('Cannot handle multiple instrumentations');

for (const instrumentation of instrumentations) {
        mod = instrumentation.patch(mod);
}
module.exports = mod;
}
`;
}

function loadFastify(build: PluginBuild) {
  build.onLoad({ filter: /fastify\/fastify.js$/ }, async () => {
    const resolved = await build.resolve('./fastify', {
      kind: 'require-call',
      resolveDir: './node_modules',
    });

    const contents = await readFile(resolved.path);
    return {
      contents: wrapModule({
        originalSource: contents.toString(),
        instrumentationPackage: '@opentelemetry/instrumentation-fastify',
        instrumentationName: 'FastifyInstrumentation',
      }),
      resolveDir: './node_modules/fastify',
    };
  });
}

interface PinoConfig {
  // TODO: Improve types
  logHook: (span: any, record: any) => void;
}

function loadPino(build: PluginBuild, config?: PinoConfig) {
  build.onLoad({ filter: /pino\/pino.js$/ }, async () => {
    const resolved = await build.resolve('./pino', {
      kind: 'require-call',
      resolveDir: './node_modules',
    });

    const contents = await readFile(resolved.path);
    return {
      contents: wrapModule({
        originalSource: contents.toString(),
        instrumentationPackage: '@opentelemetry/instrumentation-pino',
        instrumentationName: 'PinoInstrumentation',
        instrumentationConstructorArgs: `{
          logHook: ${config?.logHook.toString() ?? undefined},
        }`,
      }),
      resolveDir: './node_modules/pino',
    };
  });
}

build({
  entryPoints: ['src/server.ts'],
  bundle: true,
  outfile: 'dist/server.js',
  target: 'node18',
  platform: 'node',
  sourcemap: true,
  plugins: [
    {
      name: 'open-telemetry',
      setup(build) {
        loadFastify(build);
        loadPino(build, {
          logHook: (span, record) => {
            // Reformat the injected log fields to use camelCase, eg. trace_id -> traceId
            const context = span.spanContext();
            record.traceId = context.traceId;
            record.spanId = context.spanId;
            record.strTraceFlags = context.traceFlags;

            if (record.trace_id === context.traceId) delete record.trace_id;
            if (record.span_id === context.spanId) delete record.span_id;
            if (Number(record.trace_flags) === context.traceFlags) delete record.trace_flags;
          },
        });
      },
    },
  ],
}).catch(err => {
  throw err;
});

drewcorlin1 avatar Oct 01 '23 14:10 drewcorlin1

DataDog has an experimental esbuild plugin and I think a similar approach could be used for this. Is there any interest in this solution? I would be happy to begin contributing for a few packages, but I don't think I could commit to getting the plugin to work for every supported package.

drewcorlin1 avatar Oct 22 '23 23:10 drewcorlin1

Hey! I just stumped upon this and it'd be extremely helpful if there was a community solution for this @drewcorlin1

You have my +1

mrjackdavis avatar Nov 01 '23 03:11 mrjackdavis

This would be an awesome way to approach this. It looks like DD's work is open source too, might be able to reference their work or collab: https://github.com/DataDog/dd-trace-js/blob/master/packages/datadog-esbuild/index.js

RichiCoder1 avatar Nov 03 '23 16:11 RichiCoder1

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] avatar Feb 05 '24 06:02 github-actions[bot]

Not stale

drewcorlin1 avatar Feb 05 '24 12:02 drewcorlin1

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] avatar Jul 01 '24 06:07 github-actions[bot]

being addressed here https://github.com/open-telemetry/opentelemetry-js-contrib/pull/1856

drewcorlin1 avatar Jul 01 '24 10:07 drewcorlin1