Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main'
Browse files Browse the repository at this point in the history
# Conflicts:
#	lib/tracing/index.js
#	lib/tracing/trace.js
  • Loading branch information
Max Gruenfelder committed Dec 20, 2024
2 parents fc77277 + 50e405e commit 8db0387
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 133 deletions.
72 changes: 67 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,68 @@ 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.1.2 - 2024-12-10

### Fixed

- ConsoleSpanExporter: `cds.context` may be undefined in local scripting scenarios

## Version 1.1.1 - 2024-11-28

### Fixed

- Use attribute `url.path` (with fallback to deprecated `http.target`) for sampling decision

## Version 1.1.0 - 2024-11-27

### Added

- Predefined kind `telemetry-to-otlp` that creates exporters based on OTLP exporter configuration via environment variables
- Experimental!: Propagate W3C trace context to SAP HANA via session context `SAP_PASSPORT`
- Enable via environment variable `SAP_PASSPORT`
- If `@opentelemetry/instrumentation-runtime-node` is in the project's dependencies but not in `cds.env.requires.telemetry.instrumentations`, it is registered automatically
- Disable via `cds.env.requires.telemetry.instrumentations.instrumentation-runtime-node = false`

### Changed

- Base config moved to new `cds.requires.kinds.telemetry` for improved config merging

### Fixed

- Built-in `ConsoleMetricExporter` uses correct attribute name `process.cpu.state` while exporting host metrics
- Exporting traces to the console in the presence of a traceparent header

## Version 1.0.1 - 2024-08-10

### Fixed

- Explicitly pass own providers when registering instrumentations (the global providers may be influenced by, for example, Dynatrace OneAgent)

## Version 1.0.0 - 2024-08-08

### Added

- Support for tracing native db statements (i.e., `cds.run('SELECT * FROM DUMMY')`)
- Support for SAP Cloud Logging credentials via user-provided service
- Support for adding `@opentelemetry/instrumentation-runtime-node`
- `npm add @opentelemetry/instrumentation-runtime-node`
- To `cds.requires.telemetry.instrumentations`, add:
```json
"instrumentation-runtime-node": {
"class": "RuntimeNodeInstrumentation",
"module": "@opentelemetry/instrumentation-runtime-node"
}
```

### Changed

- Instrumentations are registered after tracing and metrics are set up
- `telemetry-to-dynatrace`: Regardless of whether Dynatrace OneAgent is present or not, if dependency `@opentelemetry/exporter-trace-otlp-proto` is present, `@cap-js/telemetry` will export the traces via OpenTelemetry.

### Fixed

- Tracing of db statements without active span

## Version 0.2.3 - 2024-06-17

### Fixed
Expand Down Expand Up @@ -40,8 +102,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Added

- Support for own, high resolution timestamps
+ Enable via `cds.env.requires.telemetry.tracing.hrtime = true`
+ Enabled by default in development profile
- Enable via `cds.env.requires.telemetry.tracing.hrtime = true`
- Enabled by default in development profile

## Version 0.0.5 - 2024-03-11

Expand All @@ -54,10 +116,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Changed

- By default, all `system.*` metrics collected by `@opentelemetry/host-metrics` are ignored
+ Disable change via environment variable `HOST_METRICS_RETAIN_SYSTEM=true`
- Disable change via environment variable `HOST_METRICS_RETAIN_SYSTEM=true`
- Metric exporter's property `temporalityPreference` always gets defaulted to `DELTA`
+ Was previously only done for kind `telemetry-to-dynatrace`
+ Set custom value via `cds.env.requires.telemetry.metrics.exporter.config.temporalityPreference`
- Was previously only done for kind `telemetry-to-dynatrace`
- Set custom value via `cds.env.requires.telemetry.metrics.exporter.config.temporalityPreference`

