Skip to content

Commit

Permalink
Rewrite to use @opentelemetry/sdk-node
Browse files Browse the repository at this point in the history
  • Loading branch information
myrotvorets-team committed Sep 26, 2023
1 parent 6a0a5d1 commit 9039aa0
Show file tree
Hide file tree
Showing 6 changed files with 557 additions and 196 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ permissions:
jobs:
prepare:
name: Prepare source code
permissions:
contents: read
packages: read
runs-on: ubuntu-latest
if: github.event_name == 'release' || github.event.inputs.gpr == 'yes'
steps:
- name: Prepare source
uses: myrotvorets/composite-actions/node-prepublish@master
with:
registry-url: https://npm.pkg.github.com

publish:
name: Publish package (${{ matrix.registry }})
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/push-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ permissions:
jobs:
build:
name: Build and test
permissions:
contents: read
packages: read
runs-on: ubuntu-latest
steps:
- name: Build and test
Expand Down
177 changes: 46 additions & 131 deletions lib/index.mts
Original file line number Diff line number Diff line change
@@ -1,166 +1,81 @@
import { type ContextManager, DiagConsoleLogger, type TextMapPropagator, diag } from '@opentelemetry/api';
import { getEnvWithoutDefaults } from '@opentelemetry/core';
import { type InstrumentationOption, registerInstrumentations } from '@opentelemetry/instrumentation';
import { BatchSpanProcessor, SimpleSpanProcessor, SpanExporter } from '@opentelemetry/sdk-trace-base';
import { type NodeTracerConfig, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
type DetectorSync,
type IResource,
Resource,
type ResourceDetectionConfig,
detectResourcesSync,
processDetector,
} from '@opentelemetry/resources';
import { processDetectorSync } from '@opentelemetry/resources';
import { NodeSDK, type NodeSDKConfiguration } from '@opentelemetry/sdk-node';
import { BatchSpanProcessor, NoopSpanProcessor, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import {
dockerDetector,
k8sDetector,
osDetector,
packageJsonDetector,
} from '@myrotvorets/opentelemetry-resource-detectors';

export interface Config {
serviceName: string;
instrumentations?: InstrumentationOption[];
resource?: IResource;
detectors?: DetectorSync[];
tracer?: Omit<NodeTracerConfig, 'plugins' | 'resource'>;
traceExporter?: SpanExporter;
contextManager?: ContextManager;
propagator?: TextMapPropagator;
}
export type Config = { serviceName: string } & Omit<Partial<NodeSDKConfiguration>, 'serviceName'>;

export class OpenTelemetryConfigurator {
private readonly serviceName: string;
private tracerProvider?: NodeTracerProvider;
private readonly nodeTracerConfig: NodeTracerConfig;
private readonly resourceDetectionConfig: ResourceDetectionConfig;
private readonly traceExporter?: SpanExporter;
private readonly instrumentations?: InstrumentationOption[];
private readonly contextManager?: ContextManager;
private readonly propagator?: TextMapPropagator;
private unloader?: () => void;
private readonly _config: Config;
private readonly _sdk: NodeSDK;
private _started = false;

public constructor(config: Config) {
this.serviceName = config.serviceName;

const envWithoutDefaults = getEnvWithoutDefaults();

if (envWithoutDefaults.OTEL_LOG_LEVEL) {
diag.setLogger(new DiagConsoleLogger(), {
logLevel: envWithoutDefaults.OTEL_LOG_LEVEL,
});
this._config = config;

if (!this._config.traceExporter && !this._config.spanProcessor) {
this._config.traceExporter = process.env.OTEL_EXPORTER_ZIPKIN_ENDPOINT ? new ZipkinExporter() : undefined;
if (this._config.traceExporter) {
/* c8 disable start */
this._config.spanProcessor =
process.env.NODE_ENV === 'production'
? new BatchSpanProcessor(this._config.traceExporter)
: new SimpleSpanProcessor(this._config.traceExporter);
/* c8 disable end */
} else {
this._config.spanProcessor = new NoopSpanProcessor();
}
}

this.nodeTracerConfig = {
...(config.tracer ?? {}),
resource: config.resource ?? Resource.empty(),
};

this.resourceDetectionConfig = {
detectors: config.detectors ?? [
processDetector,
if (!this._config.resourceDetectors?.length && false !== this._config.autoDetectResources) {
this._config.resourceDetectors = [
osDetector,
packageJsonDetector,
k8sDetector,
dockerDetector,
],
};
k8sDetector,
packageJsonDetector,
processDetectorSync,
];
}

this.traceExporter = OpenTelemetryConfigurator.getTraceExporter(config.serviceName, config.traceExporter);
this.instrumentations = config.instrumentations;
this.contextManager = config.contextManager;
this.propagator = config.propagator;
this._sdk = new NodeSDK(this._config);
}

public start(): void {
if (this.tracerProvider) {
return;
}

this.unloader = registerInstrumentations({
instrumentations: this.instrumentations,
});
public start(): boolean {
if (!this._started) {
this._sdk.start();

this.detectResources();
this.tracerProvider = new NodeTracerProvider(this.nodeTracerConfig);
process.once('SIGINT', this.shutdownHandler);
process.once('SIGTERM', this.shutdownHandler);
process.once('SIGQUIT', this.shutdownHandler);

if (this.traceExporter) {
const SpanProcessor = process.env.NODE_ENV === 'production' ? BatchSpanProcessor : SimpleSpanProcessor;
this.tracerProvider.addSpanProcessor(new SpanProcessor(this.traceExporter));
this._started = true;
return true;
}

this.tracerProvider.register({
contextManager: this.contextManager,
propagator: this.propagator,
});

process.once('SIGINT', this.shutdownHandler);
process.once('SIGTERM', this.shutdownHandler);
process.once('SIGQUIT', this.shutdownHandler);
return false;
}

public async shutdown(): Promise<void> {
const promises: Promise<unknown>[] = [];
if (this.tracerProvider) {
// Trace provider will shut down the span processor
// Span processor will shut down the span exporter
promises.push(this.tracerProvider.shutdown());
public async shutdown(): Promise<boolean> {
if (this._started) {
this._started = false;
await this._sdk.shutdown();
return true;
}

process.off('SIGINT', this.shutdownHandler);
process.off('SIGTERM', this.shutdownHandler);
process.off('SIGQUIT', this.shutdownHandler);

this.tracerProvider = undefined;

this.unloader?.();
this.unloader = undefined;
await Promise.all(promises);
return false;
}

private readonly shutdownHandler = (): void => {
this.shutdown().catch((e) => console.error(e));
};

private detectResources(): void {
if (this.resourceDetectionConfig.detectors?.length) {
const resource = detectResourcesSync(this.resourceDetectionConfig);
this.nodeTracerConfig.resource = (this.nodeTracerConfig.resource as Resource).merge(resource);
}

(this.nodeTracerConfig.resource as Resource).merge(
new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: this.serviceName,
}),
);
}

private static getTraceExporter(
serviceName: string,
traceExporter: SpanExporter | undefined,
): SpanExporter | undefined {
if (traceExporter) {
return traceExporter;
}

/* c8 ignore start */
if (process.env.OTEL_EXPORTER_ZIPKIN_ENDPOINT || process.env.ZIPKIN_ENDPOINT) {
// Environment variables:
// OTEL_EXPORTER_ZIPKIN_ENDPOINT (ZIPKIN_ENDPOINT): Endpoint for Zipkin traces
// OTEL_EXPORTER_ZIPKIN_TIMEOUT: Maximum time the Zipkin exporter will wait for each batch export (10s by default)
return new ZipkinExporter({
url: process.env.OTEL_EXPORTER_ZIPKIN_ENDPOINT ?? process.env.ZIPKIN_ENDPOINT,
serviceName,
});
}
/* c8 ignore end */

return undefined;
}

public getTraceProvider(): NodeTracerProvider | undefined {
return this.tracerProvider;
public get config(): Readonly<Config> {
return this._config;
}
}
Loading

0 comments on commit 9039aa0

Please sign in to comment.