Skip to content

Commit

Permalink
chore(otel-node): add env var parser (#442)
Browse files Browse the repository at this point in the history
  • Loading branch information
david-luna authored Nov 21, 2024
1 parent de21ab9 commit 436940a
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 193 deletions.
4 changes: 1 addition & 3 deletions packages/opentelemetry-node/lib/detectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@ function resolveDetectors(detectors) {
return detectors;
}

const detectorsFromEnv = getEnvVar('OTEL_NODE_RESOURCE_DETECTORS') || 'all';
let detectorKeys = detectorsFromEnv.split(',').map((s) => s.trim());

let detectorKeys = getEnvVar('OTEL_NODE_RESOURCE_DETECTORS');
if (detectorKeys.some((k) => k === 'all')) {
detectorKeys = Object.keys(defaultDetectors);
} else if (detectorKeys.some((k) => k === 'none')) {
Expand Down
25 changes: 11 additions & 14 deletions packages/opentelemetry-node/lib/elastic-node-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ const {BatchLogRecordProcessor} = require('@opentelemetry/sdk-logs');

const {log, registerOTelDiagLogger} = require('./logging');
const {resolveDetectors} = require('./detectors');
const {setupEnvironment, restoreEnvironment} = require('./environment');
const {
setupEnvironment,
restoreEnvironment,
getEnvVar,
} = require('./environment');
const {getInstrumentations} = require('./instrumentations');
const {enableHostMetrics, HOST_METRICS_VIEWS} = require('./metrics/host');
// @ts-ignore - compiler options do not allow lookp outside `lib` folder
Expand Down Expand Up @@ -63,8 +67,7 @@ class ElasticNodeSDK extends NodeSDK {
// Get logs exporter protocol based on environment.
const logsExportProtocol =
process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||
'http/protobuf';
getEnvVar('OTEL_EXPORTER_OTLP_PROTOCOL');
let logExporterType = exporterPkgNameFromEnvVar[logsExportProtocol];
if (!logExporterType) {
log.warn(
Expand All @@ -88,15 +91,12 @@ class ElasticNodeSDK extends NodeSDK {
// TODO what `temporalityPreference`?

// Disable metrics by config
const metricsDisabled =
process.env.ELASTIC_OTEL_METRICS_DISABLED === 'true';
const metricsDisabled = getEnvVar('ELASTIC_OTEL_METRICS_DISABLED');
if (!metricsDisabled) {
// Get metrics exporter protocol based on environment.

const metricsExportProtocol =
process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL ||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||
'http/protobuf';
getEnvVar('OTEL_EXPORTER_OTLP_PROTOCOL');
let metricExporterType =
exporterPkgNameFromEnvVar[metricsExportProtocol];
if (!metricExporterType) {
Expand All @@ -111,12 +111,9 @@ class ElasticNodeSDK extends NodeSDK {
const {OTLPMetricExporter} = require(
`@opentelemetry/exporter-metrics-otlp-${metricExporterType}`
);
// Note: Default values has been taken from the specs
// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#periodic-exporting-metricreader
const metricsInterval =
Number(process.env.OTEL_METRIC_EXPORT_INTERVAL) || 60000;
const metricsTimeout =
Number(process.env.OTEL_METRIC_EXPORT_TIMEOUT) || 30000;

const metricsInterval = getEnvVar('OTEL_METRIC_EXPORT_INTERVAL');
const metricsTimeout = getEnvVar('OTEL_METRIC_EXPORT_TIMEOUT');
defaultConfig.metricReader =
new metrics.PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
Expand Down
163 changes: 93 additions & 70 deletions packages/opentelemetry-node/lib/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,54 +17,53 @@
* under the License.
*/

// @ts-ignore - compiler options do not allow lookp outside `lib` folder
const ELASTIC_SDK_VERSION = require('../package.json').version;
const OTEL_SDK_VERSION =
require('@opentelemetry/sdk-node/package.json').version;
const USER_AGENT_PREFIX = `elastic-otel-node/${ELASTIC_SDK_VERSION}`;
const USER_AGENT_HEADER = `${USER_AGENT_PREFIX} OTel-OTLP-Exporter-JavaScript/${OTEL_SDK_VERSION}`;
// NOTE: this API may be removed in future
// ref: https://github.com/open-telemetry/opentelemetry-js/issues/5172
const {getEnv} = require('@opentelemetry/core');

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

/**
* Reads a string in the format `key-1=value,key2=value2` and parses
* it into an object. This is the format specified for key value pairs
* for OTEL environment vars. Example:
* https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_headers
*
* If the param is not defined or falsy it returns an empty object
*
* Returns an array of strings from the given input. If undefined returns the fallback
* value.
* @param {string | undefined} str
* @returns {Record<string, string>}
* @param {string[]} [fallback=[]]
* @returns {string[]}
*/
function parseKeyValuePairs(str) {
function parseStringList(str, fallback = []) {
if (!str) {
return {};
return fallback;
}
return str.split(',').map((s) => s.trim());
}

const pairs = str.split(',');

return pairs.reduce((record, text) => {
const sepIndex = text.indexOf('=');
const key = text.substring(0, sepIndex);
const val = text.substring(sepIndex + 1);

record[key] = val;
return record;
}, {});
/**
* 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';
}

/**
* Serializes an object to a string in the format `key-1=value,key2=value2`
*
* @param {Record<string, string>} pairs
* @returns {string}
* Returns a boolean from te given input
* @param {string | undefined} str
* @param {number} fallback
* @returns {number}
*/
function serializeKeyValuePairs(pairs) {
return Object.entries(pairs)
.map(([key, val]) => `${key}=${val}`)
.join(',');
function parseNumber(str, fallback) {
if (!str) {
return fallback;
}

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

/**
Expand All @@ -79,37 +78,6 @@ function setupEnvironment() {
process.env.OTEL_TRACES_EXPORTER = 'otlp';
}

// Work with exporter headers:
// - Add our `user-agent` header in headers for traces, matrics & logs
// - comply with OTEL_EXPORTER_OTLP_HEADERS spec until the issue is fixed
// TODO: should we stash and restore? if so the restoration should be done
// after start
const userAgentHeader = {'User-Agent': USER_AGENT_HEADER};
// TODO: for now we omit our user agent if already defined elsewhere
const tracesHeaders = Object.assign(
{},
userAgentHeader,
parseKeyValuePairs(process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS)
);
process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS =
serializeKeyValuePairs(tracesHeaders);

const metricsHeaders = Object.assign(
{},
userAgentHeader,
parseKeyValuePairs(process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS)
);
process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS =
serializeKeyValuePairs(metricsHeaders);

const logsHeaders = Object.assign(
{},
userAgentHeader,
parseKeyValuePairs(process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS)
);
process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS =
serializeKeyValuePairs(logsHeaders);

if ('OTEL_LOG_LEVEL' in process.env) {
envToRestore['OTEL_LOG_LEVEL'] = process.env.OTEL_LOG_LEVEL;
// Make sure NodeSDK doesn't see this envvar and overwrite our diag
Expand All @@ -127,7 +95,7 @@ function setupEnvironment() {
}

/**
* Restores any value stashed in the stup process
* Restores any value stashed in the setup process
*/
function restoreEnvironment() {
Object.keys(envToRestore).forEach((k) => {
Expand All @@ -136,12 +104,67 @@ function restoreEnvironment() {
}

/**
* Gets the env var value also checking in the vars pending to be restored
* @param {string} name
* @returns {string | undefined}
* @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>}
*/
function getEnvVar(name) {
return process.env[name] || envToRestore[name];
if (name in otelEnv) {
// @ts-ignore -- T is {keyof OtelEnv} but not sure how to make TS infer that
return otelEnv[name];
}

// @ts-ignore -- T is {keyof EdotEnv} but not sure how to make TS infer that
return edotEnv[name];
}

module.exports = {
Expand Down
4 changes: 2 additions & 2 deletions packages/opentelemetry-node/lib/instrumentations.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const {UndiciInstrumentation} = require('@opentelemetry/instrumentation-undici')
const {WinstonInstrumentation} = require('@opentelemetry/instrumentation-winston');

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 @@ -236,8 +237,7 @@ function getInstrumentations(opts = {}) {
}

// Skip if metrics are disabled by env var
const isMetricsDisabled =
process.env.ELASTIC_OTEL_METRICS_DISABLED === 'true';
const isMetricsDisabled = getEnvVar('ELASTIC_OTEL_METRICS_DISABLED');
if (
isMetricsDisabled &&
name === '@opentelemetry/instrumentation-runtime-node'
Expand Down
1 change: 1 addition & 0 deletions packages/opentelemetry-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
},
"types": "types/index.d.ts",
"dependencies": {
"@opentelemetry/core": "1.27.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.54.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.54.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.54.0",
Expand Down
Loading

0 comments on commit 436940a

Please sign in to comment.