### Fixed

Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs) and [o
- [`telemetry-to-dynatrace`](#telemetry-to-dynatrace)
- [`telemetry-to-cloud-logging`](#telemetry-to-cloud-logging)
- [`telemetry-to-jaeger`](#telemetry-to-jaeger)
- [`telemetry-to-otlp`](#telemetry-to-otlp)
- [Detailed Configuration Options](#detailed-configuration-options)
- [Configuration Pass Through](#configuration-pass-through)
- [Instrumentations](#instrumentations)
Expand Down Expand Up @@ -67,7 +68,7 @@ See [Predefined Kinds](#predefined-kinds) for additional dependencies you need t

The plugin can be disabled by setting environment variable `NO_TELEMETRY` to something truthy.

Database tracing is currently limited to [@cap-js/sqlite](https://www.npmjs.com/package/@cap-js/sqlite) and [@cap-js/hana](https://www.npmjs.com/package/@cap-js/hana).
Database tracing is limited to [@cap-js/cds-dbs](https://github.com/cap-js/cds-dbs)-based databases, such as [@cap-js/sqlite](https://www.npmjs.com/package/@cap-js/sqlite) and [@cap-js/hana](https://www.npmjs.com/package/@cap-js/hana).



Expand Down Expand Up @@ -169,7 +170,7 @@ Hence, a Dynatrace instance is required and the app must be bound to that Dynatr
Use via `cds.requires.telemetry.kind = 'to-dynatrace'`.

Required additional dependencies:
- `@opentelemetry/exporter-trace-otlp-proto`
- `@opentelemetry/exporter-trace-otlp-proto` (optional, see [Leveraging Dynatrace OneAgent](#leveraging-dynatrace-oneagent))
- `@opentelemetry/exporter-metrics-otlp-proto`

The necessary scopes for exporting traces (`openTelemetryTrace.ingest`) and metrics (`metrics.ingest`) are not part of the standard `apitoken` and must be requested.
Expand Down Expand Up @@ -204,6 +205,7 @@ If [Dynatrace OneAgent](https://www.dynatrace.com/platform/oneagent) is present,
(Your app still needs to be bound to a Dynatrace instance, of course. However, `@dynatrace/oneagent-sdk` is not required.)
Hence, additionally dependency `@opentelemetry/exporter-trace-otlp-proto` and scope `openTelemetryTrace.ingest` are not required.
This is actually the perferred operating model for `telemetry-to-dynatrace` as it provides a better experience than exporting via OpenTelemetry.
If dependency `@opentelemetry/exporter-trace-otlp-proto` is present anyway, `@cap-js/telemetry` will export the traces via OpenTelemetry as well.

### `telemetry-to-cloud-logging`

Expand Down Expand Up @@ -261,6 +263,19 @@ Run Jaeger locally via [docker](https://www.docker.com):
- With this, no custom credentials are needed
- Open `localhost:16686` to see the traces

### `telemetry-to-otlp`

Exports traces and metrics to an OTLP/gRPC or OTLP/HTTP endpoint based on [environment variables](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter).

Use via `cds.requires.telemetry.kind = 'to-otlp'`.

Required additional dependencies (`* = grpc|proto|http`):
- `@grpc/grpc-js` (in case of OTLP/gRPC)
- `@opentelemetry/exporter-trace-otlp-*`
- `@opentelemetry/exporter-metrics-otlp-*`

Please note that `@cap-js/telemetry` does not validate the configuration via environment variables!



## Detailed Configuration Options
Expand Down Expand Up @@ -437,6 +452,7 @@ Hence, the `hrtime` mode is on by default in development but not in production.

- `NO_TELEMETRY`: Disables the plugin
- `NO_LOCATE`: Disables function location in tracing
- `SAP_PASSPORT`: Enables propagating W3C trace context to SAP HANA (experimental!)
- `OTEL_LOG_LEVEL`: If not specified, the log level of cds logger `telemetry` is used
- `OTEL_SERVICE_NAME`: If not specified, the name is determined from package.json (defaulting to "CAP Application")
- `OTEL_SERVICE_VERSION`: If not specified, the version is determined from package.json (defaulting to "1.0.0")
Expand All @@ -455,8 +471,7 @@ This project is open to feature requests/suggestions, bug reports etc. via [GitH

## Code of Conduct

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/cap-js/.github/blob/main/CODE_OF_CONDUCT.md) at all times.


## Licensing
Expand Down
2 changes: 1 addition & 1 deletion lib/exporter/ConsoleMetricExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ConsoleMetricExporter extends StandardConsoleMetricExporter {
}
if (name === 'process.cpu.time' || name === 'process.cpu.utilization') {
collector[name] ??= {}
for (const dp of metric.dataPoints) collector[name][dp.attributes.state] = dp.value
for (const dp of metric.dataPoints) collector[name][dp.attributes['process.cpu.state']] = dp.value
}
if (metric.descriptor.name === 'process.memory.usage') {
collector[name] = metric.dataPoints[0].value
Expand Down
25 changes: 17 additions & 8 deletions lib/exporter/ConsoleSpanExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ const LOG = cds.log('telemetry')
const path = require('path')

const { ExportResultCode, hrTimeToMilliseconds } = require('@opentelemetry/core')
const {
ATTR_URL_PATH,
SEMATTRS_CODE_FILEPATH: ATTR_CODE_FILEPATH,
SEMATTRS_CODE_LINENO: ATTR_CODE_LINENO
// ATTR_CODE_COLUMN
} = require('@opentelemetry/semantic-conventions')
// REVISIT: ATTR_CODE_COLUMN doesn't yet exist in semantic conventions 1.27
const ATTR_CODE_COLUMN = 'code.column'

const _padded = v =>
`${`${v}`.split('.')[0].padStart(3, ' ')}.${(`${v}`.split('.')[1] || '0').padEnd(2, '0').substring(0, 2)}`
Expand All @@ -18,25 +26,25 @@ const _span2line = (span, parentStartTime = 0) => {
let result = `\n ${_padded(start)}${_padded(end)} = ${_padded(duration)} ms`

let name = span.name
if (name.match(/^[A-Z]+$/)) name = name + ' ' + span.attributes['http.target']
if (name.match(/^[A-Z]+$/)) name = name + ' ' + span.attributes[ATTR_URL_PATH]
if (name.length > 80) name = name.substring(0, 79) + '…'

result += ' ' + (span.___indent || '') + name

// REVISIT: what is this for?
if (span.attributes['code.filepath'] !== undefined) {
if (span.attributes[ATTR_CODE_FILEPATH] !== undefined) {
if (
path
.normalize(span.attributes['code.filepath'])
.normalize(span.attributes[ATTR_CODE_FILEPATH])
.match(new RegExp(path.normalize(cds.env._home).replaceAll('\\', '\\\\'), 'g')) &&
!path.normalize(span.attributes['code.filepath']).match(/node_modules/g)
!path.normalize(span.attributes[ATTR_CODE_FILEPATH]).match(/node_modules/g)
) {
result += `: .${path
.normalize(span.attributes['code.filepath'])
.normalize(span.attributes[ATTR_CODE_FILEPATH])
.substring(
path.normalize(cds.env._home).length + 1,
path.normalize(span.attributes['code.filepath']).length
)}:${span.attributes['code.lineno']}:${span.attributes['code.column']}`
path.normalize(span.attributes[ATTR_CODE_FILEPATH]).length
)}:${span.attributes[ATTR_CODE_LINENO]}:${span.attributes[ATTR_CODE_COLUMN]}`
}
}

Expand Down Expand Up @@ -92,7 +100,8 @@ class ConsoleSpanExporter /* implements SpanExporter */ {
*/
_sendSpans(spans, done) {
for (const span of spans) {
if (!span.parentSpanId || span.name === 'cds.spawn') {
const w3c_parent_id = cds.context?.http?.req.headers.traceparent?.split('-')[2]
if (!span.parentSpanId || span.parentSpanId === w3c_parent_id || span.name === 'cds.spawn') {
let toLog = 'elapsed times:'
toLog += _span2line(span)
const children = this._temporaryStorage.get(span.spanContext().traceId)
Expand Down
59 changes: 52 additions & 7 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
const cds = require('@sap/cds')
const LOG = cds.log('telemetry')

const { diag } = require('@opentelemetry/api')
const { registerInstrumentations } = require('@opentelemetry/instrumentation')
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions')

const tracing = require('./tracing')
const metrics = require('./metrics')
const { getDiagLogLevel, getResource } = require('./utils')
const { getDiagLogLevel, getResource, _require } = require('./utils')

function _getInstrumentations() {
const _instrumentations = cds.env.requires.telemetry.instrumentations

// if @opentelemetry/instrumentation-runtime-node is in project's dependencies but not in cds.env.requires.telemetry.instrumentations, add it automatically
if (
!Object.keys(_instrumentations).includes('instrumentation-runtime-node') &&
!Object.values(_instrumentations).find(i => i?.module === '@opentelemetry/instrumentation-runtime-node')
) {
try {
const pkg = require(require('path').join(cds.root, 'package'))
if (Object.keys(pkg.dependencies).includes('@opentelemetry/instrumentation-runtime-node')) {
_instrumentations['instrumentation-runtime-node'] = {
class: 'RuntimeNodeInstrumentation',
module: '@opentelemetry/instrumentation-runtime-node'
}
}
} catch (e) {
LOG._debug && LOG.debug('Failed to automatically add @opentelemetry/instrumentation-runtime-node:', e)
}
}

const instrumentations = []
for (const each of Object.values(_instrumentations)) {
if (!each) continue //> could be falsy
const module = _require(each.module)
if (!module[each.class]) throw new Error(`Unknown instrumentation "${each.class}" in module "${each.module}"`)
instrumentations.push(new module[each.class]({ ...(each.config || {}) }))
}

return instrumentations
}

module.exports = function () {
// set logger and propagate log level
Expand All @@ -14,17 +49,27 @@ module.exports = function () {

// REVISIT: better way to make available?
cds._telemetry = {
name: resource.attributes['service.name'],
version: resource.attributes['service.version']
name: resource.attributes[ATTR_SERVICE_NAME],
version: resource.attributes[ATTR_SERVICE_VERSION]
}

/*
* add tracing
* setup tracing
*/
const tracerProvider = tracing(resource)

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

/*
* add metrics
* register instrumentations
*/
metrics(resource)
registerInstrumentations({
tracerProvider,
meterProvider,
// loggerProvider,
instrumentations: _getInstrumentations()
})
}
4 changes: 0 additions & 4 deletions lib/metrics/db-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ function init(pools) {
pools.delete(tenant)
}

// // REVISIT: is "-meter" appendix the standard approach?
// const meter = metrics.getMeter(`${cds._telemetry.name}-meter`)

// REVISIT: what shall this name be?
const meter = metrics.getMeter('@cap-js/telemetry:db-pool')

const borrowed = meter.createObservableGauge('db.pool.borrowed', {
Expand Down
28 changes: 25 additions & 3 deletions lib/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 {
AggregationTemporality,
Expand All @@ -11,7 +12,13 @@ const {
View
} = require('@opentelemetry/sdk-metrics')

const { getDynatraceMetadata, getCredsForDTAsUPS, augmentCLCreds, _require } = require('../utils')
const { getDynatraceMetadata, getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds, _require } = require('../utils')

const _protocol2module = {
grpc: '@opentelemetry/exporter-metrics-otlp-grpc',
'http/protobuf': '@opentelemetry/exporter-metrics-otlp-proto',
'http/json': '@opentelemetry/exporter-metrics-otlp-http'
}

function _getExporter() {
let {
Expand All @@ -20,14 +27,26 @@ function _getExporter() {
credentials
} = cds.env.requires.telemetry

// for kind telemetry-to-otlp based on env vars
if (metricsExporter === 'env') {
const otlp_env = getEnvWithoutDefaults()
const dflt_env = getEnv()
const protocol =
otlp_env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL ??
otlp_env.OTEL_EXPORTER_OTLP_PROTOCOL ??
dflt_env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL ??
dflt_env.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)
if (!metricsExporterModule[metricsExporter.class])
throw new Error(`Unknown metrics exporter "${metricsExporter.class}" in module "${metricsExporter.module}"`)
const metricsConfig = { ...(metricsExporter.config || {}) }

if (kind.match(/dynatrace/)) {
if (kind.match(/to-dynatrace$/)) {
if (!credentials) credentials = getCredsForDTAsUPS()
if (!credentials) throw new Error('No Dynatrace credentials found.')
metricsConfig.url ??= `${credentials.apiurl}/v2/otlp/v1/metrics`
Expand All @@ -41,7 +60,8 @@ function _getExporter() {
metricsConfig.headers.authorization ??= `Api-Token ${token}`
}

if (kind.match(/cloud-logging/)) {
if (kind.match(/to-cloud-logging$/)) {
if (!credentials) credentials = getCredsForCLSAsUPS()
if (!credentials) throw new Error('No SAP Cloud Logging credentials found.')
augmentCLCreds(credentials)
metricsConfig.url ??= credentials.url
Expand Down Expand Up @@ -101,4 +121,6 @@ module.exports = resource => {
*/
require('./db-pool')()
require('./host')()

return meterProvider
}
Loading

0 comments on commit 8db0387

Please sign in to comment.