Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
134 changes: 134 additions & 0 deletions packages/opentelemetry-node/lib/processors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and contributors
* SPDX-License-Identifier: Apache-2.0
*/

// This replicates the SDKs logic of getting processors from env

const {getStringListFromEnv, getStringFromEnv} = require('@opentelemetry/core');
const {
BatchSpanProcessor,
SimpleSpanProcessor,
ConsoleSpanExporter,
} = require('@opentelemetry/sdk-trace-base');
const {log} = require('./logging');

/**
* @typedef {import('@opentelemetry/sdk-trace-base').SpanProcessor} SpanProcessor
* @typedef {import('@opentelemetry/sdk-trace-base').SpanExporter} SpanExporter
*/

const otlpPkgPrefix = '@opentelemetry/exporter-trace-otlp-';
const otlpProtocol =
getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ??
getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') ??
'http/protobuf';

// Jeager exporter is deprecated but upstream stills support it (for now)
// ref: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/CHANGELOG.md#0440
function getJaegerExporter() {
// The JaegerExporter does not support being required in bundled
// environments. By delaying the require statement to here, we only crash when
// the exporter is actually used in such an environment.
try {
// @ts-ignore
const {JaegerExporter} = require('@opentelemetry/exporter-jaeger');
return new JaegerExporter();
} catch (e) {
throw new Error(
`Could not instantiate JaegerExporter. This could be due to the JaegerExporter's lack of support for bundling. If possible, use @opentelemetry/exporter-trace-otlp-proto instead. Original Error: ${e}`
);
}
}

/**
* @param {'otlp' | 'zipkin' | 'jaeger' | 'console'} type
* @returns {SpanExporter}
*/
function getSpanExporter(type) {
if (type === 'zipkin') {
const {ZipkinExporter} = require('@opentelemetry/exporter-zipkin');
return new ZipkinExporter();
} else if (type === 'jaeger') {
return getJaegerExporter();
} else if (type === 'console') {
return new ConsoleSpanExporter();
}

let exporterPkgName = `${otlpPkgPrefix}`;
switch (otlpProtocol) {
case 'grpc':
exporterPkgName += 'grpc';
break;
case 'http/json':
exporterPkgName += 'http';
break;
case 'http/protobuf':
exporterPkgName += 'proto';
break;
default:
log.warn(
`Unsupported OTLP traces protocol: ${otlpProtocol}. Using http/protobuf.`
);
exporterPkgName += 'proto';
}
const {OTLPTraceExporter} = require(exporterPkgName);
return new OTLPTraceExporter();
}

/**
* @returns {SpanProcessor[]}
*/
function getSpanProcessors() {
// Get from env
const exporters = getStringListFromEnv('OTEL_TRACES_EXPORTER') ?? [];
const result = [];

if (exporters[0] === 'none') {
log.warn(
'OTEL_TRACES_EXPORTER contains "none". No trace information or Spans will be exported.'
);
return [];
}

if (exporters.length === 0) {
log.trace(
'OTEL_TRACES_EXPORTER is empty. Using the default "otlp" exporter.'
);
exporters.push('otlp');
} else if (exporters.length > 1 && exporters.includes('none')) {
log.warn(
'OTEL_TRACES_EXPORTER contains "none" along with other exporters. Using default otlp exporter.'
);
exporters.length = 0;
exporters.push('otlp');
}

for (const name of exporters) {
log.trace(`Initializing "${name}" traces exporter.`);
switch (name) {
case 'otlp':
result.push(new BatchSpanProcessor(getSpanExporter('otlp')));
break;
case 'console':
result.push(
new SimpleSpanProcessor(getSpanExporter('console'))
);
break;
case 'zipkin':
result.push(new BatchSpanProcessor(getSpanExporter('zipkin')));
break;
case 'jaeger':
result.push(new BatchSpanProcessor(getJaegerExporter()));
break;
default:
log.warn(`Unrecognized OTEL_TRACES_EXPORTER value: ${name}.`);
}
}

return result;
}

module.exports = {
getSpanProcessors,
};
130 changes: 130 additions & 0 deletions packages/opentelemetry-node/lib/sdk-metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright Elasticsearch B.V. and contributors
* SPDX-License-Identifier: Apache-2.0
*/

// Gets the SDK configuration and updates it to have instruments
// to collect metrics related to the SDK, for now:
// - otel.sdk.span.live
// - otel.sdk.span.ended

const {metrics, TraceFlags} = require('@opentelemetry/api');
const {
METRIC_OTEL_SDK_SPAN_ENDED,
METRIC_OTEL_SDK_SPAN_LIVE,
ATTR_OTEL_SPAN_SAMPLING_RESULT,
OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE,
OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY,
} = require('./semconv');

