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: nodejs opamp instrumentation libraries enabled remote config #1350

Merged
merged 23 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
358a9d2
feat: configure enabled inst lib from opamp
blumamir Jul 5, 2024
41cbfef
feat: instrumentations configuration from opamp server
blumamir Jul 6, 2024
3c892d7
feat: read inst enabled from CRD
blumamir Jul 6, 2024
0fe7819
fix: use disabled instead of enabled
blumamir Jul 7, 2024
7a61a2b
fix: remove opamp client resource detector
blumamir Jul 7, 2024
80b15b3
feat: reconcile instrumentation configs
blumamir Jul 11, 2024
e29b7d6
Merge remote-tracking branch 'upstream/main' into opamp-inst-libraries
blumamir Jul 11, 2024
967e8b0
refactor: remote config handling
blumamir Jul 13, 2024
1042452
fix: handling remote config in nodejs agent
blumamir Jul 13, 2024
a444517
feat: send enabled signals on omapm new connection
blumamir Jul 13, 2024
6bb00db
feat: destinations reconciler to update enabled signals in realtime
blumamir Jul 13, 2024
22981b3
refactor: use a function to generate opamp inst-lib configs
blumamir Jul 13, 2024
84f0d52
feat: set global tracer provider
blumamir Jul 13, 2024
f2d03f3
chore: disable diag logger
blumamir Jul 13, 2024
935467a
fix: don't error on old agents
blumamir Jul 14, 2024
e3e14a7
fix: handle exceptions
blumamir Jul 14, 2024
8c80263
fix: go mod tidy
blumamir Jul 14, 2024
a9a14cd
fix: disable diag logger
blumamir Jul 14, 2024
dff2405
chore: remove unrelevant comment
blumamir Jul 14, 2024
cd4e88a
chore: remove redundant if
blumamir Jul 14, 2024
28d88fb
ci: tag opampserver on release
blumamir Jul 14, 2024
eb72d91
chore: improvments from code review
blumamir Jul 15, 2024
ca6d104
Merge branch 'main' into opamp-inst-libraries
blumamir Jul 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agents/nodejs/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules # don't include node_modules in the context, as it takes care of this
5 changes: 4 additions & 1 deletion agents/nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
"@bufbuild/protobuf": "^1.10.0",
"@opentelemetry/api": "1.8.0",
"@opentelemetry/auto-instrumentations-node": "0.46.1",
"@opentelemetry/context-async-hooks": "1.25.0",
"@opentelemetry/core": "1.25.0",
"@opentelemetry/exporter-trace-otlp-grpc": "0.51.1",
"@opentelemetry/resources": "1.25.0",
"@opentelemetry/sdk-node": "0.51.1",
"@opentelemetry/sdk-trace-node": "1.25.0",
"@opentelemetry/semantic-conventions": "1.25.0",
"axios": "^1.7.2",
"semver": "^7.6.2",
"uuidv7": "^1.0.1"
},
"devDependencies": {
"@types/semver": "^7.5.8",
"@types/uuid": "^10.0.0",
"typescript": "^5.5.2"
}
Expand Down
111 changes: 81 additions & 30 deletions agents/nodejs/src/autoinstrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
// For development, uncomment the following line to see debug logs
// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);

import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OpAMPClientHttp } from "./opamp";
import {
CompositePropagator,
W3CBaggagePropagator,
W3CTraceContextPropagator,
} from "@opentelemetry/core";
import { OpAMPClientHttp, RemoteConfig } from "./opamp";
import {
SEMRESATTRS_TELEMETRY_SDK_LANGUAGE,
TELEMETRYSDKLANGUAGEVALUES_NODEJS,
SEMRESATTRS_PROCESS_PID,
} from "@opentelemetry/semantic-conventions";
import {
Resource,
detectResourcesSync,
envDetectorSync,
hostDetectorSync,
processDetectorSync,
} from "@opentelemetry/resources";
import { DiagConsoleLogger, DiagLogLevel, diag } from "@opentelemetry/api";
import {
AsyncHooksContextManager,
AsyncLocalStorageContextManager,
} from "@opentelemetry/context-async-hooks";
import { context, propagation } from "@opentelemetry/api";
import { VERSION } from "./version";

