Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(otel-node): add env var parser #442

Merged
merged 5 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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",
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps a caret dep, like the others? Also, FWIW I notice that the ".../resources" dep below is ^1.26.0. I don't think that matters. It depends on the package-lock.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was hesitating but I'm going to add it

"@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
Loading