/**
* @typedef {import('@opentelemetry/api').Meter} Meter
* @typedef {import('@opentelemetry/api').UpDownCounter} UpDownCounter
* @typedef {import('@opentelemetry/api').Counter} Counter
* @typedef {import('@opentelemetry/sdk-trace-base').Span} Span
* @typedef {import('@opentelemetry/sdk-trace-base').ReadableSpan} ReadableSpan
* @typedef {import('@opentelemetry/sdk-trace-base').SpanProcessor} SpanProcessor
*/

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

// NOTE: assuming the meter provider is not going to be replaced once
// the EDOT is started we can cache the meter and metrics in these vars
/** @type {Meter} */
let selfMetricsMeter;
/** @type {UpDownCounter} */
let liveSpans;
/** @type {Counter} */
let closedSpans;

/**
* @returns {Meter}
*/
function getSpansMeter() {
if (selfMetricsMeter) {
return selfMetricsMeter;
}
// NOTE: we have a metter for a single scope which is the EDOT package
// TODO: check WWJD (what would Java do?)
selfMetricsMeter = metrics.getMeter(ELASTIC_SDK_SCOPE, ELASTIC_SDK_VERSION);
return selfMetricsMeter;
}

/**
* @returns {UpDownCounter}
*/
function getLiveSpansCounter() {
if (liveSpans) {
return liveSpans;
}
liveSpans = getSpansMeter().createUpDownCounter(METRIC_OTEL_SDK_SPAN_LIVE, {
description:
'Number of created spans for which the end operation has not been called yet',
});
return liveSpans;
}

/**
* @returns {Counter}
*/
function getEndedSpansCounter() {
if (closedSpans) {
return closedSpans;
}
closedSpans = getSpansMeter().createCounter(METRIC_OTEL_SDK_SPAN_ENDED, {
description:
'Number of created spans for which the end operation was called',
});
return closedSpans;
}

/**
* All Spans treated in SpanProcessors are recording so here none will have
* a "DROP" sampling result
* @param {Span | ReadableSpan} span
* @returns {string}
*/
function getSamplingResult(span) {
const isSampled = span.spanContext().traceFlags & TraceFlags.SAMPLED;
return isSampled
? OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE
: OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY;
}

/** @type {SpanProcessor} */
const spanMetricsProcessor = {
forceFlush: function () {
return Promise.resolve();
},
onStart: function (span) {
getLiveSpansCounter().add(1, {
[ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span),
});
},
onEnd: function (span) {
const attrs = {
[ATTR_OTEL_SPAN_SAMPLING_RESULT]: getSamplingResult(span),
};
getLiveSpansCounter().add(-1, attrs);
getEndedSpansCounter().add(1, attrs);
},
shutdown: function () {
return Promise.resolve();
},
};

/**
*
* @param {Partial<import('@opentelemetry/sdk-node').NodeSDKConfiguration>} cfg
* @returns
*/
function setupSdkMetrics(cfg) {
cfg.spanProcessors.push(spanMetricsProcessor);
}

module.exports = {
setupSdkMetrics,
};
10 changes: 8 additions & 2 deletions packages/opentelemetry-node/lib/sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
const {resolveDetectors} = require('./detectors');
const {setupEnvironment, restoreEnvironment} = require('./environment');
const {getInstrumentations} = require('./instrumentations');
const {getSpanProcessors} = require('./processors');
const {setupCentralConfig} = require('./central-config');
const {setupSdkMetrics} = require('./sdk-metrics');
const DISTRO_VERSION = require('../package.json').version;

/**
Expand All @@ -55,7 +57,7 @@
try {
await shutdownFn();
} catch (err) {
console.warn('warning: error shutting down OTel SDK', err);

Check warning on line 60 in packages/opentelemetry-node/lib/sdk.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
}
process.exit(128 + os.constants.signals.SIGTERM);
});
Expand All @@ -65,7 +67,7 @@
try {
await shutdownFn();
} catch (err) {
console.warn('warning: error shutting down OTel SDK', err);

Check warning on line 70 in packages/opentelemetry-node/lib/sdk.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
}
});
}
Expand Down Expand Up @@ -124,8 +126,7 @@
const defaultConfig = {
resourceDetectors: resolveDetectors(cfg.resourceDetectors),
instrumentations: cfg.instrumentations || getInstrumentations(),
// Avoid setting `spanProcessor` or `traceExporter` to have NodeSDK
// use its `TracerProviderWithEnvExporters` for tracing setup.
spanProcessors: cfg.spanProcessors || getSpanProcessors(),
};

const exporterPkgNameFromEnvVar = {
Expand Down Expand Up @@ -199,6 +200,11 @@
const config = Object.assign(defaultConfig, cfg);

setupEnvironment();
if (metricsEnabled) {
// Configure OTEL SDK metrics
// ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml
setupSdkMetrics(config);
}
const sdk = new NodeSDK(config);

// TODO perhaps include some choice resource attrs in this log (sync ones): service.name, deployment.environment.name
Expand Down
43 changes: 42 additions & 1 deletion packages/opentelemetry-node/lib/semconv.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*/
const ATTR_DEPLOYMENT_ENVIRONMENT_NAME = 'deployment.environment.name';