// For development, uncomment the following line to see debug logs
// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
import { InstrumentationLibraries } from "./instrumentation-libraries";
import {
BatchSpanProcessor,
NodeTracerProvider,
} from "@opentelemetry/sdk-trace-node";
import * as semver from "semver";

// not yet published in '@opentelemetry/semantic-conventions'
const SEMRESATTRS_TELEMETRY_DISTRO_NAME = "telemetry.distro.name";
Expand All @@ -31,45 +46,81 @@ if (!opampServerHost || !instrumentationDeviceId) {
"Missing required environment variables ODIGOS_OPAMP_SERVER_HOST and ODIGOS_INSTRUMENTATION_DEVICE_ID"
);
} else {
const opampClient = new OpAMPClientHttp({
instrumentationDeviceId: instrumentationDeviceId,
opAMPServerHost: opampServerHost,
agentDescriptionIdentifyingAttributes: {
[SEMRESATTRS_TELEMETRY_SDK_LANGUAGE]: TELEMETRYSDKLANGUAGEVALUES_NODEJS,
[SEMRESATTRS_TELEMETRY_DISTRO_VERSION]: VERSION,
[SEMRESATTRS_PROCESS_PID]: process.pid,
},
agentDescriptionNonIdentifyingAttributes: {},
const staticResource = new Resource({
[SEMRESATTRS_TELEMETRY_DISTRO_NAME]: "odigos",
[SEMRESATTRS_TELEMETRY_DISTRO_VERSION]: VERSION,
});

opampClient.start();

const sdk = new NodeSDK({
resourceDetectors: [
const detectorsResource = detectResourcesSync({
detectors: [
// env detector reads resource attributes from the environment.
// we don't populate it at the moment, but if the user set anything, this detector will pick it up
envDetectorSync,
// info about executable, runtime, command, etc
processDetectorSync,
// host name, and arch
hostDetectorSync,
// attributes from OpAMP server, k8s attributes, service name, etc
opampClient,
],
// record additional data about the odigos distro
resource: new Resource({
[SEMRESATTRS_TELEMETRY_DISTRO_NAME]: "odigos",
});

// span processor
const spanProcessor = new BatchSpanProcessor(new OTLPTraceExporter());

// context manager
const ContextManager = semver.gte(process.version, "14.8.0")
? AsyncLocalStorageContextManager
: AsyncHooksContextManager;
const contextManager = new ContextManager();
contextManager.enable();
context.setGlobalContextManager(contextManager);

// propagator
const propagator = new CompositePropagator({
propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
});
propagation.setGlobalPropagator(propagator);

// instrumentation libraries
const instrumentationLibraries = new InstrumentationLibraries();
const localResource = staticResource.merge(detectorsResource);

const opampClient = new OpAMPClientHttp({
instrumentationDeviceId: instrumentationDeviceId,
opAMPServerHost: opampServerHost,
agentDescriptionIdentifyingAttributes: {
[SEMRESATTRS_TELEMETRY_SDK_LANGUAGE]: TELEMETRYSDKLANGUAGEVALUES_NODEJS,
[SEMRESATTRS_TELEMETRY_DISTRO_VERSION]: VERSION,
}),
instrumentations: [getNodeAutoInstrumentations()],
traceExporter: new OTLPTraceExporter(),
[SEMRESATTRS_PROCESS_PID]: process.pid,
},
agentDescriptionNonIdentifyingAttributes: {},
onNewRemoteConfig: (remoteConfig: RemoteConfig) => {
const resource = localResource
.merge(remoteConfig.sdk.remoteResource);

// tracer provider
const tracerProvider = new NodeTracerProvider({
resource,
});
tracerProvider.addSpanProcessor(spanProcessor);
instrumentationLibraries.onNewRemoteConfig(
remoteConfig.instrumentationLibraries,
remoteConfig.sdk.traceSignal,
tracerProvider
);
},
initialPackageStatues: instrumentationLibraries.getPackageStatuses(),
});
sdk.start();

opampClient.start();

const shutdown = async () => {
try {
diag.info("Shutting down OpenTelemetry SDK and OpAMP client");
await Promise.all([sdk.shutdown(), opampClient.shutdown()]);
await Promise.all([
// sdk.shutdown(),
opampClient.shutdown(),
spanProcessor.shutdown(),
]);
} catch (err) {
diag.error("Error shutting down OpenTelemetry SDK and OpAMP client", err);
}
Expand Down
110 changes: 110 additions & 0 deletions agents/nodejs/src/instrumentation-libraries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Instrumentation } from "@opentelemetry/instrumentation";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { ProxyTracerProvider, TracerProvider, diag, trace } from "@opentelemetry/api";
import {
InstrumentationLibraryConfiguration,
TraceSignalGeneralConfig,
} from "./opamp";
import { PackageStatus } from "./opamp/generated/opamp_pb";
import { PartialMessage } from "@bufbuild/protobuf";

type OdigosInstrumentation = {
otelInstrumentation: Instrumentation;
};

const calculateLibraryEnabled = (
traceSignal: TraceSignalGeneralConfig,
instrumentationLibraryEnabled: boolean | undefined,
): boolean => {
// if the signal is disabled globally, no library should be enabled
if (!traceSignal.enabled) {
return false;
}

// if there is a specific configuration for this library, use it
if (instrumentationLibraryEnabled != null) {
return instrumentationLibraryEnabled;
}

// if there is no remote config to enable/disable this library, use the default
return traceSignal.defaultEnabledValue;
};

export class InstrumentationLibraries {
private instrumentations: Instrumentation[];
private instrumentationLibraries: OdigosInstrumentation[];

private noopTracerProvider: TracerProvider;

private logger = diag.createComponentLogger({
namespace: "@odigos/opentelemetry-node/instrumentation-libraries",
});

constructor() {
this.instrumentations = getNodeAutoInstrumentations();

// trick to get the noop tracer provider which is not exported from @openetelemetry/api
this.noopTracerProvider = new ProxyTracerProvider().getDelegate();

this.instrumentationLibraries = this.instrumentations.map(
(otelInstrumentation) => {
// start all instrumentations with a noop tracer provider.
// the global tracer provider is noop by default, so this is just to make sure
otelInstrumentation.setTracerProvider(this.noopTracerProvider);

const odigosInstrumentation = {
otelInstrumentation,
};

return odigosInstrumentation;
}
);
}

public getPackageStatuses(): PartialMessage<PackageStatus>[] {
return this.instrumentations.map((instrumentation) => {
return {
name: instrumentation.instrumentationName,
agentHasVersion: instrumentation.instrumentationVersion,
};
});
}

public onNewRemoteConfig(
configs: InstrumentationLibraryConfiguration[],
traceSignal: TraceSignalGeneralConfig,
enabledTracerProvider: TracerProvider
) {
// it will happen when the pipeline is not setup to receive spans
const globalTracerProvider = traceSignal.enabled
? enabledTracerProvider
: this.noopTracerProvider;
// set global tracer provider to record traces from 3rd party instrumented libraries
// or application manual instrumentation
trace.setGlobalTracerProvider(globalTracerProvider);

// make the configs into a map by library name so it's quicker to find the right one
const configsMap = new Map<string, InstrumentationLibraryConfiguration>(
configs.map((config) => [config.name, config])
);

for (const odigosInstrumentation of this.instrumentationLibraries) {

// for each installed library, calculate it's specific enabled state
// which depends on the global trace signal and the specific library config
const instrumentationLibraryConfig = configsMap.get(
odigosInstrumentation.otelInstrumentation.instrumentationName
);
const enabled = calculateLibraryEnabled(
traceSignal,
instrumentationLibraryConfig?.traces?.enabled,
);
const tracerProviderInUse = enabled
? enabledTracerProvider
: this.noopTracerProvider;
odigosInstrumentation.otelInstrumentation.setTracerProvider(
tracerProviderInUse
);
}
}
}
Loading
Loading