Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ jobs:
fail-fast: false
matrix:
node-version: [24.x, 22.x]
cds-version: [9, 8]
# cds-version: [10, 9]
cds-version: [9]
steps:
- uses: actions/checkout@v7.0.0
Comment thread
sjvans marked this conversation as resolved.
- name: Use Node.js ${{ matrix.node-version }}
Expand All @@ -35,7 +36,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: npm i -g @sap/cds-dk@${{ matrix.cds-version }}
- run: npm i
- run: if [ ${{ matrix.cds-version }} -eq 8 ]; then npm i -f @sap/cds@8 @cap-js/sqlite@1 @sap/cds-mtxs@2; fi
- run: npm explore better-sqlite3 -- npm run install
#- run: if [ ${{ matrix.cds-version }} -eq 9 ]; then npm i -f @sap/cds@9 @cap-js/sqlite@2 @sap/cds-mtxs@3; fi
- run: if [ ${{ matrix.cds-version }} -eq 9 ]; then npm explore better-sqlite3 -- npm run install; fi
- run: cds v
- run: npm run test
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
node-version: 24
registry-url: https://registry.npmjs.org/
- name: run tests
# REVISIT: remove "npm explore better-sqlite3 -- npm run install" with cds^10
run: |
npm i -g @sap/cds-dk
npm i
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,5 @@ dist
# test
test/msg-box
test/bookshop/gen
test/bookshop/.cdsrc.json

.claude/
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 1.7.0 - tbd
## Version 2.0.0 - 2026-06-29

### Added

- Support for OpenTelemetry SDK 2.0
- `@opentelemetry/instrumentation-undici` added to the list of default instrumentations
- Support for `@sap/cds^10`

### Changed

- `@opentelemetry/instrumentation-host-metrics` replaces deprecated `@opentelemetry/host-metrics`

### Fixed

- Prefer `VCAP_APPLICATION.name` over `name` in `package.json` for app name resolution

### Removed

- Support for `@sap/cds^8`

## Version 1.6.0 - 2025-12-16

### Added
Expand Down
26 changes: 14 additions & 12 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 Expand Up @@ -108,17 +103,14 @@ In environments where Dynatrace OneAgent is installed (e.g., SAP BTP CF), no Ope
Metrics are "measurements captured at runtime", which help you understand your app's health and performance.