/**
* The name of the deployment.
*
Expand All @@ -37,7 +36,49 @@ const ATTR_DEPLOYMENT_ENVIRONMENT_NAME = 'deployment.environment.name';
*/
const ATTR_DEPLOYMENT_NAME = 'deployment.name';

/**
* The number of created spans for which the end operation was called
*
* @note For spans with `recording=true`: Implementations **MUST** record both `otel.sdk.span.live` and `otel.sdk.span.ended`.
* For spans with `recording=false`: If implementations decide to record this metric, they **MUST** also record `otel.sdk.span.live`.
*
* @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*/
const METRIC_OTEL_SDK_SPAN_ENDED = 'otel.sdk.span.ended';

/**
* The number of created spans for which the end operation has not been called yet
*
* @note For spans with `recording=true`: Implementations **MUST** record both `otel.sdk.span.live` and `otel.sdk.span.ended`.
* For spans with `recording=false`: If implementations decide to record this metric, they **MUST** also record `otel.sdk.span.ended`.
*
* @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*/
const METRIC_OTEL_SDK_SPAN_LIVE = 'otel.sdk.span.live';

/**
* The result value of the sampler for this span
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*/
const ATTR_OTEL_SPAN_SAMPLING_RESULT = 'otel.span.sampling_result';

/**
* Enum value "RECORD_AND_SAMPLE" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}.
*/
const OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE = 'RECORD_AND_SAMPLE';

/**
* Enum value "RECORD_ONLY" for attribute {@link ATTR_OTEL_SPAN_SAMPLING_RESULT}.
*/
const OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY = 'RECORD_ONLY';

module.exports = {
ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
ATTR_DEPLOYMENT_NAME,
METRIC_OTEL_SDK_SPAN_ENDED,
METRIC_OTEL_SDK_SPAN_LIVE,
ATTR_OTEL_SPAN_SAMPLING_RESULT,
OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_AND_SAMPLE,
OTEL_SPAN_SAMPLING_RESULT_VALUE_RECORD_ONLY,
};
2 changes: 2 additions & 0 deletions packages/opentelemetry-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/opentelemetry-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"lint:eslint": "eslint --ext=js,mjs,cjs . # requires node >=16.0.0",
"lint:types": "rm -rf build/lint-types && tsc --outDir build/lint-types && diff -ur types build/lint-types",
"lint:fix": "eslint --ext=js,mjs,cjs --fix . # requires node >=16.0.0",
"lint:deps": "dependency-check require.js import.mjs 'lib/**/*.js' 'test/**/*.js' '!test/fixtures/a-ts-proj' '!test/fixtures/an-esm-pkg' -e mjs:../../scripts/parse-mjs-source -i @types/tape -i dotenv -i @opentelemetry/winston-transport -i @opentelemetry/exporter-logs-* -i @opentelemetry/exporter-metrics-*",
"lint:deps": "dependency-check require.js import.mjs 'lib/**/*.js' 'test/**/*.js' '!test/fixtures/a-ts-proj' '!test/fixtures/an-esm-pkg' -e mjs:../../scripts/parse-mjs-source -i @types/tape -i dotenv -i @opentelemetry/winston-transport -i @opentelemetry/exporter-logs-* -i @opentelemetry/exporter-metrics-* -i @opentelemetry/exporter-jaeger",
"lint:license-files": "../../scripts/gen-notice.sh --lint . # requires node >=16",
"lint:changelog": "../../scripts/extract-release-notes.js .",
"test": "NODE_OPTIONS='-r dotenv/config' DOTENV_CONFIG_PATH=./test/test-services.env tape test/**/*.test.js",
Expand Down Expand Up @@ -81,6 +81,7 @@
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.202.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.202.0",
"@opentelemetry/exporter-zipkin": "^2.0.1",
"@opentelemetry/host-metrics": "^0.36.0",
"@opentelemetry/instrumentation-amqplib": "^0.49.0",
"@opentelemetry/instrumentation-aws-sdk": "^0.54.0",
Expand Down Expand Up @@ -129,6 +130,7 @@
"@opentelemetry/sdk-logs": "^0.202.0",
"@opentelemetry/sdk-metrics": "^2.0.0",
"@opentelemetry/sdk-node": "^0.202.0",
"@opentelemetry/sdk-trace-base": "^2.0.1",
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 to reviewer: added to make the linter happy but this is a dependency of @opentelemetry/sdk-node which is going to be installed regardless if here or not. I worry if this may lead to some side effects

Copy link
Member

Choose a reason for hiding this comment

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

Should be fine. However, I'd look into updating the other ^2.0.0 otel deps to ^2.0.1 as well. E.g.: sdk-metrics.

"@opentelemetry/semantic-conventions": "^1.30.0",
"@opentelemetry/winston-transport": "^0.13.0",
"import-in-the-middle": "^1.12.0",
Expand Down
Loading