From ff2ae861e3abc69362dbb0870c29d2cf2663e04b Mon Sep 17 00:00:00 2001 From: D050513 Date: Fri, 1 Dec 2023 13:41:16 +0100 Subject: [PATCH] replace dependency func-loc --- lib/exporter/ConsoleMetricExporter.js | 2 +- lib/exporter/ConsoleSpanExporter.js | 2 +- lib/metrics/db-pool.js | 2 +- lib/metrics/index.js | 2 +- lib/tracing/index.js | 2 +- lib/tracing/locate.js | 41 +++++ lib/tracing/trace.js | 231 +++++++++++++------------- lib/utils.js | 10 +- package.json | 3 +- 9 files changed, 165 insertions(+), 130 deletions(-) create mode 100644 lib/tracing/locate.js diff --git a/lib/exporter/ConsoleMetricExporter.js b/lib/exporter/ConsoleMetricExporter.js index 5dfa3f1..e8b977c 100644 --- a/lib/exporter/ConsoleMetricExporter.js +++ b/lib/exporter/ConsoleMetricExporter.js @@ -1,5 +1,5 @@ const cds = require('@sap/cds') -const LOG = cds.log('otel', { label: 'otel:metrics' }) +const LOG = cds.log('telemetry') const { ConsoleMetricExporter: StandardConsoleMetricExporter } = require('@opentelemetry/sdk-metrics') const { ExportResultCode, hrTimeToTimeStamp } = require('@opentelemetry/core') diff --git a/lib/exporter/ConsoleSpanExporter.js b/lib/exporter/ConsoleSpanExporter.js index 02dbffb..39fa040 100644 --- a/lib/exporter/ConsoleSpanExporter.js +++ b/lib/exporter/ConsoleSpanExporter.js @@ -1,5 +1,5 @@ const cds = require('@sap/cds') -const LOG = cds.log('otel', { label: 'otel:traces' }) +const LOG = cds.log('telemetry') const path = require('path') diff --git a/lib/metrics/db-pool.js b/lib/metrics/db-pool.js index d236d8b..f890eba 100644 --- a/lib/metrics/db-pool.js +++ b/lib/metrics/db-pool.js @@ -1,5 +1,5 @@ const cds = require('@sap/cds') -const LOG = cds.log('otel', { label: 'otel:metrics' }) +const LOG = cds.log('telemetry') const { metrics, ValueType } = require('@opentelemetry/api') diff --git a/lib/metrics/index.js b/lib/metrics/index.js index 0de053f..d85c1d6 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -1,5 +1,5 @@ const cds = require('@sap/cds') -const LOG = cds.log('otel') +const LOG = cds.log('telemetry') const fs = require('fs') diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 297f18d..1b6109c 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -1,5 +1,5 @@ const cds = require('@sap/cds') -const LOG = cds.log('otel') +const LOG = cds.log('telemetry') const { trace, SpanKind } = require('@opentelemetry/api') const { registerInstrumentations } = require('@opentelemetry/instrumentation') diff --git a/lib/tracing/locate.js b/lib/tracing/locate.js new file mode 100644 index 0000000..23427d2 --- /dev/null +++ b/lib/tracing/locate.js @@ -0,0 +1,41 @@ +const cds = require('@sap/cds') +const LOG = cds.log('telemetry') + +const { Session } = require('inspector') +const { promisify } = require('util') +const { uuid } = cds.utils + +const locations = new WeakMap() + +module.exports = async fn => { + if (locations.has(fn)) return locations.get(fn) + + let session + const script_urls = {} + const random = `__${uuid().replace(/-/g, '_')}__` + try { + global[random] = fn + session = new Session() + session.connect() + session.on('Debugger.scriptParsed', result => { + script_urls[result.params.scriptId] = result.params.url + }) + const session_post = promisify(session.post).bind(session) + await session_post('Debugger.enable') + const { result: { objectId } } = await session_post('Runtime.evaluate', { expression: random }) + const { internalProperties } = await session_post('Runtime.getProperties', { objectId }) + const function_location = internalProperties.find(({ name }) => name === '[[FunctionLocation]]') + const location = { + url: script_urls[function_location.value.value.scriptId], + line: function_location.value.value.lineNumber + 1, + column: function_location.value.value.columnNumber + 1 + } + locations.set(fn, location) + return location + } catch (err) { + LOG.warn(`Unable to locate function "${fn.name}" due to error:`, err) + } finally { + session?.disconnect() + delete global[random] + } +} diff --git a/lib/tracing/trace.js b/lib/tracing/trace.js index e66298b..04609d3 100644 --- a/lib/tracing/trace.js +++ b/lib/tracing/trace.js @@ -1,60 +1,115 @@ const cds = require('@sap/cds') -const LOG = cds.log('otel', { label: 'otel:traces' }) - -// FIXME: remove dependency to func-loc -const { locate } = require('func-loc') - -const { - SpanKind, - SpanStatusCode, - ROOT_CONTEXT, - trace: otelTrace, - context: otelContextAPI, - createContextKey: otelCreateContextKey -} = require('@opentelemetry/api') +const LOG = cds.log('telemetry') + +const locate = require('./locate') + +const otel = require('@opentelemetry/api') +const { SpanKind, SpanStatusCode, ROOT_CONTEXT } = otel const { SemanticAttributes } = require('@opentelemetry/semantic-conventions') +function _updateAttributeMapBasedOnRequestType(args, attributeMap) { + if (Array.isArray(args) && args.length > 0) { + switch (args[0].constructor.name) { + case 'Request': + attributeMap.set('sap.cds.entity', args[0].context.entity) + break + case 'ODataRequest': + attributeMap.set('sap.cds.entity', args[0].entity) + break + default: + break + } + } +} + +function _getParentSpan() { + if (!cds.context._otelctx) { + cds.context._otelKey = otel.createContextKey(cds.context.id) + cds.context._otelctx = otel.context.active() + const parent = otel.trace.getSpan(otel.context.active()) + if (!parent?.attributes['sap.cds.async']) { + cds.context._otelctx.setValue(cds.context._otelKey, parent) + } + } + return otel.context.active().getValue(cds.context._otelKey) || cds.context._otelctx.getValue(cds.context._otelKey) +} + +function _getDBTarget(targetObj) { + if (targetObj.context?.target?.projection?.from?.ref[0]) return targetObj.context?.target?.projection?.from?.ref[0] + else if (targetObj._propagated) return _getDBTarget(targetObj._propagated) + else if (targetObj?.context?._propagated) return _getDBTarget(targetObj.context._propagated) + else return null +} + +function _getSpanName({ phase, event, path }, func, attributeMap, targetObj) { + if (targetObj.dbc) { + // DB name -- Guidelines: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + // . or or db.name + // db.name = targetObj.dbc.filename, db.operation = event, db.operation = targetObj.context.query - INSERT.into, SELECT.from, DELETE.from, UPDATE.entity + return ( + `${event ? event + ' ' : ''}` + + `${targetObj.dbc.filename || (targetObj.dbc.name in { hdb: 1, 'hana-client': 1 } ? `HANA` : 'db')}` + + `${ + !(event in { BEGIN: 1, COMMIT: 1, ROLLBACK: 1 }) && _getDBTarget(targetObj) + ? '."' + _getDBTarget(targetObj) + '"' + : '' + }` + ) + } + if (func.name && targetObj.constructor.name !== 'OData') { + attributeMap.set(SemanticAttributes.CODE_FUNCTION, func.name) + return `${targetObj.constructor.name}::${func.name}::${phase ? `${phase}::` : ''}${event}${path ? ` ${path}` : ''}` + } else { + return `${targetObj.constructor.name}::${phase}-${event}` + } +} + +function _determineKind(targetObj, phase, isAsyncConsumer, options) { + // DB Calls & Remote calls are client calls + if (targetObj.dbc || targetObj.constructor.name === 'RemoteService' || options.outbound) return SpanKind.CLIENT + if (targetObj.constructor.name === 'cds' || phase === 'emit') + // cds.spawn or srv.emit + return SpanKind.PRODUCER + if (isAsyncConsumer) return SpanKind.CONSUMER + return SpanKind.INTERNAL +} + /** * @param {string|object} name - * @param {*} func + * @param {*} fn * @param {*} targetObj * @param {*} args * @param {String} options.loggerName * @param {String} options.outbound Name of BTP destination * @returns */ -module.exports = async function trace(name, func, targetObj, args, options = {}) { +module.exports = async function trace(name, fn, targetObj, args, options = {}) { const attributeMap = new Map() - let spanName = typeof name === 'string' ? name : getSpanName(name, func, attributeMap, targetObj) + // REVISIT: what is this for? attributeMap.set('sap.cds.logger', options.loggerName || LOG.label) - try { - const fileSourceDetails = await locate(func) - const source = fileSourceDetails.source - const buildNamespace = source => { - const parts = source.split('/') - if (parts.some(part => part === 'cds')) return source // cds module not processed - const isInSrv = parts.some(part => part === 'srv') - let namespace = '', - srvPassed = false - for (const part of parts) { - if (isInSrv && !srvPassed) { - if (part === 'srv') srvPassed = true - } else namespace += `${namespace.length === 0 ? '' : '.'}${part}` - } - return namespace - } - attributeMap.set(SemanticAttributes.CODE_NAMESPACE, buildNamespace(source)) - attributeMap.set(SemanticAttributes.CODE_FILEPATH, fileSourceDetails.path) - attributeMap.set(SemanticAttributes.CODE_LINENO, fileSourceDetails.line) - attributeMap.set('code.column', fileSourceDetails.column) // REVISIT: SemanticAttributes.CODE_COLUMN did not yet exists when programming - } catch { - LOG.warn('Could not locate function and hence attributes are not specified in trace') + + const location = await locate(fn) + if (location) { + const path = location.url.replace('file://', '') + attributeMap.set(SemanticAttributes.CODE_FILEPATH, path) + const namespace = path.match(/\/node_modules\//) ? path.split('/node_modules/')[1] : path + attributeMap.set(SemanticAttributes.CODE_NAMESPACE, namespace) + attributeMap.set(SemanticAttributes.CODE_LINENO, location.line) + // REVISIT: SemanticAttributes.CODE_COLUMN did not yet exists when programming + attributeMap.set('code.column', location.column) } - if (cds.context?.http?.headers) attributeMap.set('http.correlation-id', cds.context.http.headers['x-correlation-id']) - if (cds.context?.tenant) attributeMap.set('sap.tenancy.tenant_id', cds.context.tenant) // https://github.tools.sap/CPA/telemetry-semantic-conventions/blob/main/specification/sap-extensions/resource/tenancy.md#sap-tenancy-related-otel-attributes + if (cds.context?.http?.headers) { + // REVISIT: 'http.correlation-id' or 'http.correlation_id'? + attributeMap.set('http.correlation-id', cds.context.http.headers['x-correlation-id']) + } + if (cds.context?.tenant) { + // https://github.tools.sap/CPA/telemetry-semantic-conventions/blob/main/specification/sap-extensions/resource/tenancy.md#sap-tenancy-related-otel-attributes + attributeMap.set('sap.tenancy.tenant_id', cds.context.tenant) + } + // REVISIT: correct? if (targetObj.dbc) { // DB specific attributes attributeMap.set(SemanticAttributes.DB_SYSTEM, cds.db.options.kind) // hanadb, postgresql, sqlite @@ -68,30 +123,21 @@ module.exports = async function trace(name, func, targetObj, args, options = {}) attributeMap.set(SemanticAttributes.DB_OPERATION, name?.event) } + // REVISIT: correct? if (targetObj.constructor.name === 'cds' || name?.phase === 'emit') { // cds for cds.spawn - emit for srv.emit attributeMap.set('sap.cds.async', true) } - updateAttributeMapBasedOnRequestType(args, attributeMap) - const getParent = () => { - if (!cds.context._otelctx) { - cds.context._otelKey = otelCreateContextKey(cds.context.id) - cds.context._otelctx = otelContextAPI.active() - const parent = otelTrace.getSpan(otelContextAPI.active()) - if (!parent?.attributes['sap.cds.async']) { - cds.context._otelctx.setValue(cds.context._otelKey, parent) - } - } - return otelContextAPI.active().getValue(cds.context._otelKey) || cds.context._otelctx.getValue(cds.context._otelKey) - } - const parentSpan = getParent(), - isAsyncCall = parentSpan?.attributes['sap.cds.async'] + _updateAttributeMapBasedOnRequestType(args, attributeMap) + + const parentSpan = _getParentSpan() + const isAsyncCall = parentSpan?.attributes['sap.cds.async'] const ctx = isAsyncCall ? ROOT_CONTEXT : parentSpan - ? otelTrace.setSpan(otelContextAPI.active(), parentSpan) - : otelContextAPI.active() + ? otel.trace.setSpan(otel.context.active(), parentSpan) + : otel.context.active() const spanOptions = { /* attributes: { @@ -100,16 +146,20 @@ module.exports = async function trace(name, func, targetObj, args, options = {}) [SemanticAttributes.HTTP_METHOD]: "GET", } */ - kind: determineKind(targetObj, name?.phase, isAsyncCall, options) + kind: _determineKind(targetObj, name?.phase, isAsyncCall, options) } + if (options.outbound) { attributeMap.set('sap.btp.destination', options.outbound) } + if (isAsyncCall) { spanOptions.links = [{ context: parentSpan.spanContext() }] spanOptions.parent = undefined } + // REVISIT: better way to get tracer? + const spanName = typeof name === 'string' ? name : _getSpanName(name, fn, attributeMap, targetObj) const span = cds._telemetry.tracer.startSpan(spanName, spanOptions, ctx) attributeMap.forEach((value, key) => { span.setAttribute(key, value) @@ -118,18 +168,18 @@ module.exports = async function trace(name, func, targetObj, args, options = {}) return getResult() function getResult() { - return otelContextAPI.with( + return otel.context.with( // otelTrace.setSpan(ctx, span), cds.context._otelctx.setValue(cds.context._otelKey, span), fnToExecute() ) } function fnToExecute() { - if (func.constructor.name === 'AsyncFunction') + if (fn.constructor.name === 'AsyncFunction') return async () => { let methodResult try { - methodResult = await func.apply(targetObj, args) + methodResult = await fn.apply(targetObj, args) span.setStatus({ code: SpanStatusCode.OK }) } catch (e) { span.recordException(e) @@ -144,7 +194,7 @@ module.exports = async function trace(name, func, targetObj, args, options = {}) return () => { let methodResult try { - methodResult = func.apply(targetObj, args) + methodResult = fn.apply(targetObj, args) span.setStatus({ code: SpanStatusCode.OK }) } catch (e) { span.recordException(e) @@ -157,62 +207,3 @@ module.exports = async function trace(name, func, targetObj, args, options = {}) } } } - -function updateAttributeMapBasedOnRequestType(args, attributeMap) { - if (Array.isArray(args) && args.length > 0) { - switch (args[0].constructor.name) { - case 'Request': - updateAttributeMap(attributeMap, args[0].context) - break - case 'ODataRequest': - updateAttributeMap(attributeMap, args[0]) - break - default: - break - } - } -} - -function getDBTarget(targetObj) { - if (targetObj.context?.target?.projection?.from?.ref[0]) return targetObj.context?.target?.projection?.from?.ref[0] - else if (targetObj._propagated) return getDBTarget(targetObj._propagated) - else if (targetObj?.context?._propagated) return getDBTarget(targetObj.context._propagated) - else return null -} - -function getSpanName({ phase, event, path }, func, attributeMap, targetObj) { - if (targetObj.dbc) { - // DB name -- Guidelines: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - // . or or db.name - // db.name = targetObj.dbc.filename, db.operation = event, db.operation = targetObj.context.query - INSERT.into, SELECT.from, DELETE.from, UPDATE.entity - return ( - `${event ? event + ' ' : ''}` + - `${targetObj.dbc.filename || (targetObj.dbc.name in { hdb: 1, 'hana-client': 1 } ? `HANA` : 'db')}` + - `${ - !(event in { BEGIN: 1, COMMIT: 1, ROLLBACK: 1 }) && getDBTarget(targetObj) - ? '."' + getDBTarget(targetObj) + '"' - : '' - }` - ) - } - if (func.name && targetObj.constructor.name !== 'OData') { - attributeMap.set(SemanticAttributes.CODE_FUNCTION, func.name) - return `${targetObj.constructor.name}::${func.name}::${phase ? `${phase}::` : ''}${event}${path ? ` ${path}` : ''}` - } else { - return `${targetObj.constructor.name}::${phase}-${event}` - } -} - -function updateAttributeMap(attributeMap, arg) { - attributeMap.set('sap.cds.entity', arg.entity) -} - -function determineKind(targetObj, phase, isAsyncConsumer, options) { - // DB Calls & Remote calls are client calls - if (targetObj.dbc || targetObj.constructor.name === 'RemoteService' || options.outbound) return SpanKind.CLIENT - if (targetObj.constructor.name === 'cds' || phase === 'emit') - // cds.spawn or srv.emit - return SpanKind.PRODUCER - if (isAsyncConsumer) return SpanKind.CONSUMER - return SpanKind.INTERNAL -} diff --git a/lib/utils.js b/lib/utils.js index b60a79c..3f78824 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,5 @@ const cds = require('@sap/cds') -const LOG = cds.log('otel') +const LOG = cds.log('telemetry') const { DiagLogLevel } = require('@opentelemetry/api') const { Resource } = require('@opentelemetry/resources') @@ -70,7 +70,11 @@ function isDynatraceEnabled() { const pkg = require(cds.root + '/package.json') return Object.keys(pkg.dependencies).includes('@dynatrace/oneagent-sdk') } catch (err) { - LOG._info && LOG.info('Unable to require package.json to check whether @dynatrace/oneagent-sdk is in dependencies due to error:', err) + LOG._info && + LOG.info( + 'Unable to require package.json to check whether @dynatrace/oneagent-sdk is in dependencies due to error:', + err + ) } return false } @@ -90,4 +94,4 @@ module.exports = { getResource, isDynatraceEnabled, _require -} \ No newline at end of file +} diff --git a/package.json b/package.json index 74f4b84..0ba7cf3 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ "@opentelemetry/sdk-node": "^0.36.1", "@opentelemetry/sdk-trace-base": "^1.10.1", "@opentelemetry/sdk-trace-node": "^1.17.1", - "@opentelemetry/semantic-conventions": "^1.10.1", - "func-loc": "^0.1.16" + "@opentelemetry/semantic-conventions": "^1.10.1" }, "devDependencies": { "@cap-js/opentelemetry-instrumentation": "file:.",