-
Notifications
You must be signed in to change notification settings - Fork 12
feat: add span metrics processor #849
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
base: main
Are you sure you want to change the base?
Changes from all commits
236aa41
8417ad5
75cea92
e584385
838a8b0
e64765d
28573fa
76363ee
59cb9ed
942941c
9fbe684
d22da50
ebd0802
35bcb2b
a389f5c
e25c31a
d4b503c
e82db68
e3f7ca0
001a817
cebfcd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
// This replicates the sdk-node logic of getting processors from env, because | ||
// the created NodeSDK doesn't expose the created processors and EDOT Node.js | ||
// wants to customize the default. | ||
|
||
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'; | ||
|
||
// Jaeger 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, | ||
}; |
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 meter for a single scope which is the EDOT package | ||
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(); | ||
}, | ||
}; | ||
|
||
/** | ||
* Updates the configuration to add OTEL SDK metrics | ||
* ref: https://github.com/open-telemetry/semantic-conventions/blob/main/model/otel/metrics.yaml | ||
* @param {Partial<import('@opentelemetry/sdk-node').NodeSDKConfiguration>} cfg | ||
* @returns | ||
*/ | ||
function setupSdkMetrics(cfg) { | ||
cfg.spanProcessors.push(spanMetricsProcessor); | ||
} | ||
|
||
module.exports = { | ||
setupSdkMetrics, | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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", | ||
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be fine. However, I'd look into updating the other |
||
"@opentelemetry/semantic-conventions": "^1.30.0", | ||
"@opentelemetry/winston-transport": "^0.13.0", | ||
"import-in-the-middle": "^1.12.0", | ||
|
Uh oh!
There was an error while loading. Please reload this page.