Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
19b08e7
bump otel deps to ^2.0
sjvans Aug 12, 2025
f6bd730
rm warning
sjvans Aug 12, 2025
a7023ef
disable version check
sjvans Aug 12, 2025
b3aaf01
new Resource() -> resourceFromAttributes()
sjvans Aug 12, 2025
e2aeae6
tracing
sjvans Aug 12, 2025
8a7df1a
metrics
sjvans Aug 12, 2025
2a90135
Merge branch 'main' into otel-2.0
sjvans Aug 12, 2025
1a7089f
rm otel<2 check
sjvans Aug 13, 2025
a2fa911
feat: replace access to otel env variables
PDT42 Aug 15, 2025
fd40b73
feat: replace deprecated core imports
PDT42 Aug 15, 2025
1dd8369
feat: adopt tracing sdk api changes replacing parentSpanId
PDT42 Aug 15, 2025
3c7d1c0
feat: pass a ViewOptions object with type
PDT42 Aug 15, 2025
0b682d2
Merge branch 'main' into otel-2.0
PDT42 Aug 15, 2025
6fd5731
feat: distinguish push & pull
PDT42 Aug 20, 2025
a5cdbf8
Merge branch 'main' into otel-2.0
PDT42 Aug 20, 2025
9f44b52
Merge branch 'main' into otel-2.0
PDT42 Sep 2, 2025
1e8e7e7
fix: add on handler to mocked remote service
PDT42 Sep 3, 2025
7b897a6
fix: add on handler to mocked remote service
PDT42 Sep 3, 2025
5c5709a
chore: integrate moder api
PDT42 Sep 3, 2025
ed97ff7
chore: integrate modern api
PDT42 Sep 3, 2025
ae432fc
Revert "chore: integrate modern api"
PDT42 Sep 3, 2025
4ee89ef
Revert "chore: integrate moder api"
PDT42 Sep 3, 2025
40780c5
Merge branch 'fix/adopt-unhandled-action-throws' into otel-2.0
PDT42 Sep 3, 2025
4acff8b
Merge branch 'main' into otel-2.0
PDT42 Sep 4, 2025
aae86f9
chore: bump dependency versions
PDT42 Sep 17, 2025
c5d15ff
feat: stop checking for calm delegates
PDT42 Sep 17, 2025
3538c4c
Merge branch 'main' into otel-2.0
PDT42 Sep 22, 2025
40ac5c5
Merge branch 'main' into otel-2.0
PDT42 Nov 27, 2025
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
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
36 changes: 0 additions & 36 deletions cds-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')()
})()
6 changes: 3 additions & 3 deletions lib/exporter/ConsoleSpanExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
30 changes: 11 additions & 19 deletions lib/logging/index.js
Original file line number Diff line number Diff line change
@@ -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')

Expand All @@ -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' }
}

Expand Down Expand Up @@ -75,23 +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.constructor.name === 'ProxyLoggerProvider') {
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.')
}


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 = {}
Expand Down
102 changes: 51 additions & 51 deletions lib/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ const cds = require('@sap/cds')
const LOG = cds.log('telemetry')

const { metrics } = require('@opentelemetry/api')
const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core')
const { Resource } = require('@opentelemetry/resources')
const { getStringFromEnv } = 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')

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'
}
Expand All @@ -27,55 +26,59 @@ function _getExporter() {
credentials
} = cds.env.requires.telemetry

// 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
// on kyma, the otlp endpoint speaks grpc, but otel's default protocol is http/protobuf -> fix default
if (metricsExporter === 'env') { // ... process env to determine exporter module to use
let protocol = getStringFromEnv('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL') ?? getStringFromEnv('OTEL_EXPORTER_OTLP_PROTOCOL')

if (!protocol) {
const endpoint = otlp_env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? otlp_env.OTEL_EXPORTER_OTLP_ENDPOINT ?? ''
// > 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 ??= 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' }
}

// 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
}

Expand All @@ -85,39 +88,36 @@ module.exports = resource => {
/*
* general setup
*/
let meterProvider = metrics.getMeterProvider()
if (meterProvider.constructor.name === 'NoopMeterProvider') {
const dtmetadata = getDynatraceMetadata()
resource = new Resource({}).merge(resource).merge(dtmetadata)
const metricsConfig = cds.env.requires.telemetry.metrics.config
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 })
}

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(
new View({
meterName: '@cap-js/telemetry:host-metrics',
instrumentName: 'system.*',
aggregation: new DropAggregation()
})
)
views.push({
meterName: "@cap-js/telemetry:host-metrics",
instrumentName: "system.*",
aggregation: {
type: AggregationType.DROP,
},
});
}
meterProvider = new MeterProvider({ resource, views })
metrics.setGlobalMeterProvider(meterProvider)
} else {
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)
}
// TODO: CALM may have initialized a global provider already

const meterProvider = new MeterProvider({ resource, readers: [exporter], views });
metrics.setGlobalMeterProvider(meterProvider);

/*
* add individual metrics
Expand Down
32 changes: 13 additions & 19 deletions lib/tracing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ const cds = require('@sap/cds')
const LOG = cds.log('telemetry')

const { trace } = require('@opentelemetry/api')
const { getEnv, getEnvWithoutDefaults } = require('@opentelemetry/core')
const { Resource } = require('@opentelemetry/resources')
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')

Expand Down Expand Up @@ -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' }
}

Expand Down Expand Up @@ -137,16 +135,7 @@ module.exports = resource => {
/*
* general setup
*/
let tracerProvider = trace.getTracerProvider()
if (!tracerProvider.getDelegateTracer()) {
const dtmetadata = getDynatraceMetadata()
resource = new Resource({}).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$/) &&
Expand All @@ -157,13 +146,18 @@ 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)
}

// 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) {
cds.on('served', () => {
Expand Down
2 changes: 1 addition & 1 deletion lib/tracing/trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading