Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support instrumentation of modules via bundler plugins #4174

Open
drewcorlin1 opened this issue Oct 1, 2023 · 9 comments
Open

Support instrumentation of modules via bundler plugins #4174

drewcorlin1 opened this issue Oct 1, 2023 · 9 comments

Comments

@drewcorlin1
Copy link
Contributor

drewcorlin1 commented Oct 1, 2023

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 #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 #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. Allow module to be passed directly into plugins / bypass module hooks #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
Copy link
Contributor Author

drewcorlin1 commented Oct 22, 2023

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.

@mrjackdavis
Copy link

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

@RichiCoder1
Copy link

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

Copy link

github-actions bot commented Feb 5, 2024

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 github-actions bot added the stale label Feb 5, 2024
@drewcorlin1
Copy link
Contributor Author

Not stale

Copy link

github-actions bot commented Jul 1, 2024

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 github-actions bot added the stale label Jul 1, 2024
@drewcorlin1
Copy link
Contributor Author

being addressed here open-telemetry/opentelemetry-js-contrib#1856

@github-actions github-actions bot removed the stale label Jul 22, 2024
Copy link

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 github-actions bot added the stale label Sep 30, 2024
@drewcorlin1
Copy link
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants