packages/opentelemetry-node/lib/sdk.js (146 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and contributors * SPDX-License-Identifier: Apache-2.0 */ // This is the '@elastic/opentelemetry-node/sdk' entry-point. const os = require('os'); const { getBooleanFromEnv, getStringFromEnv, getStringListFromEnv, } = require('@opentelemetry/core'); const { api, core, logs, metrics, node, resources, tracing, NodeSDK, } = require('@opentelemetry/sdk-node'); const {BatchLogRecordProcessor} = require('@opentelemetry/sdk-logs'); const {createAddHookMessageChannel} = require('import-in-the-middle'); const {log, registerOTelDiagLogger} = require('./logging'); const luggite = require('./luggite'); const {resolveDetectors} = require('./detectors'); const {setupEnvironment, restoreEnvironment} = require('./environment'); const {getInstrumentations} = require('./instrumentations'); const {enableHostMetrics, HOST_METRICS_VIEWS} = require('./metrics/host'); const DISTRO_VERSION = require('../package.json').version; /** * @typedef {import('@opentelemetry/sdk-node').NodeSDKConfiguration} NodeSDKConfiguration */ /** * @typedef {Object} ElasticNodeSDKOptions * @property {boolean} elasticSetupShutdownHandlers - Whether to setup handlers * on `process` events to shutdown the SDK. Default true. * * Note: To avoid collisions with NodeSDKConfiguration properties, all/most * properities of this type should be prefixed with "elastic". */ function setupShutdownHandlers(sdk) { // TODO avoid possible double sdk.shutdown(). I think that results in unnecessary work. process.on('SIGTERM', async () => { try { await sdk.shutdown(); } catch (err) { console.warn('warning: error shutting down OTel SDK', err); } process.exit(128 + os.constants.signals.SIGTERM); }); process.once('beforeExit', async () => { // Flush recent telemetry data if about the shutdown. try { await sdk.shutdown(); } catch (err) { console.warn('warning: error shutting down OTel SDK', err); } }); } /** * Create and start an OpenTelemetry NodeSDK. * * While this returns an object with `shutdown()` method, the default behavior * is to setup `process.on(...)` handlers to handle shutdown. See the * `setupShutdownHandlers` boolean option. * * @param {Partial<NodeSDKConfiguration & ElasticNodeSDKOptions>} cfg * @returns {{ shutdown(): Promise<void>; }} */ function startNodeSDK(cfg = {}) { log.trace('startNodeSDK cfg:', cfg); // TODO: test behaviour with OTEL_SDK_DISABLED. // Do we still log preamble? See NodeSDK _disabled handling. // Do we still attempt to enableHostMetrics()? if (getBooleanFromEnv('OTEL_SDK_DISABLED')) { // Note: This differs slightly from current NodeSDK, which does *some* // processing, even if disabled. log.trace('startNodeSDK: disabled'); return { shutdown() { return Promise.resolve(); }, }; } registerOTelDiagLogger(api); /** @type {Partial<NodeSDKConfiguration>} */ const defaultConfig = { resourceDetectors: resolveDetectors(cfg.resourceDetectors), instrumentations: cfg.instrumentations || getInstrumentations(), // Avoid setting `spanProcessor` or `traceExporter` to have NodeSDK // use its `TracerProviderWithEnvExporters` for tracing setup. }; const exporterPkgNameFromEnvVar = { grpc: 'grpc', 'http/json': 'http', 'http/protobuf': 'proto', // default }; // Logs config. const logsExportProtocol = getStringFromEnv('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL') || getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') || 'http/protobuf'; let logExporterType = exporterPkgNameFromEnvVar[logsExportProtocol]; if (!logExporterType) { log.warn( `Logs exporter protocol "${logsExportProtocol}" unknown. Using default "http/protobuf" protocol` ); logExporterType = 'proto'; } log.trace(`Logs exporter protocol set to ${logsExportProtocol}`); const {OTLPLogExporter} = require( `@opentelemetry/exporter-logs-otlp-${logExporterType}` ); defaultConfig.logRecordProcessors = [ new BatchLogRecordProcessor(new OTLPLogExporter()), ]; // Metrics config. // TODO: support `OTEL_METRICS_EXPORTER`, including being a list of exporters (e.g. console, debug) // TODO: metrics exporter should do for metrics what `TracerProviderWithEnvExporters` does for traces, does that include `url` export endpoint? // Setting default temporality to delta to avoid histogram storing issues in ES. // Or log if there is a different value set by the user // Ref: https://github.com/elastic/opentelemetry/pull/63 const temporalityPreference = getStringFromEnv( 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE' ); if (typeof temporalityPreference === 'undefined') { process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = 'delta'; } else if (temporalityPreference !== 'delta') { const docsUrl = 'https://elastic.github.io/opentelemetry/compatibility/limitations.html#ingestion-of-metrics-data'; log.info( `Metrics temporality preference set to "${temporalityPreference}". Use "delta" temporality if you want to store Histogram metrics in Elasticsearch. See ${docsUrl}` ); } // The implementation in SDK does treats the undefined and 'none' value // as same. https://github.com/open-telemetry/opentelemetry-js/blob/dac72912b3a895c91ee95cfa39a22a916411ba4c/experimental/packages/opentelemetry-sdk-node/src/sdk.ts#L117 // According to https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection // the default should be 'otlp' so IMHO the implementation is wrong // We need to set the default value if env var is not defined until it's fixed in // upstream SDK // TODO: remove this when https://github.com/open-telemetry/opentelemetry-js/issues/5612 is closed if (!process.env.OTEL_METRICS_EXPORTER?.trim()) { process.env.OTEL_METRICS_EXPORTER = 'otlp'; } const metricsExporters = getStringListFromEnv('OTEL_METRICS_EXPORTER'); const metricsEnabled = metricsExporters.every((e) => e !== 'none'); if (metricsEnabled) { defaultConfig.views = [ // Add views for `host-metrics` to avoid excess of data being sent // to the server. ...HOST_METRICS_VIEWS, ]; } const config = Object.assign(defaultConfig, cfg); setupEnvironment(); const sdk = new NodeSDK(config); if (config.elasticSetupShutdownHandlers ?? true) { setupShutdownHandlers(sdk); } log.info( { preamble: true, distroVersion: DISTRO_VERSION, env: { // For darwin: https://en.wikipedia.org/wiki/Darwin_%28operating_system%29#Release_history os: `${os.platform()} ${os.release()}`, arch: os.arch(), runtime: `Node.js ${process.version}`, }, // The "config" object structure is not stable. config: { logLevel: luggite.nameFromLevel[log.level()] ?? log.level(), }, }, 'start EDOT Node.js' ); sdk.start(); // .start() *does* use `process.env` though I think it should not. restoreEnvironment(); if (metricsEnabled) { // TODO: make this configurable, user might collect host metrics with a separate utility. Perhaps use 'host-metrics' in DISABLED_INSTRs existing env var. enableHostMetrics(); } // Return an object that is a subset of the upstream NodeSDK interface, // just enough to shutdown. return { shutdown() { return sdk.shutdown(); }, }; } module.exports = { getInstrumentations, startNodeSDK, createAddHookMessageChannel, // re-export from import-in-the-middle // Re-exports from sdk-node, so that users bootstrapping EDOT Node.js in // code can access useful parts of the SDK. One difference from the // re-exports in `@opentelmetry/sdk-node`, is that we are not re-exporting // `contextBase`. It is a dupe of `api` and feels vestigial. api, core, logs, metrics, node, resources, tracing, };