From 19b08e769932bbeec107140ad10c632818a1e521 Mon Sep 17 00:00:00 2001 From: D050513 Date: Tue, 12 Aug 2025 15:38:36 +0200 Subject: [PATCH 01/20] bump otel deps to ^2.0 --- package.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 478c1505..54d3dee2 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,14 @@ }, "dependencies": { "@opentelemetry/api": "^1.9", - "@opentelemetry/core": "^1.27", - "@opentelemetry/instrumentation": "^0.57", - "@opentelemetry/instrumentation-http": "^0.57", - "@opentelemetry/resources": "^1.27", - "@opentelemetry/sdk-metrics": "^1.27", - "@opentelemetry/sdk-trace-base": "^1.27", - "@opentelemetry/sdk-trace-node": "^1.27", - "@opentelemetry/semantic-conventions": "^1.34" + "@opentelemetry/core": "^2", + "@opentelemetry/instrumentation": "^0.200", + "@opentelemetry/instrumentation-http": "^0.200", + "@opentelemetry/resources": "^2", + "@opentelemetry/sdk-metrics": "^2", + "@opentelemetry/sdk-trace-base": "^2", + "@opentelemetry/sdk-trace-node": "^2", + "@opentelemetry/semantic-conventions": "^1.36" }, "peerDependencies": { "@sap/cds": ">=8" @@ -38,12 +38,12 @@ "@cap-js/telemetry": "file:.", "@dynatrace/oneagent-sdk": "^1.5.0", "@grpc/grpc-js": "^1.9.14", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.0", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.57.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.57.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.200", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.200", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.200", + "@opentelemetry/exporter-trace-otlp-proto": "^0.200", "@opentelemetry/host-metrics": "^0.36.0", - "@opentelemetry/instrumentation-runtime-node": "^0.16.0", + "@opentelemetry/instrumentation-runtime-node": "^0.17.0", "@sap/cds-mtxs": ">=2", "axios": "^1.6.7", "chai": "^4.4.1", From f6bd730042f042bdfbf3a3d7e172e06d383c15fd Mon Sep 17 00:00:00 2001 From: D050513 Date: Tue, 12 Aug 2025 15:47:32 +0200 Subject: [PATCH 02/20] rm warning --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index c2fb440c..37362166 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,6 @@ -> [!WARNING] -> [OpenTelemetry SDK 2.0](https://github.com/open-telemetry/opentelemetry-js/releases/tag/v2.0.0) is not yet supported. - - - ## About This Project `@cap-js/telemetry` is a CDS plugin providing observability features, including [automatic OpenTelemetry instrumentation](https://opentelemetry.io/docs/concepts/instrumentation/automatic). From a7023efc45fb9ff87d4178d1d0093fdb1060cc53 Mon Sep 17 00:00:00 2001 From: D050513 Date: Tue, 12 Aug 2025 15:49:20 +0200 Subject: [PATCH 03/20] disable version check --- cds-plugin.js | 68 +++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/cds-plugin.js b/cds-plugin.js index a3b091f2..e7a9a7e4 100644 --- a/cds-plugin.js +++ b/cds-plugin.js @@ -8,41 +8,41 @@ if (!!process.env.NO_TELEMETRY && process.env.NO_TELEMETRY !== 'false') return - const _version_of = module => { - let pkg - try { - pkg = require(`${module}/package.json`) - } catch { - try { - const path = require.resolve(module).split(module)[0] + module + '/package.json' - pkg = JSON.parse(require('fs').readFileSync(path, 'utf-8')) - } catch { - // ignore - } - } - if (!pkg) { - cds.log('telemetry').warn(`Unable to determine version of ${module}`) - return - } - return pkg.version - } + // const _version_of = module => { + // let pkg + // try { + // pkg = require(`${module}/package.json`) + // } catch { + // try { + // const path = require.resolve(module).split(module)[0] + module + '/package.json' + // pkg = JSON.parse(require('fs').readFileSync(path, 'utf-8')) + // } catch { + // // ignore + // } + // } + // if (!pkg) { + // cds.log('telemetry').warn(`Unable to determine version of ${module}`) + // return + // } + // return pkg.version + // } - // check versions of @opentelemetry dependencies - const { dependencies } = require(require('path').join(cds.root, 'package')) - let violations = [] - for (const each in dependencies) { - if (!each.match(/^@opentelemetry\//)) continue - const version = _version_of(each) - if (!version) continue - const [major, minor] = version.split('.') - if (major >= 2 || minor >= 200) violations.push(`${each}@${version}`) - } - if (violations.length) { - const msg = - '@cap-js/telemetry does not yet support OpenTelemetry SDK 2.0 (^2 and ^0.200):' + - `\n - ${violations.join('\n - ')}\n` - throw new Error(msg) - } + // // check versions of @opentelemetry dependencies + // const { dependencies } = require(require('path').join(cds.root, 'package')) + // let violations = [] + // for (const each in dependencies) { + // if (!each.match(/^@opentelemetry\//)) continue + // const version = _version_of(each) + // if (!version) continue + // const [major, minor] = version.split('.') + // if (major >= 2 || minor >= 200) violations.push(`${each}@${version}`) + // } + // if (violations.length) { + // const msg = + // '@cap-js/telemetry does not yet support OpenTelemetry SDK 2.0 (^2 and ^0.200):' + + // `\n - ${violations.join('\n - ')}\n` + // throw new Error(msg) + // } require('./lib')() })() From b3aaf0179eb9ed607fd03c17df31786eb5c06dc8 Mon Sep 17 00:00:00 2001 From: D050513 Date: Tue, 12 Aug 2025 15:52:39 +0200 Subject: [PATCH 04/20] new Resource() -> resourceFromAttributes() --- lib/metrics/index.js | 4 ++-- lib/tracing/index.js | 4 ++-- lib/utils.js | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/metrics/index.js b/lib/metrics/index.js index 28c001f8..3dc8e5e1 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -3,7 +3,7 @@ const LOG = cds.log('telemetry') const { metrics } = require('@opentelemetry/api') const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core') -const { Resource } = require('@opentelemetry/resources') +const { resourceFromAttributes } = require('@opentelemetry/resources') const { AggregationTemporality, DropAggregation, @@ -88,7 +88,7 @@ module.exports = resource => { let meterProvider = metrics.getMeterProvider() if (meterProvider.constructor.name === 'NoopMeterProvider') { const dtmetadata = getDynatraceMetadata() - resource = new Resource({}).merge(resource).merge(dtmetadata) + resource = resourceFromAttributes({}).merge(resource).merge(dtmetadata) // unfortunately, we have to pass views to the MeterProvider constructor // something like meterProvider.addView() would be a lot nicer for locality let views = [] diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 7d735ddf..43a616ff 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -3,7 +3,7 @@ const LOG = cds.log('telemetry') const { trace } = require('@opentelemetry/api') const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core') -const { Resource } = require('@opentelemetry/resources') +const { resourceFromAttributes } = require('@opentelemetry/resources') const { BatchSpanProcessor, SimpleSpanProcessor, SamplingDecision } = require('@opentelemetry/sdk-trace-base') const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node') @@ -140,7 +140,7 @@ module.exports = resource => { let tracerProvider = trace.getTracerProvider() if (!tracerProvider.getDelegateTracer()) { const dtmetadata = getDynatraceMetadata() - resource = new Resource({}).merge(resource).merge(dtmetadata) + resource = resourceFromAttributes({}).merge(resource).merge(dtmetadata) tracerProvider = new NodeTracerProvider({ resource, sampler: _getSampler() }) tracerProvider.register({ propagator: _getPropagator() }) } else { diff --git a/lib/utils.js b/lib/utils.js index 0c6f983f..e010364a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -5,7 +5,7 @@ const fs = require('fs') const { DiagLogLevel } = require('@opentelemetry/api') const { hrTimeToMilliseconds } = require('@opentelemetry/core') -const { Resource } = require('@opentelemetry/resources') +const { resourceFromAttributes } = require('@opentelemetry/resources') const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, @@ -64,14 +64,14 @@ function getResource() { attributes['sap.cf.process.type'] = VCAP_APPLICATION.process_type } - return new Resource(attributes) + return resourceFromAttributes(attributes) } let dtmetadata function getDynatraceMetadata() { if (dtmetadata) return dtmetadata - dtmetadata = new Resource({}) + dtmetadata = resourceFromAttributes({}) for (let name of [ 'dt_metadata_e617c525669e072eebe3d0f08212e8f2.json', '/var/lib/dynatrace/enrichment/dt_metadata.json' @@ -82,7 +82,7 @@ function getDynatraceMetadata() { .readFileSync(name.startsWith('/var') ? name : fs.readFileSync(name).toString('utf-8').trim()) .toString('utf-8') LOG._debug && LOG.debug('Successful') - dtmetadata = dtmetadata.merge(new Resource(JSON.parse(content))) + dtmetadata = dtmetadata.merge(resourceFromAttributes(JSON.parse(content))) break } catch (err) { LOG._debug && LOG.debug('Failed with error:', err) From e2aeae6d210512de0d0a814f4af65729f01e195b Mon Sep 17 00:00:00 2001 From: D050513 Date: Tue, 12 Aug 2025 22:12:23 +0200 Subject: [PATCH 05/20] tracing --- lib/tracing/index.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 43a616ff..649743a2 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -137,16 +137,7 @@ module.exports = resource => { /* * general setup */ - let tracerProvider = trace.getTracerProvider() - if (!tracerProvider.getDelegateTracer()) { - const dtmetadata = getDynatraceMetadata() - resource = resourceFromAttributes({}).merge(resource).merge(dtmetadata) - tracerProvider = new NodeTracerProvider({ resource, sampler: _getSampler() }) - tracerProvider.register({ propagator: _getPropagator() }) - } else { - LOG._warn && LOG.warn('TracerProvider already initialized by a different module. It will be used as is.') - tracerProvider = tracerProvider.getDelegate() - } + let processor const via_one_agent = process.env.DT_NODE_PRELOAD_OPTIONS && cds.env.requires.telemetry.kind.match(/to-dynatrace$/) && @@ -157,11 +148,22 @@ module.exports = resource => { } else { const exporter = _getExporter() const processorConfig = cds.env.requires.telemetry.tracing.processor?.config || {} - const processor = + processor = process.env.NODE_ENV === 'production' ? new BatchSpanProcessor(exporter, processorConfig) : new SimpleSpanProcessor(exporter, processorConfig) - tracerProvider.addSpanProcessor(processor) + } + + let tracerProvider = trace.getTracerProvider() + if (!tracerProvider.getDelegateTracer()) { + const dtmetadata = getDynatraceMetadata() + resource = resourceFromAttributes({}).merge(resource).merge(dtmetadata) + tracerProvider = new NodeTracerProvider({ resource, spanProcessors: [processor], sampler: _getSampler() }) + tracerProvider.register({ propagator: _getPropagator() }) + } else { + // TODO: CALM + LOG._warn && LOG.warn('TracerProvider already initialized by a different module. It will be used as is.') + tracerProvider = tracerProvider.getDelegate() } // clear sap passport for new tx From 8a7df1a78a609bb03a8c4275ce56a611a9c66289 Mon Sep 17 00:00:00 2001 From: D050513 Date: Tue, 12 Aug 2025 22:12:30 +0200 Subject: [PATCH 06/20] metrics --- lib/metrics/index.js | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/lib/metrics/index.js b/lib/metrics/index.js index 3dc8e5e1..9cff643d 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -6,10 +6,9 @@ const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core') const { resourceFromAttributes } = require('@opentelemetry/resources') const { AggregationTemporality, - DropAggregation, + AggregationType, MeterProvider, - PeriodicExportingMetricReader, - View + PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics') const { getDynatraceMetadata, getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds, _require } = require('../utils') @@ -85,6 +84,11 @@ module.exports = resource => { /* * general setup */ + const metricsConfig = cds.env.requires.telemetry.metrics.config + const exporter = _getExporter() + // TODO: pull + const metricReader = new PeriodicExportingMetricReader({ ...metricsConfig, exporter }) + let meterProvider = metrics.getMeterProvider() if (meterProvider.constructor.name === 'NoopMeterProvider') { const dtmetadata = getDynatraceMetadata() @@ -95,30 +99,19 @@ module.exports = resource => { if (process.env.HOST_METRICS_RETAIN_SYSTEM) { // nothing to do } else { - views.push( - new View({ - meterName: '@cap-js/telemetry:host-metrics', - instrumentName: 'system.*', - aggregation: new DropAggregation() - }) - ) + views.push({ + meterName: '@cap-js/telemetry:host-metrics', + instrumentName: 'system.*', + type: AggregationType.DROP + }) } - meterProvider = new MeterProvider({ resource, views }) + meterProvider = new MeterProvider({ resource, readers: [metricReader], views }) metrics.setGlobalMeterProvider(meterProvider) } else { + // TODO: CALM LOG._warn && LOG.warn('MeterProvider already initialized by a different module. It will be used as is.') } - const metricsConfig = cds.env.requires.telemetry.metrics.config - const exporter = _getExporter() - // push vs. pull - if (typeof exporter.export === 'function') { - const metricReader = new PeriodicExportingMetricReader({ ...metricsConfig, exporter }) - meterProvider.addMetricReader(metricReader) - } else { - meterProvider.addMetricReader(exporter) - } - /* * add individual metrics */ From 1a7089f79673f77667e6d3a95c3a870c63778ea3 Mon Sep 17 00:00:00 2001 From: D050513 Date: Wed, 13 Aug 2025 11:25:53 +0200 Subject: [PATCH 07/20] rm otel<2 check --- cds-plugin.js | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/cds-plugin.js b/cds-plugin.js index e7a9a7e4..1704e142 100644 --- a/cds-plugin.js +++ b/cds-plugin.js @@ -8,41 +8,5 @@ if (!!process.env.NO_TELEMETRY && process.env.NO_TELEMETRY !== 'false') return - // const _version_of = module => { - // let pkg - // try { - // pkg = require(`${module}/package.json`) - // } catch { - // try { - // const path = require.resolve(module).split(module)[0] + module + '/package.json' - // pkg = JSON.parse(require('fs').readFileSync(path, 'utf-8')) - // } catch { - // // ignore - // } - // } - // if (!pkg) { - // cds.log('telemetry').warn(`Unable to determine version of ${module}`) - // return - // } - // return pkg.version - // } - - // // check versions of @opentelemetry dependencies - // const { dependencies } = require(require('path').join(cds.root, 'package')) - // let violations = [] - // for (const each in dependencies) { - // if (!each.match(/^@opentelemetry\//)) continue - // const version = _version_of(each) - // if (!version) continue - // const [major, minor] = version.split('.') - // if (major >= 2 || minor >= 200) violations.push(`${each}@${version}`) - // } - // if (violations.length) { - // const msg = - // '@cap-js/telemetry does not yet support OpenTelemetry SDK 2.0 (^2 and ^0.200):' + - // `\n - ${violations.join('\n - ')}\n` - // throw new Error(msg) - // } - require('./lib')() })() From a2fa9117f1803b81eb4e4a843951d7bb21514c08 Mon Sep 17 00:00:00 2001 From: I548646 Date: Fri, 15 Aug 2025 10:57:13 +0200 Subject: [PATCH 08/20] feat: replace access to otel env variables --- lib/index.js | 3 ++- lib/logging/index.js | 10 ++++------ lib/metrics/index.js | 10 ++++------ lib/tracing/index.js | 10 ++++------ lib/utils.js | 8 ++++---- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1c573822..d4368f01 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,7 @@ const LOG = cds.log('telemetry') const path = require('path') const { diag } = require('@opentelemetry/api') +const { getStringFromEnv, diagLogLevelFromString } = require('@opentelemetry/core') const { registerInstrumentations } = require('@opentelemetry/instrumentation') const tracing = require('./tracing') @@ -60,7 +61,7 @@ function _getInstrumentations() { module.exports = function () { // set logger and propagate log level - diag.setLogger(cds.log('telemetry'), process.env.OTEL_LOG_LEVEL || getDiagLogLevel()) + diag.setLogger(cds.log('telemetry'), diagLogLevelFromString(getStringFromEnv('OTEL_LOG_LEVEL')) || getDiagLogLevel()) const resource = getResource() diff --git a/lib/logging/index.js b/lib/logging/index.js index 6d77d766..6701bdd2 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -1,7 +1,7 @@ const cds = require('@sap/cds') const LOG = cds.log('telemetry') -const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core') +const { getStringFromEnv } = require('@opentelemetry/core') const { getCredsForCLSAsUPS, augmentCLCreds, _require } = require('../utils') @@ -20,15 +20,13 @@ function _getExporter() { // for kind telemetry-to-otlp based on env vars if (loggingExporter === 'env') { - const cstm_env = getEnvWithoutDefaults() - const otlp_env = getEnv() - let protocol = cstm_env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? cstm_env.OTEL_EXPORTER_OTLP_PROTOCOL + let protocol = getStringFromEnv('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') // on kyma, the otlp endpoint speaks grpc, but otel's default protocol is http/protobuf -> fix default if (!protocol) { - const endpoint = otlp_env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? otlp_env.OTEL_EXPORTER_OTLP_ENDPOINT ?? '' + const endpoint = (getStringFromEnv('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_ENDPOINT') ?? '') if (endpoint.match(/:4317/)) protocol = 'grpc' } - protocol ??= otlp_env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? otlp_env.OTEL_EXPORTER_OTLP_PROTOCOL + protocol ??= (getStringFromEnv('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL')) loggingExporter = { module: _protocol2module[protocol], class: 'OTLPLogExporter' } } diff --git a/lib/metrics/index.js b/lib/metrics/index.js index 9cff643d..a2a9d92f 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -2,7 +2,7 @@ const cds = require('@sap/cds') const LOG = cds.log('telemetry') const { metrics } = require('@opentelemetry/api') -const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core') +const { getStringFromEnv } = require('@opentelemetry/core') const { resourceFromAttributes } = require('@opentelemetry/resources') const { AggregationTemporality, @@ -28,15 +28,13 @@ function _getExporter() { // for kind telemetry-to-otlp based on env vars if (metricsExporter === 'env') { - const cstm_env = getEnvWithoutDefaults() - const otlp_env = getEnv() - let protocol = cstm_env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL ?? cstm_env.OTEL_EXPORTER_OTLP_PROTOCOL + let protocol = getStringFromEnv('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') // on kyma, the otlp endpoint speaks grpc, but otel's default protocol is http/protobuf -> fix default if (!protocol) { - const endpoint = otlp_env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? otlp_env.OTEL_EXPORTER_OTLP_ENDPOINT ?? '' + const endpoint = (getStringFromEnv('OTEL_EXPORTER_OTLP_METRICS_ENDPOINT') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_ENDPOINT') ?? '') if (endpoint.match(/:4317/)) protocol = 'grpc' } - protocol ??= otlp_env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL ?? otlp_env.OTEL_EXPORTER_OTLP_PROTOCOL + protocol ??= (getStringFromEnv('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL')) metricsExporter = { module: _protocol2module[protocol], class: 'OTLPMetricExporter' } } diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 649743a2..d9e5fdaa 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -2,7 +2,7 @@ const cds = require('@sap/cds') const LOG = cds.log('telemetry') const { trace } = require('@opentelemetry/api') -const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core') +const { getStringFromEnv } = require('@opentelemetry/core') const { resourceFromAttributes } = require('@opentelemetry/resources') const { BatchSpanProcessor, SimpleSpanProcessor, SamplingDecision } = require('@opentelemetry/sdk-trace-base') const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node') @@ -85,15 +85,13 @@ function _getExporter() { // for kind telemetry-to-otlp based on env vars if (tracingExporter === 'env') { - const cstm_env = getEnvWithoutDefaults() - const otlp_env = getEnv() - let protocol = cstm_env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ?? cstm_env.OTEL_EXPORTER_OTLP_PROTOCOL + let protocol = getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') // on kyma, the otlp endpoint speaks grpc, but otel's default protocol is http/protobuf -> fix default if (!protocol) { - const endpoint = otlp_env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? otlp_env.OTEL_EXPORTER_OTLP_ENDPOINT ?? '' + const endpoint = (getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_ENDPOINT') ?? '') if (endpoint.match(/:4317/)) protocol = 'grpc' } - protocol ??= otlp_env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ?? otlp_env.OTEL_EXPORTER_OTLP_PROTOCOL + protocol ??= (getStringFromEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL')) tracingExporter = { module: _protocol2module[protocol], class: 'OTLPTraceExporter' } } diff --git a/lib/utils.js b/lib/utils.js index e010364a..cf9cbb80 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,7 @@ const LOG = cds.log('telemetry') const fs = require('fs') const { DiagLogLevel } = require('@opentelemetry/api') -const { hrTimeToMilliseconds } = require('@opentelemetry/core') +const { hrTimeToMilliseconds, getStringFromEnv } = require('@opentelemetry/core') const { resourceFromAttributes } = require('@opentelemetry/resources') const { ATTR_SERVICE_NAME, @@ -38,11 +38,11 @@ function getResource() { const attributes = {} // Service - attributes[ATTR_SERVICE_NAME] = process.env.OTEL_SERVICE_NAME || name - attributes[ATTR_SERVICE_VERSION] = process.env.OTEL_SERVICE_VERSION || version + attributes[ATTR_SERVICE_NAME] = getStringFromEnv('OTEL_SERVICE_NAME') || name + attributes[ATTR_SERVICE_VERSION] = getStringFromEnv('OTEL_SERVICE_VERSION') || version // Service (Experimental) - if (process.env.OTEL_SERVICE_NAMESPACE) attributes[ATTR_SERVICE_NAMESPACE] = process.env.OTEL_SERVICE_NAMESPACE + if (getStringFromEnv('OTEL_SERVICE_NAMESPACE')) attributes[ATTR_SERVICE_NAMESPACE] = getStringFromEnv('OTEL_SERVICE_NAMESPACE') if (VCAP_APPLICATION) attributes[ATTR_SERVICE_INSTANCE_ID] = VCAP_APPLICATION.instance_id if (process.env.CF_INSTANCE_GUID) { From fd40b73beb9127aa20c4e796deec766baa29d700 Mon Sep 17 00:00:00 2001 From: I548646 Date: Fri, 15 Aug 2025 11:04:49 +0200 Subject: [PATCH 09/20] feat: replace deprecated core imports --- lib/tracing/trace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tracing/trace.js b/lib/tracing/trace.js index fed83d59..0497af25 100644 --- a/lib/tracing/trace.js +++ b/lib/tracing/trace.js @@ -278,7 +278,7 @@ function trace(req, fn, that, args, opts = {}) { if (!root && parent?.isRecording() === false) return fn.apply(that, args) // augment root span with request attributes, overwrite start time, and adjust root name - if (parent?.instrumentationLibrary?.name === '@opentelemetry/instrumentation-http' && !parent[$adjusted]) { + if (parent?.instrumentationScope?.name === '@opentelemetry/instrumentation-http' && !parent[$adjusted]) { parent[$adjusted] = true _setAttributes(parent, _getRequestAttributes()) const ctx = cds.context From 1dd8369d982f4ff729f7585f9d3154ab88e2c8ff Mon Sep 17 00:00:00 2001 From: I548646 Date: Fri, 15 Aug 2025 11:14:12 +0200 Subject: [PATCH 10/20] feat: adopt tracing sdk api changes replacing parentSpanId --- lib/exporter/ConsoleSpanExporter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/exporter/ConsoleSpanExporter.js b/lib/exporter/ConsoleSpanExporter.js index 023ae9f5..1564be74 100644 --- a/lib/exporter/ConsoleSpanExporter.js +++ b/lib/exporter/ConsoleSpanExporter.js @@ -58,7 +58,7 @@ const _span_sorter = (a, b) => { const _list2tree = (span, spans, flat, indent) => { const spanId = span.spanContext().spanId - const children = spans.filter(s => s.parentSpanId === spanId) + const children = spans.filter(s => s.parentSpanContext?.spanId === spanId) if (children.length === 0) return children.sort(_span_sorter) for (const each of children) { @@ -100,13 +100,13 @@ class ConsoleSpanExporter /* implements SpanExporter */ { _sendSpans(spans, done) { for (const span of spans) { const w3c_parent_id = cds.context?.http?.req.headers.traceparent?.split('-')[2] - if (!span.parentSpanId || span.parentSpanId === w3c_parent_id) { + if (!span.parentSpanContext?.spanId || span.parentSpanContext?.spanId === w3c_parent_id) { let toLog = 'elapsed times:' toLog += _span2line(span) const children = this._temporaryStorage.get(span.spanContext().traceId) if (children) { const ids = new Set(children.map(s => s.spanContext().spanId).filter(s => !!s)) - const reqs = children.filter(s => s.spanContext().spanId && !ids.has(s.parentSpanId)) + const reqs = children.filter(s => s.spanContext().spanId && !ids.has(s.parentSpanContext?.spanId)) const flat = [] reqs.sort(_span_sorter) for (const each of reqs) { From 3c7d1c012fc387904c3f79819b3bd26a93fcbed8 Mon Sep 17 00:00:00 2001 From: I548646 Date: Fri, 15 Aug 2025 11:29:39 +0200 Subject: [PATCH 11/20] feat: pass a ViewOptions object with type --- lib/metrics/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/metrics/index.js b/lib/metrics/index.js index a2a9d92f..c49cefd6 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -100,7 +100,9 @@ module.exports = resource => { views.push({ meterName: '@cap-js/telemetry:host-metrics', instrumentName: 'system.*', - type: AggregationType.DROP + aggregation: { + type: AggregationType.DROP + } }) } meterProvider = new MeterProvider({ resource, readers: [metricReader], views }) From 6fd5731bcb490f2c9ff63c477950b6d391ba0c15 Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 20 Aug 2025 08:51:59 +0200 Subject: [PATCH 12/20] feat: distinguish push & pull --- lib/metrics/index.js | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/metrics/index.js b/lib/metrics/index.js index c49cefd6..0f93ca4a 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -14,7 +14,7 @@ const { const { getDynatraceMetadata, getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds, _require } = require('../utils') const _protocol2module = { - grpc: '@opentelemetry/exporter-metrics-otlp-grpc', + 'grpc': '@opentelemetry/exporter-metrics-otlp-grpc', 'http/protobuf': '@opentelemetry/exporter-metrics-otlp-proto', 'http/json': '@opentelemetry/exporter-metrics-otlp-http' } @@ -26,53 +26,59 @@ function _getExporter() { credentials } = cds.env.requires.telemetry - // for kind telemetry-to-otlp based on env vars - if (metricsExporter === 'env') { + if (metricsExporter === 'env') { // ... process env to determine exporter module to use let protocol = getStringFromEnv('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL') - // on kyma, the otlp endpoint speaks grpc, but otel's default protocol is http/protobuf -> fix default + if (!protocol) { + // > On kyma, the otlp endpoint speaks grpc, but otel's default protocol is http/protobuf -> fix default const endpoint = (getStringFromEnv('OTEL_EXPORTER_OTLP_METRICS_ENDPOINT') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_ENDPOINT') ?? '') if (endpoint.match(/:4317/)) protocol = 'grpc' } + protocol ??= (getStringFromEnv('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL')) metricsExporter = { module: _protocol2module[protocol], class: 'OTLPMetricExporter' } } - // use _require for better error message - const metricsExporterModule = - metricsExporter.module === '@cap-js/telemetry' ? require('../exporter') : _require(metricsExporter.module) + // Import the configured exporter module > use _require for better error message + const metricsExporterModule = metricsExporter.module === '@cap-js/telemetry' + ? require('../exporter') + : _require(metricsExporter.module) if (!metricsExporterModule[metricsExporter.class]) throw new Error(`Unknown metrics exporter "${metricsExporter.class}" in module "${metricsExporter.module}"`) + const config = { ...(metricsExporter.config || {}) } + config.temporalityPreference ??= AggregationTemporality.DELTA + // Augment configruation depending on 'kind' of telementry if (kind.match(/to-dynatrace$/)) { if (!credentials) credentials = getCredsForDTAsUPS() if (!credentials) throw new Error('No Dynatrace credentials found.') + config.url ??= `${credentials.apiurl}/v2/otlp/v1/metrics` config.headers ??= {} - // credentials.rest_apitoken?.token is deprecated and only supported for compatibility reasons + + // Extract REST API token from credentials to configure auth: + // > 'metrics_apitoken' for compatibility with previous releases + // > 'credentials.rest_apitoken?.token' is deprecated and only supported for compatibility reasons const { token_name } = cds.env.requires.telemetry - // metrics_apitoken for compatibility with previous releases const token = credentials[token_name] || credentials.metrics_apitoken || credentials.rest_apitoken?.token - if (!token) - throw new Error(`Neither "${token_name}" nor deprecated "rest_apitoken.token" found in Dynatrace credentials`) + if (!token) throw new Error(`Neither "${token_name}" nor deprecated "rest_apitoken.token" found in Dynatrace credentials`) + config.headers.authorization ??= `Api-Token ${token}` } if (kind.match(/to-cloud-logging$/)) { if (!credentials) credentials = getCredsForCLSAsUPS() if (!credentials) throw new Error('No SAP Cloud Logging credentials found.') + augmentCLCreds(credentials) + config.url ??= credentials.url config.credentials ??= credentials.credentials } - // default to DELTA - config.temporalityPreference ??= AggregationTemporality.DELTA - const exporter = new metricsExporterModule[metricsExporter.class](config) LOG._debug && LOG.debug('Using metrics exporter:', exporter) - return exporter } @@ -83,9 +89,13 @@ module.exports = resource => { * general setup */ const metricsConfig = cds.env.requires.telemetry.metrics.config - const exporter = _getExporter() - // TODO: pull - const metricReader = new PeriodicExportingMetricReader({ ...metricsConfig, exporter }) + let exporter = _getExporter() + + if (typeof exporter.export === 'function') { + // In case export is a function to be called by this runtime (push): + // > The exporter needs to be wrappeed thus, to set an export interval + exporter = new PeriodicExportingMetricReader({ ...metricsConfig, exporter }) + } let meterProvider = metrics.getMeterProvider() if (meterProvider.constructor.name === 'NoopMeterProvider') { @@ -105,7 +115,7 @@ module.exports = resource => { } }) } - meterProvider = new MeterProvider({ resource, readers: [metricReader], views }) + meterProvider = new MeterProvider({ resource, readers: [exporter], views }) metrics.setGlobalMeterProvider(meterProvider) } else { // TODO: CALM From 1e8e7e73a234113702e9c810cfe6838a41864c5c Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 3 Sep 2025 21:42:31 +0200 Subject: [PATCH 13/20] fix: add on handler to mocked remote service --- test/metrics-outbox.test.js | 42 ++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index 5ad37b42..24fe7fa9 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -55,21 +55,39 @@ describe("queue metrics for single tenant service", () => { beforeEach(() => (consoleDirLogs.length = 0)); - test("metrics are collected", async () => { - if (cds.version.split(".")[0] < 9) return; + describe("given the target service succeeds immediately", () => { + let unboxedService - await GET("/odata/v4/proxy/proxyCallToExternalService", admin); + beforeAll(async () => { + unboxedService = await cds.connect.to("ExternalService"); - await wait(150); // Wait for metrics to be collected + unboxedService.on("call", (req) => { + return req.reply("OK"); + }) + }) - expect(metricValue("cold_entries")).to.eq(totalCold); - expect(metricValue("remaining_entries")).to.eq(0); - expect(metricValue("incoming_messages")).to.eq(totalInc); - expect(metricValue("outgoing_messages")).to.eq(totalOut); - expect(metricValue("min_storage_time_in_seconds")).to.eq(0); - expect(metricValue("med_storage_time_in_seconds")).to.eq(0); - expect(metricValue("max_storage_time_in_seconds")).to.eq(0); - }); + afterAll(async () => { + unboxedService.handlers.before = unboxedService.handlers.before.filter( + (handler) => handler.on !== "call" + ); + }); + + test("metrics are collected", async () => { + if (cds.version.split(".")[0] < 9) return; + + await GET("/odata/v4/proxy/proxyCallToExternalService", admin); + + await wait(150); // Wait for metrics to be collected + + expect(metricValue("cold_entries")).to.eq(totalCold); + expect(metricValue("remaining_entries")).to.eq(0); + expect(metricValue("incoming_messages")).to.eq(totalInc); + expect(metricValue("outgoing_messages")).to.eq(totalOut); + expect(metricValue("min_storage_time_in_seconds")).to.eq(0); + expect(metricValue("med_storage_time_in_seconds")).to.eq(0); + expect(metricValue("max_storage_time_in_seconds")).to.eq(0); + }); + }) describe("given a target service that requires retries", () => { let currentRetryCount = 0; From 7b897a6859be3704af103ee7052d8d167daefbeb Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 3 Sep 2025 21:44:39 +0200 Subject: [PATCH 14/20] fix: add on handler to mocked remote service --- test/metrics-outbox-multitenant.test.js | 65 ++++++++++++++++--------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 03867ef0..30e44c15 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -74,31 +74,48 @@ describe("queue metrics for multi tenant service", () => { beforeEach(() => (consoleDirLogs.length = 0)); - test("metrics are collected per tenant", async () => { - if (cds.version.split(".")[0] < 9) return; + describe("given the target service succeeds immediately", () => { + let unboxedService; + + beforeAll(async () => { + unboxedService = await cds.connect.to("ExternalService"); + + unboxedService.on("call", (req) => { + return req.reply("OK"); + }); + }); + + afterAll(async () => { + unboxedService.handlers.before = unboxedService.handlers.before.filter( + (handler) => handler.on !== "call" + ); + }); + test("metrics are collected per tenant", async () => { + if (cds.version.split(".")[0] < 9) return; + + await Promise.all([ + GET("/odata/v4/proxy/proxyCallToExternalService", user[T1]), + GET("/odata/v4/proxy/proxyCallToExternalService", user[T2]), + ]); + + await wait(150); // Wait for metrics to be collected + + expect(metricValue(T1, "cold_entries")).to.eq(totalCold[T1]); + expect(metricValue(T1, "incoming_messages")).to.eq(totalInc[T1]); + expect(metricValue(T1, "outgoing_messages")).to.eq(totalOut[T1]); + expect(metricValue(T1, "remaining_entries")).to.eq(0); + expect(metricValue(T1, "min_storage_time_in_seconds")).to.eq(0); + expect(metricValue(T1, "med_storage_time_in_seconds")).to.eq(0); + expect(metricValue(T1, "max_storage_time_in_seconds")).to.eq(0); - await Promise.all([ - GET("/odata/v4/proxy/proxyCallToExternalService", user[T1]), - GET("/odata/v4/proxy/proxyCallToExternalService", user[T2]), - ]); - - await wait(150); // Wait for metrics to be collected - - expect(metricValue(T1, "cold_entries")).to.eq(totalCold[T1]); - expect(metricValue(T1, "incoming_messages")).to.eq(totalInc[T1]); - expect(metricValue(T1, "outgoing_messages")).to.eq(totalOut[T1]); - expect(metricValue(T1, "remaining_entries")).to.eq(0); - expect(metricValue(T1, "min_storage_time_in_seconds")).to.eq(0); - expect(metricValue(T1, "med_storage_time_in_seconds")).to.eq(0); - expect(metricValue(T1, "max_storage_time_in_seconds")).to.eq(0); - - expect(metricValue(T2, "cold_entries")).to.eq(totalCold[T2]); - expect(metricValue(T2, "incoming_messages")).to.eq(totalInc[T2]); - expect(metricValue(T2, "outgoing_messages")).to.eq(totalOut[T2]); - expect(metricValue(T2, "remaining_entries")).to.eq(0); - expect(metricValue(T2, "min_storage_time_in_seconds")).to.eq(0); - expect(metricValue(T2, "med_storage_time_in_seconds")).to.eq(0); - expect(metricValue(T2, "max_storage_time_in_seconds")).to.eq(0); + expect(metricValue(T2, "cold_entries")).to.eq(totalCold[T2]); + expect(metricValue(T2, "incoming_messages")).to.eq(totalInc[T2]); + expect(metricValue(T2, "outgoing_messages")).to.eq(totalOut[T2]); + expect(metricValue(T2, "remaining_entries")).to.eq(0); + expect(metricValue(T2, "min_storage_time_in_seconds")).to.eq(0); + expect(metricValue(T2, "med_storage_time_in_seconds")).to.eq(0); + expect(metricValue(T2, "max_storage_time_in_seconds")).to.eq(0); + }); }); describe("given a target service that requires retries", () => { From 5c5709aa539eef5480713fd4cf0842af21ee1bd4 Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 3 Sep 2025 21:51:44 +0200 Subject: [PATCH 15/20] chore: integrate moder api --- test/metrics-outbox-multitenant.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 30e44c15..0a13ac3a 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -55,7 +55,7 @@ describe("queue metrics for multi tenant service", () => { beforeAll(async () => { const proxyService = await cds.connect.to("ProxyService"); const unboxedService = await cds.connect.to("ExternalService"); - const queuedService = cds.outboxed(unboxedService); + const queuedService = cds.queued(unboxedService); proxyService.on("proxyCallToExternalService", async (req) => { totalInc[cds.context.tenant] += 1; From ed97ff79e4cea94075d8525cc741647277d148af Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 3 Sep 2025 21:54:10 +0200 Subject: [PATCH 16/20] chore: integrate modern api --- test/metrics-outbox.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index 24fe7fa9..f78c4fb2 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -40,7 +40,7 @@ describe("queue metrics for single tenant service", () => { beforeAll(async () => { const proxyService = await cds.connect.to("ProxyService"); const externalService = await cds.connect.to("ExternalService"); - const queuedService = cds.outboxed(externalService); + const queuedService = cds.queued(externalService); proxyService.on("proxyCallToExternalService", async (req) => { await queuedService.send("call", {}); From ae432fc5b1c6d8aa04250c74c0d67fa6041149ef Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 3 Sep 2025 21:56:27 +0200 Subject: [PATCH 17/20] Revert "chore: integrate modern api" This reverts commit ed97ff79e4cea94075d8525cc741647277d148af. --- test/metrics-outbox.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index f78c4fb2..24fe7fa9 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -40,7 +40,7 @@ describe("queue metrics for single tenant service", () => { beforeAll(async () => { const proxyService = await cds.connect.to("ProxyService"); const externalService = await cds.connect.to("ExternalService"); - const queuedService = cds.queued(externalService); + const queuedService = cds.outboxed(externalService); proxyService.on("proxyCallToExternalService", async (req) => { await queuedService.send("call", {}); From 4ee89ef8bf9448b80677f48efbbad43a920cb068 Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 3 Sep 2025 21:56:44 +0200 Subject: [PATCH 18/20] Revert "chore: integrate moder api" This reverts commit 5c5709aa539eef5480713fd4cf0842af21ee1bd4. --- test/metrics-outbox-multitenant.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 0a13ac3a..30e44c15 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -55,7 +55,7 @@ describe("queue metrics for multi tenant service", () => { beforeAll(async () => { const proxyService = await cds.connect.to("ProxyService"); const unboxedService = await cds.connect.to("ExternalService"); - const queuedService = cds.queued(unboxedService); + const queuedService = cds.outboxed(unboxedService); proxyService.on("proxyCallToExternalService", async (req) => { totalInc[cds.context.tenant] += 1; From aae86f9687f687ceaedf3610ee0b9cfc19495622 Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 17 Sep 2025 13:22:34 +0200 Subject: [PATCH 19/20] chore: bump dependency versions --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b62af35f..52422440 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,13 @@ "dependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/core": "^2", - "@opentelemetry/instrumentation": "^0.200", - "@opentelemetry/instrumentation-http": "^0.200", + "@opentelemetry/instrumentation": "^0.205", + "@opentelemetry/instrumentation-http": "^0.205", "@opentelemetry/resources": "^2", "@opentelemetry/sdk-metrics": "^2", "@opentelemetry/sdk-trace-base": "^2", "@opentelemetry/sdk-trace-node": "^2", - "@opentelemetry/semantic-conventions": "^1.36" + "@opentelemetry/semantic-conventions": "^1.37" }, "peerDependencies": { "@sap/cds": ">=8" @@ -37,19 +37,19 @@ "@cap-js/telemetry": "file:.", "@dynatrace/oneagent-sdk": "^1.5.0", "@grpc/grpc-js": "^1.9.14", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.200", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.200", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.200", - "@opentelemetry/exporter-trace-otlp-proto": "^0.200", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.205", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.205", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.205", + "@opentelemetry/exporter-trace-otlp-proto": "^0.205", "@opentelemetry/host-metrics": "^0.36.0", - "@opentelemetry/instrumentation-runtime-node": "^0.17.0", + "@opentelemetry/instrumentation-runtime-node": "^0.17.1", "@sap/cds-mtxs": ">=2", "axios": "^1.6.7", "chai": "^4.4.1", "chai-as-promised": "^7.1.1", "chai-subset": "^1.6.0", - "eslint": "^9.7.0", - "express": "^4.18.2", + "eslint": "^9.35.0", + "express": "^4.21.2", "jest": "^29.7.0" }, "cds": { From c5d15ff6d8dcbf4833d221922657e57ba119c327 Mon Sep 17 00:00:00 2001 From: I548646 Date: Wed, 17 Sep 2025 13:22:49 +0200 Subject: [PATCH 20/20] feat: stop checking for calm delegates --- lib/logging/index.js | 21 +++++++-------------- lib/metrics/index.js | 29 +++++++++++++---------------- lib/tracing/index.js | 16 +++++----------- 3 files changed, 25 insertions(+), 41 deletions(-) diff --git a/lib/logging/index.js b/lib/logging/index.js index 6701bdd2..c7ce4f46 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -73,24 +73,17 @@ module.exports = resource => { const { logs, SeverityNumber } = require('@opentelemetry/api-logs') const { LoggerProvider, BatchLogRecordProcessor, SimpleLogRecordProcessor } = require('@opentelemetry/sdk-logs') - - let loggerProvider = logs.getLoggerProvider() - if (!loggerProvider.getDelegateLogger()) { - loggerProvider = new LoggerProvider({ resource }) - logs.setGlobalLoggerProvider(loggerProvider) - } else { - LOG._warn && LOG.warn('LoggerProvider already initialized by a different module. It will be used as is.') - loggerProvider = loggerProvider.getDelegateLogger() - } - + const exporter = _getExporter() - - const logProcessor = - _getCustomProcessor(exporter) || + const logProcessor = _getCustomProcessor(exporter) || (process.env.NODE_ENV === 'production' ? new BatchLogRecordProcessor(exporter) : new SimpleLogRecordProcessor(exporter)) - loggerProvider.addLogRecordProcessor(logProcessor) + + // TODO: CALM may have initialized a global provider already + + const loggerProvider = new LoggerProvider({ resource, processors: [logProcessor]}) + logs.setGlobalLoggerProvider(loggerProvider) cds.on('served', () => { const loggers = {} diff --git a/lib/metrics/index.js b/lib/metrics/index.js index 0f93ca4a..c0bfdca6 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -97,30 +97,27 @@ module.exports = resource => { exporter = new PeriodicExportingMetricReader({ ...metricsConfig, exporter }) } - let meterProvider = metrics.getMeterProvider() - if (meterProvider.constructor.name === 'NoopMeterProvider') { - const dtmetadata = getDynatraceMetadata() - resource = resourceFromAttributes({}).merge(resource).merge(dtmetadata) + const dtmetadata = getDynatraceMetadata(); + resource = resourceFromAttributes({}).merge(resource).merge(dtmetadata); // unfortunately, we have to pass views to the MeterProvider constructor // something like meterProvider.addView() would be a lot nicer for locality - let views = [] + let views = []; if (process.env.HOST_METRICS_RETAIN_SYSTEM) { // nothing to do } else { views.push({ - meterName: '@cap-js/telemetry:host-metrics', - instrumentName: 'system.*', + meterName: "@cap-js/telemetry:host-metrics", + instrumentName: "system.*", aggregation: { - type: AggregationType.DROP - } - }) + type: AggregationType.DROP, + }, + }); } - meterProvider = new MeterProvider({ resource, readers: [exporter], views }) - metrics.setGlobalMeterProvider(meterProvider) - } else { - // TODO: CALM - LOG._warn && LOG.warn('MeterProvider already initialized by a different module. It will be used as is.') - } + + // TODO: CALM may have initialized a global provider already + + const meterProvider = new MeterProvider({ resource, readers: [exporter], views }); + metrics.setGlobalMeterProvider(meterProvider); /* * add individual metrics diff --git a/lib/tracing/index.js b/lib/tracing/index.js index d9e5fdaa..7e97b001 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -152,17 +152,11 @@ module.exports = resource => { : new SimpleSpanProcessor(exporter, processorConfig) } - let tracerProvider = trace.getTracerProvider() - if (!tracerProvider.getDelegateTracer()) { - const dtmetadata = getDynatraceMetadata() - resource = resourceFromAttributes({}).merge(resource).merge(dtmetadata) - tracerProvider = new NodeTracerProvider({ resource, spanProcessors: [processor], sampler: _getSampler() }) - tracerProvider.register({ propagator: _getPropagator() }) - } else { - // TODO: CALM - LOG._warn && LOG.warn('TracerProvider already initialized by a different module. It will be used as is.') - tracerProvider = tracerProvider.getDelegate() - } + // TODO: CALM may have initialized a global provider already + + resource = resourceFromAttributes({}).merge(resource).merge(getDynatraceMetadata()) + const tracerProvider = new NodeTracerProvider({ resource, spanProcessors: [processor], sampler: _getSampler() }) + tracerProvider.register({ propagator: _getPropagator() }) // clear sap passport for new tx if (process.env.SAP_PASSPORT) {