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

feat: Esbuild plugin diag channel #5334

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ All notable changes to experimental packages in this project will be documented

### :rocket: (Enhancement)

* feat(instrumentation): Add support for patching modules via diagnostic channels, to support auto instrumentation with bundlers [#5334](https://github.com/open-telemetry/opentelemetry-js/pull/5334)

### :bug: (Bug Fix)

### :books: (Refine Doc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import * as types from '../../types';
import * as path from 'path';
import * as diagch from 'diagnostics_channel';
import { types as utilTypes } from 'util';
import { satisfies } from 'semver';
import { wrap, unwrap, massWrap, massUnwrap } from 'shimmer';
Expand All @@ -30,6 +31,7 @@ import {
InstrumentationConfig,
InstrumentationModuleDefinition,
} from '../../types';
import { DiagChSubscribe, OTelBundleLoadMessage } from '../../types_internal';
import { diag } from '@opentelemetry/api';
import type { OnRequireFn } from 'require-in-the-middle';
import { Hook as HookRequire } from 'require-in-the-middle';
Expand Down Expand Up @@ -178,9 +180,10 @@ export abstract class InstrumentationBase<
module: InstrumentationModuleDefinition,
exports: T,
name: string,
baseDir?: string | void
baseDir?: string | void,
version?: string
): T {
if (!baseDir) {
if (!version && !baseDir) {
if (typeof module.patch === 'function') {
module.moduleExports = exports;
if (this._enabled) {
Expand All @@ -196,7 +199,9 @@ export abstract class InstrumentationBase<
return exports;
}

const version = this._extractPackageVersion(baseDir);
if (!version && baseDir) {
version = this._extractPackageVersion(baseDir);
}
module.moduleVersion = version;
if (module.name === name) {
// main module
Expand Down Expand Up @@ -285,6 +290,11 @@ export abstract class InstrumentationBase<
}

this._warnOnPreloadedModules();

const imdsFromHookPath = new Map<
string,
InstrumentationModuleDefinition[]
>();
for (const module of this._modules) {
const hookFn: HookFn = (exports, name, baseDir) => {
if (!baseDir && path.isAbsolute(name)) {
Expand Down Expand Up @@ -312,6 +322,56 @@ export abstract class InstrumentationBase<
<HookFn>hookFn
);
this._hooks.push(esmHook);

const imdsByModuleName = imdsFromHookPath.get(module.name) ?? [];
imdsFromHookPath.set(module.name, imdsByModuleName);
imdsByModuleName.push(module);
for (const file of module.files) {
const imdsByFileName = imdsFromHookPath.get(file.name) ?? [];
imdsFromHookPath.set(file.name, imdsByFileName);
imdsByFileName.push(module);
}
}

// `diagch.subscribe` was added in Node.js v18.7.0, v16.17.0.
const subscribe: DiagChSubscribe = (diagch as any).subscribe;
if (typeof subscribe === 'function') {
// A bundler plugin, e.g. `@opentelemetry/esbuild-plugin`, can pass
// a loaded module to this instrumentation via the well-known
// `otel:bundle:load` diagnostics channel message. The message includes
// the module exports, that can be patched in-place.
subscribe('otel:bundle:load', rawMessage => {
const message = rawMessage as OTelBundleLoadMessage;
if (
(typeof message.name !== 'string' &&
typeof message.file !== 'string') ||
typeof message.version !== 'string'
) {
this._diag.debug(
'skipping invalid "otel:bundle:load" diagch message',
rawMessage
);
return;
}
const names = [message.name, message.file].filter(Boolean) as string[];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trentm this differed from your original implementation. Idea here is to handle instrumentations that patch just the top level module (eg fastify) and instrumentations that patch specific files (eg graphQL)

for (const name of names) {
const imds = imdsFromHookPath.get(name);
if (!imds) {
// This loaded module is not relevant for this instrumentation.
return;
}
for (const imd of imds) {
const patchedExports = this._onRequire<typeof exports>(
imd,
message.exports,
name,
undefined,
message.version // Package version was determined at bundle-time.
);
message.exports = patchedExports;
}
}
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,25 @@ export interface AutoLoaderOptions {
meterProvider?: MeterProvider;
loggerProvider?: LoggerProvider;
}

/**
* A subset of types for Node.js `diagnostics_channel`.
* `diagnostics_channel.subscribe` was added in Node.js v18.7.0, v16.17.0.
* The current `@types/node` dependency is for an earlier version (v14) of
* Node.js
*/
type DiagChChannelListener = (message: unknown, name: string | symbol) => void;
export type DiagChSubscribe = (
name: string | symbol,
onMessage: DiagChChannelListener
) => void;

/**
* The shape of a `otel:bundle:load` diagnostics_channel message.
*/
export type OTelBundleLoadMessage = {
name?: string;
file?: string;
version: string;
exports: any;
};
Loading