The `@cap-js/telemetry` enables the observation of some metrics out of the box.
These include generic [`@opentelemetry/host-metrics`](https://www.npmjs.com/package/@opentelemetry/host-metrics) (if the package is found in the app's dependencies),
These include generic host metrics collected by [`@opentelemetry/instrumentation-host-metrics`](https://www.npmjs.com/package/@opentelemetry/instrumentation-host-metrics) (if the package is found in the app's dependencies),
metrics regarding the app's database pool, namely the [pool info](https://www.npmjs.com/package/generic-pool#pool-info) statistics of `generic-pool`,
as well as metrics regarding [CAP's Persistent Queue](https://cap.cloud.sap/docs/node.js/queue#persistent-queue).

#### Host Metrics

Currently, there is no public config option to influence which metrics `@opentelemetry/host-metrics` collects.
However, it is possible to instruct the meter provider during initialization, which metrics shall be ignored.
By default, this is done for all `system.*` metrics collected by `@opentelemetry/host-metrics`.
This can be disabled via environment variable `HOST_METRICS_RETAIN_SYSTEM=true`.
As these so-called *views* must be passed into the constructor, the above only applies in case `@cap-js/telemetry` initializes the meter provider.
[@opentelemetry/instrumentation-host-metrics](https://www.npmjs.com/package/@opentelemetry/instrumentation-host-metrics) allows you to optionally restrict collection to one or more metric groups via config option `metricGroups`.
For backward compatibility, `@cap-js/telemetry` limits collection to `['process.cpu', 'process.memory']` if not configured manually (via [`cds.requires.telemetry.instrumentations`](#instrumentations)) or disabled via environment variable `HOST_METRICS_RETAIN_SYSTEM=true`.

To avoid spamming the console, only `process.*` metrics are printed by default, regardless of whether the `system.*` metrics are ignored or not.
Printing the `system.*` metrics (if not ignored) in the built-in console exporter can be enabled via environment variable `HOST_METRICS_LOG_SYSTEM=true`.
Expand Down Expand Up @@ -289,7 +281,13 @@ In order to receive OpenTelemetry credentials in the binding to the SAP Cloud Lo
}
```

If you are binding your app to SAP Cloud Logging via a [user-provided service instance](https://docs.cloudfoundry.org/devguide/services/user-provided.html), make sure that it has either tag `cloud-logging` or `Cloud Logging`.
If you are binding your app to SAP Cloud Logging via a [user-provided service instance](https://docs.cloudfoundry.org/devguide/services/user-provided.html), make sure that it has the tag `Cloud Logging`.

> Tip: To add the required tag to an existing user-provided service, you can use:
> ```
> cf update-user-provided-service {service-name} -t "Cloud Logging"
> ```
> For detailed information about binding resolution in CAP, consult [`cds.connect()` β†’ Service Bindings](https://cap.cloud.sap/docs/node.js/cds-connect#service-bindings).

### `telemetry-to-jaeger`

Expand Down Expand Up @@ -395,6 +393,10 @@ Default:
"http": {
"module": "@opentelemetry/instrumentation-http",
"class": "HttpInstrumentation"
},
"undici": {
"module": "@opentelemetry/instrumentation-undici",
"class": "UndiciInstrumentation"
}
}
```
Expand Down
40 changes: 4 additions & 36 deletions cds-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,15 @@
const cds = require('@sap/cds')
if (!(cds.cli?.command in { '': 1, serve: 1, run: 1 })) return

// REVISIT: workaround for cds.cli.command bug
const j = process.argv.indexOf('build')
if (j > 1 && process.argv[j - 1].match(/cds(\.js)?$/)) return

// cds add XXX currently also has cli.command === ''
const i = process.argv.indexOf('add')
if (i > 1 && process.argv[i - 1].match(/cds(\.js)?$/)) return

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')()
})()
2 changes: 1 addition & 1 deletion lib/exporter/ConsoleMetricExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ConsoleMetricExporter extends StandardConsoleMetricExporter {
}

for (const scopeMetrics of metrics.scopeMetrics) {
if (scopeMetrics.scope.name.endsWith(':host-metrics')) {
if (scopeMetrics.scope.name === '@opentelemetry/instrumentation-host-metrics') {
// aggregate host metrics
const collector = {}
for (const metric of scopeMetrics.metrics) {
Expand Down
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
57 changes: 41 additions & 16 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const { registerInstrumentations } = require('@opentelemetry/instrumentation')
const tracing = require('./tracing')
const metrics = require('./metrics')
const logging = require('./logging')
const { getDiagLogLevel, getResource, _require } = require('./utils')
const { getDiagLogLevel, getResource, hasDependency, _require } = require('./utils')

function _getInstrumentations() {
const _instrumentations = cds.env.requires.telemetry.instrumentations
Expand All @@ -36,6 +36,33 @@ function _getInstrumentations() {
}
}

// if @opentelemetry/instrumentation-host-metrics is in project's dependencies but not in cds.env.requires.telemetry.instrumentations, add it automatically
if (
!Object.keys(_instrumentations).includes('instrumentation-host-metrics') &&
!Object.values(_instrumentations).find(i => i?.module === '@opentelemetry/instrumentation-host-metrics')
) {
try {
const pkg = require(require('path').join(cds.root, 'package'))
if (Object.keys(pkg.dependencies).includes('@opentelemetry/instrumentation-host-metrics')) {
Comment thread
sjvans marked this conversation as resolved.
_instrumentations['instrumentation-host-metrics'] = {
class: 'HostMetricsInstrumentation',
module: '@opentelemetry/instrumentation-host-metrics'
}
}
} catch (err) {
LOG._debug && LOG.debug('Failed to automatically add @opentelemetry/instrumentation-host-metrics:', err)
}
}

// by default, all `system.*` metrics shall be ignored
const host_metrics = Object.values(_instrumentations).find(
i => i?.module === '@opentelemetry/instrumentation-host-metrics'
)
if (host_metrics && !host_metrics.config?.metricGroups && !process.env.HOST_METRICS_RETAIN_SYSTEM) {
host_metrics.config ??= {}
host_metrics.config.metricGroups = ['process.cpu', 'process.memory']
}

const instrumentations = []
for (const each of Object.values(_instrumentations)) {
if (!each) continue //> could be falsy
Expand All @@ -62,34 +89,32 @@ function _getInstrumentations() {
return instrumentations
}

module.exports = function () {
function setup_standalone() {
// set logger and propagate log level
diag.setLogger(cds.log('telemetry'), getDiagLogLevel())

// create resource
const resource = getResource()

/*
* setup tracing
*/
// setup tracing, metrics, and logging
const tracerProvider = tracing(resource)

/*
* setup metrics
*/
const meterProvider = metrics(resource)

/*
* setup logging
*/
const loggerProvider = cds.env.requires.telemetry.logging ? logging(resource) : undefined

/*
* register instrumentations
*/
// register instrumentations
registerInstrumentations({
tracerProvider,
meterProvider,
loggerProvider,
instrumentations: _getInstrumentations()
})
}

function setup_with_calm() {
// setup tracing, metrics, and logging
tracing()
metrics()
if (cds.env.requires.telemetry.logging) logging()
}

module.exports = hasDependency('@sap/xotel-agent-ext-js') ? setup_with_calm : setup_standalone
Comment thread
sjvans marked this conversation as resolved.
Loading