Skip to content

Commit

Permalink
replace dependency func-loc
Browse files Browse the repository at this point in the history
  • Loading branch information
sjvans committed Dec 1, 2023
1 parent d067c49 commit ff2ae86
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 130 deletions.
2 changes: 1 addition & 1 deletion lib/exporter/ConsoleMetricExporter.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
2 changes: 1 addition & 1 deletion lib/exporter/ConsoleSpanExporter.js
Original file line number Diff line number Diff line change
@@ -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')

Expand Down
2 changes: 1 addition & 1 deletion lib/metrics/db-pool.js
Original file line number Diff line number Diff line change
@@ -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')

Expand Down
2 changes: 1 addition & 1 deletion lib/metrics/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const cds = require('@sap/cds')
const LOG = cds.log('otel')
const LOG = cds.log('telemetry')

const fs = require('fs')

Expand Down
2 changes: 1 addition & 1 deletion lib/tracing/index.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
41 changes: 41 additions & 0 deletions lib/tracing/locate.js
Original file line number Diff line number Diff line change
@@ -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]
}
}
231 changes: 111 additions & 120 deletions lib/tracing/trace.js
Original file line number Diff line number Diff line change
@@ -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
// <db.operation> <db.name>.<db.sql.table> or <db.operation> <db.name> 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
Expand All @@ -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: {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
// <db.operation> <db.name>.<db.sql.table> or <db.operation> <db.name> 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
}
Loading

0 comments on commit ff2ae86

Please sign in to comment.