Skip to content

Commit

Permalink
config
Browse files Browse the repository at this point in the history
  • Loading branch information
sjvans committed Oct 25, 2023
1 parent 2b1bfae commit c4f8b87
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 138 deletions.
98 changes: 83 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,90 @@ See [Getting Started](https://cap.cloud.sap/docs/get-started) on how to jumpstar

Add `@cap-js/opentelemetry-instrumentation` to your dependencies.

TODO: Which modules must be installed per feature?
TODO:
- Prerequisites?
- Which modules must be installed per feature?

## Configuration options - TODO

### Instrumentation range

## Configuration Options

### Instrumentations

TODO: add more info

Configure via `cds.requires.telemetry.instrumentations = { name: { module, class, config? } }`

Default:
```
{
"http": {
"module": "@opentelemetry/instrumentation-http",
"class": "HttpInstrumentation",
"config": {
"ignoreIncomingPaths": [
"/health"
]
}
},
"express": {
"module": "@opentelemetry/instrumentation-express",
"class": "ExpressInstrumentation",
"config": {
"ignoreLayersType": [
"middleware"
]
}
}
}
```

#### Http

Via `cds.env.requires.telemetry.instrumentations.http.ignoreIncomingPaths`, you can specify an array of endpoints which shall be excluded.

#### Express

By default the middlewares of express are not traced.
You can override this via `cds.env.requires.telemetry.instrumentations.express.ignoreLayersType`.
Allowed values are `router`, `middleware`, and `request_handler`.
For more information see [ExpressInstrumentation](https://www.npmjs.com/package/@opentelemetry/instrumentation-express)

### Sampler

TODO: add more info

Configure via `cds.requires.telemetry.trace.sampler = { kind, root?, ratio? }`

Default:
```
{
"kind": "ParentBasedSampler",
"root": "AlwaysOnSampler"
}
```

### Propagators

TODO: add more info

Configure via `cds.requires.telemetry.trace.propagators = [<name> | { module, class, config? }]`

Default:
```
["W3CTraceContextPropagator"]
```


### Instrumentation range - TODO

- Set the log level for the cds logger `app` to `trace`, to trace individual CAP handler
- With log level `info` of `cds` the handling function in each Service is traced, including DB Services
- Annotate services with `@cds.tracing : false` to disable all tracing for that service. Counterwise, you can enable only the tracing for one service with `@cds.tracing : true`. The exception is detailed OData Adapter tracing, which can only be enabled or disabled globally. At the moment the annotation also only disables all CAP tracing, but not the HTTP and Express tracing.
- Use `const { instrumentations } = require('@cap-js/opentelemetry-instrumentation')` to adjust the instrumentations which are used by this plugin. By default HTTP, Express and HDB instrumentations are used
- -> no hdb as not published
- -> all done via cds config
- By default the middlewares of express are not traced. You can override this, by overriding `cds.env.requires.telemetry.instrumentations.express.ignoreLayersType`. Allowed values are 'router', 'middleware' or 'request_handler'. For more information see [ExpressInstrumentation](https://www.npmjs.com/package/@opentelemetry/instrumentation-express)
- Annotate services with `@cds.tracing : false` to disable all tracing for that service. Counterwise, you can enable only the tracing for one service with `@cds.tracing : true`. The exception is detailed OData Adapter tracing, which can only be enabled or disabled globally. At the moment the annotation also only disables all CAP tracing, but not the HTTP and Express tracing.



### Exporter
### Exporter - TODO

Locally the default exporter is a custom console exporter.
With the following setting you get the normal console exporter output from OTEL in the form of larger json objects:
Expand All @@ -45,23 +114,25 @@ You can also manually specify the exporter:
"export": "jaeger" | "http" | "grpc" | "proto"
}
```
With `cds.env.requires.telemetry.instrumentations.http.ignoreIncomingPaths` you can specify an array of endpoints which shall be excluded. By default it is `/health`

### Details


### Details - TODO

- In production the BatchSpanProcessor, locally SimpleSpanProcessor is used.
- For Jaeger locally run `docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -e COLLECTOR_OTLP_ENABLED=true -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 4317:4317 -p 4318:4318 -p 14250:14250 -p 14268:14268 -p 14269:14269 -p 9411:9411 jaegertracing/all-in-one:latest` and open `localhost:16686` to see the traces.
- Due to the tracing initial requests might be slower, locally all requests are slower due to the sync writing to the console.
- In CF Environments `process.env.VCAP_APPLICATION` and `process.env.CF_INSTANCE_GUID` are used to determine the appropriate Resource Attributes



### Environment variables

- NO_TELEMETRY | Disables all tracing
- OTEL_RESOURCE_ATTRIBUTES | Specify additional resource attributes. Per specification the "user defined" attributes, e.g. what CAP defines, has higher priority
- OTEL_SERVICE_NAME | Allows to override the name identified CAP. CAP will use the package.json name and version
- OTEL_LOG_LEVEL | Override the log level for OTEL, by default log level of cds logger `trace` is used
- OTEL_TRACES_EXPORTER | Override the exporter type
- OTEL_PROPAGATORS | Override propagator. Default is W3CTraceContextPropagator

[Batch Span processor config](https://opentelemetry.io/docs/reference/specification/sdk-environment-variables/#batch-span-processor):
- OTEL_BSP_SCHEDULE_DELAY | Override default OTEL value
Expand All @@ -79,10 +150,7 @@ Should all work, as no explizit configuration is provided by this package:
- DEFAULT_EXPORT_MAX_BACKOFF
- DEFAULT_EXPORT_BACKOFF_MULTIPLIER

### Configuration Options

- cds.env.requires.telemetry.trace.sampler = { kind, root?, ratio? }
- defaults: kind: ParentBasedSampler, root: AlwaysOnSampler

## Troubleshooting - TODO

Expand Down
2 changes: 1 addition & 1 deletion cds-plugin.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
if (!process.env.NO_TELEMETRY) require('./lib').instrumentApplication()
if (!process.env.NO_TELEMETRY) require('./lib')()
132 changes: 10 additions & 122 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,11 @@ const xsenv = require('@sap/xsenv')
// @opentelemetry
//

const { SpanKind, trace, metrics, diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api')
const { trace, metrics, diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api')
const { registerInstrumentations } = require('@opentelemetry/instrumentation')
const { Resource } = require('@opentelemetry/resources')
const { SemanticAttributes, SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions')

const {
BatchSpanProcessor,
ConsoleSpanExporter,
SimpleSpanProcessor,
SamplingDecision
} = require('@opentelemetry/sdk-trace-base')
const { BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base')
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node')

const {
Expand All @@ -37,8 +31,7 @@ const { OTLPMetricExporter: OTLPMetricExporterProto } = require('@opentelemetry/
// own
//

// _require for better error message
const _require = require('./utils/require')
const { getInstrumentations, getResource, getSampler, getPropagator } = require('./utils/config')

const registerMetrics = require('./metrics')
const { CDSConsoleMetricsExporter } = require('./metrics/CDSConsoleMetricsExporter')
Expand Down Expand Up @@ -214,110 +207,9 @@ function dependencyExists(name) {

// ----------------------------------------------------

function _getInstrumentations() {
const instrumentations = []
for (const each of Object.values(cds.env.requires.telemetry.instrumentations)) {
const module = _require(each.module)
if (!module[each.class]) throw new Error(`Unknown class "${each.class}" in module "${each.module}"`)
instrumentations.push(new module[each.class]({ ...each.config }))
}
return instrumentations
}

function _getResource() {
// REVISIT: Think about adding more from:
// https://github.tools.sap/CPA/telemetry-semantic-conventions/blob/main/specification/sap-extensions/resource/service.md
const attributes = {
[SemanticResourceAttributes.SERVICE_NAME]: process.env.OTEL_SERVICE_NAME
? process.env.OTEL_SERVICE_NAME
: cds.env.requires.otel.trace.name, // Set service name to CDS Service
[SemanticResourceAttributes.SERVICE_NAMESPACE]: cds.env.requires.otel.trace.name,
[SemanticResourceAttributes.SERVICE_VERSION]: cds.env.requires.otel.trace.version,
[SemanticResourceAttributes.PROCESS_RUNTIME_NAME]: 'nodejs',
[SemanticResourceAttributes.PROCESS_RUNTIME_VERSION]: process.versions.node,
[SemanticResourceAttributes.PROCESS_PID]: process.pid,
'process.parent_pid': process.ppid,
// [SemanticResourceAttributes.PROCESS_EXECUTABLE_NAME]: process.execArgv, // REVISIT: What is the executable name
[SemanticResourceAttributes.PROCESS_EXECUTABLE_PATH]: process.execPath,
// [SemanticResourceAttributes.PROCESS_OWNER]: process.owner, // REVISIT: Who should be the owner
'sap.visibility.level': process.env.NODE_ENV !== 'production' ? 'confidential' : 'internal'
}

if (process.env.CF_INSTANCE_GUID)
attributes[SemanticResourceAttributes.SERVICE_INSTANCE_ID] = process.env.CF_INSTANCE_GUID

// Specified CF attributes in https://github.tools.sap/CPA/telemetry-semantic-conventions/blob/main/specification/sap-extensions/resource/cf.md
const vcapApplication = process.env.VCAP_APPLICATION && JSON.parse(process.env.VCAP_APPLICATION)
if (vcapApplication) {
attributes['sap.cf.source_id'] = vcapApplication.application_id
attributes['sap.cf.instance_id'] = process.env.CF_INSTANCE_GUID
attributes['sap.cf.app_id'] = vcapApplication.application_id
attributes['sap.cf.app_name'] = vcapApplication.name
attributes['sap.cf.space_id'] = vcapApplication.space_id
attributes['sap.cf.space_name'] = vcapApplication.space_name
attributes['sap.cf.org_id'] = vcapApplication.organization_id
attributes['sap.cf.org_name'] = vcapApplication.organization_name
// attributes["sap.cf.source_type"] = vcapApplication -- for logs // REVISIT: ???
attributes['sap.cf.process.id'] = vcapApplication.process_id
attributes['sap.cf.process.instance_id'] = vcapApplication.process_id // REVISIT: Not sure
attributes['sap.cf.process.type'] = vcapApplication.process_type
}

return new Resource(attributes)
}

function _filterSampler(filterFn, parent) {
return {
shouldSample(ctx, tid, spanName, spanKind, attr, links) {
if (!filterFn(spanName, spanKind, attr)) return { decision: SamplingDecision.NOT_RECORD }
return parent.shouldSample(ctx, tid, spanName, spanKind, attr, links)
}
}
}

function _ignoreSpecifiedPaths(spanName, spanKind, attributes) {
const { ignoreIncomingPaths } = cds.env.requires.telemetry.instrumentations.http
return (
!Array.isArray(ignoreIncomingPaths) ||
(Array.isArray(ignoreIncomingPaths) && !ignoreIncomingPaths.some(path => path === spanName)
? spanKind !== SpanKind.SERVER ||
!ignoreIncomingPaths.some(path => path === attributes[SemanticAttributes.HTTP_ROUTE])
: false)
)
}

function _getSampler() {
const { kind, root, ratio } = cds.env.requires.telemetry.trace.sampler
const base = require('@opentelemetry/sdk-trace-base')
if (!base[kind]) throw new Error(`Unknown sampler ${kind}`)
if (kind === 'ParentBasedSampler') {
if (!base[root]) throw new Error(`Unknown sampler ${root}`)
return new base[kind]({ root: new base[root](ratio || 0) })
}
return new base[kind]()
}

function _getPropagator() {
const propagators = []
const core = require('@opentelemetry/core')
let b3
for (const each of cds.env.requires.telemetry.trace.propagators) {
if (typeof each === 'string') {
if (!core[each]) throw new Error(`Unknown propagator "${each}" in module "@opentelemetry/core"`)
propagators.push(new core[each]())
} else {
b3 ??= _require('@opentelemetry/propagator-b3')
const { kind, injectEncoding } = each
if (!b3[kind]) throw new Error(`Unknown propagator "${kind}" in module "@opentelemetry/propagator-b3"`)
propagators.push(new b3[each]({ injectEncoding }))
}
}
return new core.CompositePropagator({ propagators })
}

// ----------------------------------------------------

function instrumentApplication() {
module.exports = function () {
/**
* Registers OpenTelemetry trace provider
* cds.env.requires.otel.trace.tracer is the place where the tracer is stored
Expand All @@ -331,18 +223,18 @@ function instrumentApplication() {
cds.env.requires.otel.trace.version = pack.version
}

const instrumentations = _getInstrumentations()

const resource = _getResource()

const sampler = _filterSampler(_ignoreSpecifiedPaths, _getSampler())
const instrumentations = getInstrumentations()
const resource = getResource()
const sampler = getSampler()
const tracerProvider = new NodeTracerProvider({ resource, sampler })

// --- HERE ---

const exporters = getExporters()

if (process.env.NODE_ENV !== 'production' || !isDynatraceEnabled()) setSpanProcessor(tracerProvider, exporters.trace)

const propagator = _getPropagator()
const propagator = getPropagator()
tracerProvider.register({ propagator })

registerInstrumentations({ tracerProvider, instrumentations })
Expand Down Expand Up @@ -390,7 +282,3 @@ function instrumentApplication() {
metrics.setGlobalMeterProvider(meterProvider)
registerMetrics()
}

module.exports = {
instrumentApplication
}
Loading

0 comments on commit c4f8b87

Please sign in to comment.