Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -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()

Expand Down
10 changes: 4 additions & 6 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
91 changes: 47 additions & 44 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,40 +88,40 @@ module.exports = resource => {
/*
* general setup
*/
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 })
}

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 = []
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 })
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.')
}

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
*/
Expand Down
38 changes: 19 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,11 +146,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
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