Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
60 changes: 30 additions & 30 deletions packages/opentelemetry-node/lib/detectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
*/

/**
* NOTE: when `Detector` is finally removed import only `DetectorSync` and
* get rid of the aliasing
* @typedef {import('@opentelemetry/resources').Detector} DetectorOrig
* @typedef {import('@opentelemetry/resources').DetectorSync} DetectorSyncOrig
* @typedef {DetectorOrig | DetectorSyncOrig} DetectorSync
* @typedef {import('@opentelemetry/resources').ResourceDetector} ResourceDetector
*/

const {
Expand All @@ -31,41 +27,42 @@ const {
} = require('@opentelemetry/resource-detector-container');
const {gcpDetector} = require('@opentelemetry/resource-detector-gcp');
const {
envDetectorSync,
hostDetectorSync,
osDetectorSync,
processDetectorSync,
serviceInstanceIdDetectorSync,
Resource,
envDetector,
hostDetector,
osDetector,
processDetector,
serviceInstanceIdDetector,
} = require('@opentelemetry/resources');

const {getEnvVar} = require('./environment');
const {log} = require('./logging');
const {getEnvStringList} = require('./environment');

// @ts-ignore - compiler options do not allow lookp outside `lib` folder
const ELASTIC_SDK_VERSION = require('../package.json').version;

// Elastic's own detector to add distro related metadata
/** @type {DetectorSync} */
const distroDetectorSync = {
/** @type {ResourceDetector} */
const distroDetector = {
detect() {
// TODO: change to semconv resource attribs when
// `@opentelemetry/semantic-conventions` gets updated with the attribs used
// https://github.com/open-telemetry/opentelemetry-js/issues/4235
return new Resource({
'telemetry.distro.name': 'elastic',
'telemetry.distro.version': ELASTIC_SDK_VERSION,
});
return {
attributes: {
'telemetry.distro.name': 'elastic',
'telemetry.distro.version': ELASTIC_SDK_VERSION,
},
};
},
};

/** @type {Record<string, DetectorSync | Array<DetectorSync>>} */
/** @type {Record<string, ResourceDetector | Array<ResourceDetector>>} */
const defaultDetectors = {
env: envDetectorSync,
process: processDetectorSync,
serviceinstance: serviceInstanceIdDetectorSync,
os: osDetectorSync,
host: hostDetectorSync,
env: envDetector,
process: processDetector,
serviceinstance: serviceInstanceIdDetector,
os: osDetector,
host: hostDetector,
container: containerDetector,
alibaba: alibabaCloudEcsDetector,
aws: [
Expand All @@ -80,25 +77,28 @@ const defaultDetectors = {
};

/**
* @param {Array<DetectorSync>} [detectors]
* @returns {Array<DetectorSync>}
* @param {Array<ResourceDetector>} [detectors]
* @returns {Array<ResourceDetector>}
*/
function resolveDetectors(detectors) {
if (detectors) {
detectors.push(distroDetectorSync);
detectors.push(distroDetector);
return detectors;
}

let detectorKeys = getEnvVar('OTEL_NODE_RESOURCE_DETECTORS');
let detectorKeys = getEnvStringList('OTEL_NODE_RESOURCE_DETECTORS', [
'all',
]);
if (detectorKeys.some((k) => k === 'all')) {
detectorKeys = Object.keys(defaultDetectors);
} else if (detectorKeys.some((k) => k === 'none')) {
return [];
}

/** @type {Array<DetectorSync | DetectorSync[]>} */
const resolvedDetectors = [distroDetectorSync];
/** @type {Array<ResourceDetector | ResourceDetector[]>} */
const resolvedDetectors = [distroDetector];
for (const key of detectorKeys) {
log.warn(`resolving detector ${key}`);
if (defaultDetectors[key]) {
resolvedDetectors.push(defaultDetectors[key]);
} else {
Expand Down
25 changes: 19 additions & 6 deletions packages/opentelemetry-node/lib/elastic-node-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const {resolveDetectors} = require('./detectors');
const {
setupEnvironment,
restoreEnvironment,
getEnvVar,
getEnvString,
getEnvBoolean,
getEnvNumber,
} = require('./environment');
const {getInstrumentations} = require('./instrumentations');
const {enableHostMetrics, HOST_METRICS_VIEWS} = require('./metrics/host');
Expand Down Expand Up @@ -53,7 +55,7 @@ class ElasticNodeSDK extends NodeSDK {
// Get logs exporter protocol based on environment.
const logsExportProtocol =
process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ||
getEnvVar('OTEL_EXPORTER_OTLP_PROTOCOL');
getEnvString('OTEL_EXPORTER_OTLP_PROTOCOL');
Copy link
Member

Choose a reason for hiding this comment

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

% node --import @elastic/opentelemetry-node simple-http-request.js
{"name":"elastic-otel-node","level":40,"msg":"Logs exporter protocol \"undefined\" unknown. Using default \"http/protobuf\" protocol","time":"2025-03-18T20:10:04.261Z"}
...

I think that log warn is because this doesn't have a default fallback now.

let logExporterType = exporterPkgNameFromEnvVar[logsExportProtocol];
if (!logExporterType) {
log.warn(
Expand All @@ -77,12 +79,15 @@ class ElasticNodeSDK extends NodeSDK {
// TODO what `temporalityPreference`?

// Disable metrics by config
const metricsDisabled = getEnvVar('ELASTIC_OTEL_METRICS_DISABLED');
const metricsDisabled = getEnvBoolean(
'ELASTIC_OTEL_METRICS_DISABLED',
false
);
if (!metricsDisabled) {
// Get metrics exporter protocol based on environment.
const metricsExportProtocol =
process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL ||
getEnvVar('OTEL_EXPORTER_OTLP_PROTOCOL');
getEnvString('OTEL_EXPORTER_OTLP_PROTOCOL');
Copy link
Member

Choose a reason for hiding this comment

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

% node --import @elastic/opentelemetry-node simple-http-request.js
...
{"name":"elastic-otel-node","level":40,"msg":"Metrics exporter protocol \"undefined\" unknown. Using default \"http/protobuf\" protocol","time":"2025-03-18T20:10:04.263Z"}
...

I think that log warn is because this doesn't have a default fallback now.

let metricExporterType =
exporterPkgNameFromEnvVar[metricsExportProtocol];
if (!metricExporterType) {
Expand All @@ -98,8 +103,16 @@ class ElasticNodeSDK extends NodeSDK {
`@opentelemetry/exporter-metrics-otlp-${metricExporterType}`
);

const metricsInterval = getEnvVar('OTEL_METRIC_EXPORT_INTERVAL');
const metricsTimeout = getEnvVar('OTEL_METRIC_EXPORT_TIMEOUT');
// Note: Default values has been taken from the specs
// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#periodic-exporting-metricreader
const metricsInterval = getEnvNumber(
'OTEL_METRIC_EXPORT_INTERVAL',
60000
);
const metricsTimeout = getEnvNumber(
'OTEL_METRIC_EXPORT_TIMEOUT',
30000
);
defaultConfig.metricReader =
new metrics.PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
Expand Down
136 changes: 32 additions & 104 deletions packages/opentelemetry-node/lib/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,16 @@
* SPDX-License-Identifier: Apache-2.0
*/

// NOTE: this API may be removed in future
// ref: https://github.com/open-telemetry/opentelemetry-js/issues/5172
const {getEnv} = require('@opentelemetry/core');
const {
getBooleanFromEnv,
getNumberFromEnv,
getStringListFromEnv,
getStringFromEnv,
} = require('@opentelemetry/core');

/** @type {NodeJS.ProcessEnv} */
const envToRestore = {};

/**
* Returns an array of strings from the given input. If undefined returns the fallback
* value.
* @param {string | undefined} str
* @param {string[]} [fallback=[]]
* @returns {string[]}
*/
function parseStringList(str, fallback = []) {
if (!str) {
return fallback;
}
return str.split(',').map((s) => s.trim());
}

/**
* Returns a boolean from the given input
* @param {string | undefined} str
* @param {boolean} fallback
* @returns {boolean}
*/
function parseBoolean(str, fallback) {
if (!str) {
return fallback;
}
return str.toLowerCase() === 'true';
}

/**
* Returns a boolean from te given input
* @param {string | undefined} str
* @param {number} fallback
* @returns {number}
*/
function parseNumber(str, fallback) {
if (!str) {
return fallback;
}

const num = Number(str);
return isNaN(num) ? fallback : num;
}

/**
* This funtion makes necessari changes to the environment so:
* - Avoid OTEL's NodeSDK known warnings (eg. OTEL_TRACES_EXPORTER not set)
Expand Down Expand Up @@ -86,75 +47,42 @@ function setupEnvironment() {
function restoreEnvironment() {
Object.keys(envToRestore).forEach((k) => {
process.env[k] = envToRestore[k];
delete envToRestore[k];
Copy link
Member Author

Choose a reason for hiding this comment

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

note for reviewer: IMO the getters defined below should take values directly from prcess.env once the environment vars are restored.

});
}

/**
* @typedef {Object} EdotEnv
* @property {string[]} OTEL_NODE_RESOURCE_DETECTORS
* @property {number} OTEL_METRIC_EXPORT_INTERVAL
* @property {number} OTEL_METRIC_EXPORT_TIMEOUT
* @property {boolean} ELASTIC_OTEL_METRICS_DISABLED
*/
/**
* @typedef {keyof EdotEnv} EdotEnvKey
*/
/** @type {EdotEnv} */
const edotEnv = {
// Missing OTEL_ vars from global spec and nodejs specific spec
OTEL_NODE_RESOURCE_DETECTORS: parseStringList(
process.env.OTEL_NODE_RESOURCE_DETECTORS,
['all']
),
// Note: Default values has been taken from the specs
// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#periodic-exporting-metricreader
OTEL_METRIC_EXPORT_INTERVAL: parseNumber(
process.env.OTEL_METRIC_EXPORT_INTERVAL,
60000
),
OTEL_METRIC_EXPORT_TIMEOUT: parseNumber(
process.env.OTEL_METRIC_EXPORT_TIMEOUT,
30000
),
// ELASTIC_OTEL_ vars
ELASTIC_OTEL_METRICS_DISABLED: parseBoolean(
process.env.ELASTIC_OTEL_METRICS_DISABLED,
false
),
};

/**
* @typedef {import('@opentelemetry/core').ENVIRONMENT} OtelEnv
*/
/**
* @typedef {keyof OtelEnv} OtelEnvKey
*/
const otelEnv = getEnv();

/**
* @template T
* @typedef {T extends OtelEnvKey ? OtelEnv[T] : T extends EdotEnvKey ? EdotEnv[T] : never} EnvValue<T>
*/
/**
* @template {OtelEnvKey | EdotEnvKey} T
* Returns the value of the env var already parsed to the proper type. If
* the variable is not defined it will return the default value based on
* the environmment variables spec https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
* @param {T} name
* @returns {EnvValue<T>}
* @param {(name: string) => T} getterFn
* @returns {(name: string, defaultValue?: T) => T}
*/
function getEnvVar(name) {
if (name in otelEnv) {
// @ts-ignore -- T is {keyof OtelEnv} but not sure how to make TS infer that
return otelEnv[name];
}
function makeEnvVarGetter(getterFn) {
Copy link
Member

Choose a reason for hiding this comment

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

Hrm. AFAICT, the only reason for the isStashed handling in here is because in elastic-node-sdk.js we are currently calling setupEnvironment before the call the resolveDetectors that uses the OTEL_NODE_RESOURCE_DETECTORS envvar value.

What if we move setupEnvironment() to be just before the super(configuration) call. I think that was the original intent. The only reason the setupEnvironment / restoreEnvironment things are done is to tweak the env for when the NodeSDK constructor runs. (Possibly also its .start() as well.)

Then if the isStashed stuff isn't needed, we could drop the getEnv* functions and just use the get*FromEnv functions from @opentelemetry/core directly, right?

Copy link
Member

Choose a reason for hiding this comment

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

See commit 3c28ba4 for an attempt at this.

return function (name, defaultValue = undefined) {
const isStashed = name in envToRestore;
let result;

// @ts-ignore -- T is {keyof EdotEnv} but not sure how to make TS infer that
return edotEnv[name];
if (isStashed) {
process.env[name] = envToRestore[name];
}
result = getterFn(name);
if (isStashed) {
delete process.env[name];
}

return typeof result === 'undefined' ? defaultValue : result;
};
}

const getEnvBoolean = makeEnvVarGetter(getBooleanFromEnv);
const getEnvNumber = makeEnvVarGetter(getNumberFromEnv);
const getEnvString = makeEnvVarGetter(getStringFromEnv);
const getEnvStringList = makeEnvVarGetter(getStringListFromEnv);

module.exports = {
setupEnvironment,
restoreEnvironment,
getEnvVar,
getEnvBoolean,
getEnvNumber,
getEnvString,
getEnvStringList,
};
11 changes: 7 additions & 4 deletions packages/opentelemetry-node/lib/instrumentations.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ const {TediousInstrumentation} = require('@opentelemetry/instrumentation-tedious
const {UndiciInstrumentation} = require('@opentelemetry/instrumentation-undici');
const {WinstonInstrumentation} = require('@opentelemetry/instrumentation-winston');

const {getEnvStringList, getEnvBoolean} = require('./environment');
const {log} = require('./logging');
const {getEnvVar} = require('./environment');

// Instrumentations attach their Hook (for require-in-the-middle or import-in-the-middle)
// when the `enable` method is called and this happens inside their constructor
Expand Down Expand Up @@ -179,9 +179,9 @@ for (const name of Object.keys(instrumentationsMap)) {
* @returns {Array<string> | undefined}
*/
function getInstrumentationsFromEnv(envvar) {
if (process.env[envvar]) {
const names = getEnvStringList(envvar);
if (names) {
const instrumentations = [];
const names = process.env[envvar].split(',').map((s) => s.trim());

for (const name of names) {
if (otelInstrShortNames.has(name)) {
Expand Down Expand Up @@ -267,7 +267,10 @@ function getInstrumentations(opts = {}) {
}

// Skip if metrics are disabled by env var
const isMetricsDisabled = getEnvVar('ELASTIC_OTEL_METRICS_DISABLED');
const isMetricsDisabled = getEnvBoolean(
'ELASTIC_OTEL_METRICS_DISABLED',
false
);
if (
isMetricsDisabled &&
name === '@opentelemetry/instrumentation-runtime-node'
Expand Down
11 changes: 5 additions & 6 deletions packages/opentelemetry-node/lib/metrics/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
*/

const {metrics} = require('@opentelemetry/api');
const {metrics: metricsSdk} = require('@opentelemetry/sdk-node');
const {Aggregation, View} = metricsSdk;
const {AggregationType} = require('@opentelemetry/sdk-metrics');
const {HostMetrics} = require('@opentelemetry/host-metrics');

/** @type {HostMetrics} */
Expand All @@ -22,12 +21,12 @@ function enableHostMetrics() {
// - sends a lot of data. Ref: https://github.com/elastic/elastic-otel-node/issues/51
// - not displayed by Kibana in metrics dashboard. Ref: https://github.com/elastic/kibana/pull/199353
// - recommendation is to use OTEL collector to get and export them
/** @type {metricsSdk.View[]} */
/** @type {import('@opentelemetry/sdk-metrics').ViewOptions[]} */
const HOST_METRICS_VIEWS = [
new View({
{
instrumentName: 'system.*',
aggregation: Aggregation.Drop(),
}),
aggregation: {type: AggregationType.DROP},
},
];

module.exports = {
Expand Down
Loading
Loading