/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import { metrics, trace, diag, DiagConsoleLogger } from '@opentelemetry/api';
import { logs } from '@opentelemetry/api-logs';
import type { Instrumentation } from '@opentelemetry/instrumentation';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import type {
Resource,
ResourceDetectionConfig,
ResourceDetector,
} from '@opentelemetry/resources';
import {
defaultResource,
detectResources,
envDetector,
hostDetector,
processDetector,
resourceFromAttributes,
} from '@opentelemetry/resources';
import type {
LogRecordProcessor,
LogRecordExporter,
} from '@opentelemetry/sdk-logs';
import {
LoggerProvider,
ConsoleLogRecordExporter,
SimpleLogRecordProcessor,
} from '@opentelemetry/sdk-logs';
import { OTLPLogExporter as OTLPHttpLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPLogExporter as OTLPGrpcLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPLogExporter as OTLPProtoLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
import { PrometheusExporter as PrometheusMetricExporter } from '@opentelemetry/exporter-prometheus';
import type { IMetricReader, ViewOptions } from '@opentelemetry/sdk-metrics';
import {
MeterProvider,
ConsoleMetricExporter,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import type { SpanProcessor } from '@opentelemetry/sdk-trace';
import { TracerProvider } from '@opentelemetry/sdk-trace';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import type { NodeSDKConfiguration } from './types';
import {
getBooleanFromEnv,
getNumberFromEnv,
getStringFromEnv,
getStringListFromEnv,
diagLogLevelFromString,
} from '@opentelemetry/core';
import {
getResourceDetectorsFromEnv,
getSpanProcessorsFromEnv,
getPropagatorFromEnv,
setupPropagator,
setupContextManager,
getPeriodicExportingMetricReaderFromEnv,
getOtlpMetricExporterFromEnv,
getBatchLogRecordProcessorFromEnv,
getLoggerProviderConfigFromEnv,
} from './utils';
import {
createBatchSpanProcessorFromEnv,
createSamplerFromEnv,
createSpanLimitsFromEnv,
} from './create-from-env';
export type MeterProviderConfig = {
/**
* Reference to the MetricReader instances by the NodeSDK
*/
readers?: IMetricReader[];
/**
* List of {@link ViewOptions}s that should be passed to the MeterProvider
*/
views?: ViewOptions[];
};
export type LoggerProviderConfig = {
/**
* Reference to the LoggerRecordProcessor instance by the NodeSDK
*/
logRecordProcessors: LogRecordProcessor[];
};
/**
*
* @returns MetricReader[] if appropriate environment variables are configured
*/
function getMetricReadersFromEnv(): IMetricReader[] {
const metricReaders: IMetricReader[] = [];
const enabledExporters = Array.from(
new Set(getStringListFromEnv('OTEL_METRICS_EXPORTER') ?? [])
);
if (enabledExporters.length === 0) {
diag.debug('OTEL_METRICS_EXPORTER is empty. Using default otlp exporter.');
enabledExporters.push('otlp');
}
if (enabledExporters.includes('none')) {
diag.info(
'OTEL_METRICS_EXPORTER contains "none". Metric provider will not be initialized.'
);
return metricReaders;
}
enabledExporters.forEach(exporter => {
if (exporter === 'otlp') {
metricReaders.push(
getPeriodicExportingMetricReaderFromEnv(getOtlpMetricExporterFromEnv())
);
} else if (exporter === 'console') {
metricReaders.push(
new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
})
);
} else if (exporter === 'prometheus') {
metricReaders.push(
new PrometheusMetricExporter({
host: getStringFromEnv('OTEL_EXPORTER_PROMETHEUS_HOST'),
port: getNumberFromEnv('OTEL_EXPORTER_PROMETHEUS_PORT'),
})
);
} else {
diag.warn(
`Unsupported OTEL_METRICS_EXPORTER value: "${exporter}". Supported values are: otlp, console, prometheus, none.`
);
}
});
return metricReaders;
}
/**
* A setup helper for the OpenTelemetry SDKs (logs, metrics, traces).
*
After successful setup using {@link NodeSDK#start()}, use `@opentelemetry/api` to obtain the registered components.
*
Use the shutdown handler {@link NodeSDK#shutdown()} to ensure your telemetry is exported before the process exits.
*
* @example
Register SDK by using environment variables
* const nodeSdk = new NodeSDK(); // providing no options uses OTEL_* environment variables for SDK setup.
* nodeSdk.start(); // registers all configured SDK components
* @example Override environment variable config with your own components
* const nodeSdk = new NodeSDK({
* // override the list of metric reader with your own options and ignore environment variable config
* // explore the docs of other options to learn more!
* metricReaders: [ new PeriodicExportingMetricReader({
* exporter: new OTLPMetricsExporter()
* })]
* });
* nodeSdk.start(); // registers all configured SDK components
*/
export class NodeSDK {
private _loggerProviderConfig?: LoggerProviderConfig;
private _meterProviderConfig?: MeterProviderConfig;
private _instrumentations: Instrumentation[];
private _resource: Resource;
private _resourceDetectors: Array;
private _autoDetectResources: boolean;
private _tracerProvider?: TracerProvider;
private _loggerProvider?: LoggerProvider;
private _meterProvider?: MeterProvider;
private _serviceName?: string;
private _configuration?: Partial;
private _disabled?: boolean;
/**
* Create a new NodeJS SDK instance
*/
public constructor(configuration: Partial = {}) {
if (getBooleanFromEnv('OTEL_SDK_DISABLED')) {
this._disabled = true;
// Functions with possible side-effects are set
// to no-op via the _disabled flag
}
const logLevel = getStringFromEnv('OTEL_LOG_LEVEL');
if (logLevel != null) {
diag.setLogger(new DiagConsoleLogger(), {
logLevel: diagLogLevelFromString(logLevel),
});
}
this._configuration = configuration;
this._resource = configuration.resource ?? defaultResource();
this._autoDetectResources = configuration.autoDetectResources ?? true;
if (!this._autoDetectResources) {
this._resourceDetectors = [];
} else if (configuration.resourceDetectors != null) {
this._resourceDetectors = configuration.resourceDetectors;
} else if (getStringFromEnv('OTEL_NODE_RESOURCE_DETECTORS')) {
this._resourceDetectors = getResourceDetectorsFromEnv();
} else {
this._resourceDetectors = [envDetector, processDetector, hostDetector];
}
this._serviceName = configuration.serviceName;
if (configuration.logRecordProcessors) {
this._loggerProviderConfig = {
logRecordProcessors: configuration.logRecordProcessors,
};
}
if (configuration.metricReaders) {
this._meterProviderConfig = {
readers: configuration.metricReaders,
views: configuration.views,
};
} else {
this._meterProviderConfig = {
readers: getMetricReadersFromEnv(),
views: configuration.views,
};
}
this._instrumentations = configuration.instrumentations?.flat() ?? [];
}
/**
* Call this method to construct SDK components and register them with the OpenTelemetry API.
*/
public start(): void {
if (this._disabled) {
return;
}
registerInstrumentations({
instrumentations: this._instrumentations,
});
setupContextManager(this._configuration?.contextManager);
setupPropagator(
this._configuration?.textMapPropagator === null
? null // null means don't set, so we cannot fall back to env config.
: (this._configuration?.textMapPropagator ?? getPropagatorFromEnv())
);
if (this._autoDetectResources) {
const internalConfig: ResourceDetectionConfig = {
detectors: this._resourceDetectors,
};
this._resource = this._resource.merge(detectResources(internalConfig));
}
this._resource =
this._serviceName === undefined
? this._resource
: this._resource.merge(
resourceFromAttributes({
[ATTR_SERVICE_NAME]: this._serviceName,
})
);
// While SDK metrics are unstable, we require an opt-in.
// https://opentelemetry.io/docs/specs/semconv/otel/sdk-metrics/
const sdkMetricsEnabled = getBooleanFromEnv(
'OTEL_NODE_EXPERIMENTAL_SDK_METRICS'
);
if (
this._meterProviderConfig?.readers &&
// only register if there is a reader, otherwise we waste compute/memory.
this._meterProviderConfig.readers.length > 0
) {
const meterProvider = new MeterProvider({
resource: this._resource,
views: this._meterProviderConfig?.views ?? [],
readers: this._meterProviderConfig.readers,
sdkMetricsEnabled,
});
this._meterProvider = meterProvider;
metrics.setGlobalMeterProvider(meterProvider);
// TODO: This is a workaround to fix https://github.com/open-telemetry/opentelemetry-js/issues/3609
// If the MeterProvider is not yet registered when instrumentations are registered, all metrics are dropped.
// This code is obsolete once https://github.com/open-telemetry/opentelemetry-js/issues/3622 is implemented.
for (const instrumentation of this._instrumentations) {
instrumentation.setMeterProvider(metrics.getMeterProvider());
}
}
// Determine `spanProcessors` from configuration options.
let spanProcessors: SpanProcessor[];
if (this._configuration?.spanProcessors) {
spanProcessors = this._configuration.spanProcessors;
} else if (this._configuration?.traceExporter) {
spanProcessors = [
createBatchSpanProcessorFromEnv(
this._configuration.traceExporter!,
sdkMetricsEnabled ? this._meterProvider : undefined
),
];
} else {
spanProcessors = getSpanProcessorsFromEnv(
sdkMetricsEnabled ? this._meterProvider : undefined
);
}
// Only register if there is a span processor
if (spanProcessors.length > 0) {
this._tracerProvider = new TracerProvider({
sampler: this._configuration?.sampler ?? createSamplerFromEnv(),
spanLimits: {
...createSpanLimitsFromEnv(),
...this._configuration?.spanLimits,
},
resource: this._resource,
meterProvider: sdkMetricsEnabled ? this._meterProvider : undefined,
idGenerator: this._configuration?.idGenerator,
spanProcessors,
});
trace.setGlobalTracerProvider(this._tracerProvider);
}
if (!this._loggerProviderConfig) {
this.configureLoggerProviderFromEnv(
sdkMetricsEnabled ? this._meterProvider : undefined
);
}
if (this._loggerProviderConfig) {
const loggerProvider = new LoggerProvider({
...getLoggerProviderConfigFromEnv(),
resource: this._resource,
processors: this._loggerProviderConfig.logRecordProcessors,
meterProvider: sdkMetricsEnabled ? this._meterProvider : undefined,
});
this._loggerProvider = loggerProvider;
logs.setGlobalLoggerProvider(loggerProvider);
}
}
public shutdown(): Promise {
const promises: Promise[] = [];
if (this._tracerProvider) {
promises.push(this._tracerProvider.shutdown());
}
if (this._loggerProvider) {
promises.push(this._loggerProvider.shutdown());
}
if (this._meterProvider) {
promises.push(this._meterProvider.shutdown());
}
return (
Promise.all(promises)
// return void instead of the array from Promise.all
.then(() => {})
);
}
private configureLoggerProviderFromEnv(
meterProvider: MeterProvider | undefined
): void {
const enabledExporters = Array.from(
new Set(getStringListFromEnv('OTEL_LOGS_EXPORTER') ?? [])
);
if (enabledExporters.length === 0) {
diag.debug('OTEL_LOGS_EXPORTER is empty. Using default otlp exporter.');
enabledExporters.push('otlp');
}
if (enabledExporters.includes('none')) {
diag.info(
'OTEL_LOGS_EXPORTER contains "none". Logger provider will not be initialized.'
);
return;
}
const exporters: LogRecordExporter[] = [];
enabledExporters.forEach(exporter => {
if (exporter === 'otlp') {
const protocol =
(
getStringFromEnv('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL') ??
getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL')
)?.trim() || 'http/protobuf'; // Using || to also fall back on empty string
switch (protocol) {
case 'grpc':
exporters.push(new OTLPGrpcLogExporter());
break;
case 'http/json':
exporters.push(new OTLPHttpLogExporter());
break;
case 'http/protobuf':
exporters.push(new OTLPProtoLogExporter());
break;
default:
diag.warn(
`Unsupported OTLP logs protocol: "${protocol}". Using http/protobuf.`
);
exporters.push(new OTLPProtoLogExporter());
}
} else if (exporter === 'console') {
exporters.push(new ConsoleLogRecordExporter());
} else {
diag.warn(
`Unsupported OTEL_LOGS_EXPORTER value: "${exporter}". Supported values are: otlp, console, none.`
);
}
});
if (exporters.length > 0) {
this._loggerProviderConfig = {
logRecordProcessors: exporters.map(exporter => {
if (exporter instanceof ConsoleLogRecordExporter) {
return new SimpleLogRecordProcessor({
exporter,
selfObsMeterProvider: meterProvider,
});
} else {
return getBatchLogRecordProcessorFromEnv(exporter, meterProvider);
}
}),
};
}
}
}