From cfa7bf8558f042e69ef659d89dc55ba539d81428 Mon Sep 17 00:00:00 2001 From: ankul <116072312+kulliax@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:54:44 +0000 Subject: [PATCH 1/2] feat: optional axios tracing setup based on axios-debug-log presence --- lib/tracing/axios-debug.js | 210 +++++++++++++++++++++++++++++++++++++ lib/tracing/index.js | 9 ++ 2 files changed, 219 insertions(+) create mode 100644 lib/tracing/axios-debug.js diff --git a/lib/tracing/axios-debug.js b/lib/tracing/axios-debug.js new file mode 100644 index 00000000..935277fc --- /dev/null +++ b/lib/tracing/axios-debug.js @@ -0,0 +1,210 @@ +const cds = require('@sap/cds') +const axios = require('axios') + +let _initialized = false + +/** + * Setup axios-debug-log mit OpenTelemetry Span Integration + */ +function setupAxiosTracing() { + if (_initialized) return + _initialized = true + + const LOG = cds.log('axios-telemetry') + + // Dynamisch axios-debug-log laden und konfigurieren + require('axios-debug-log')({ + request: (debug, config) => { + const span = _startSpan(config) + config._otelSpan = span + config._startTime = Date.now() + + debug( + `Request: ${config.method?.toUpperCase()} ${config.url}`, + config.headers ? `Headers: ${JSON.stringify(config.headers)}` : '' + ) + }, + response: (debug, response) => { + const { config } = response + const duration = Date.now() - (config._startTime || Date.now()) + + _endSpan(config._otelSpan, response.status, duration) + + debug( + `Response: ${response.status} ${response.statusText}`, + `Duration: ${duration}ms` + ) + }, + error: (debug, error) => { + const config = error.config || {} + const duration = Date.now() - (config._startTime || Date.now()) + + _endSpanWithError(config._otelSpan, error, duration) + + debug( + `Error: ${error.message}`, + error.response ? `Status: ${error.response.status}` : '' + ) + } + }) + + // Interceptors für alle axios Instanzen + _addGlobalInterceptors() + + LOG.debug('axios-debug-log configured with OpenTelemetry spans') +} + +/** + * Wrap eine einzelne axios Instanz mit Tracing + * @param {import('axios').AxiosInstance} instance + * @returns {import('axios').AxiosInstance} + */ +function wrapAxiosInstance(instance) { + const LOG = cds.log('axios-telemetry') + + instance.interceptors.request.use( + (config) => { + const span = _startSpan(config) + config._otelSpan = span + config._startTime = Date.now() + + // Trace Context Propagation + _injectTraceContext(config) + + return config + }, + (error) => { + LOG.error('Request interceptor error:', error.message) + return Promise.reject(error) + } + ) + + instance.interceptors.response.use( + (response) => { + const { config } = response + const duration = Date.now() - (config._startTime || Date.now()) + _endSpan(config._otelSpan, response.status, duration) + return response + }, + (error) => { + const config = error.config || {} + const duration = Date.now() - (config._startTime || Date.now()) + _endSpanWithError(config._otelSpan, error, duration) + return Promise.reject(error) + } + ) + + return instance +} + +/** + * Startet einen neuen OpenTelemetry Span für den Request + */ +function _startSpan(config) { + try { + // @cap-js/telemetry nutzt @opentelemetry/api + const { trace, SpanKind } = require('@opentelemetry/api') + const tracer = trace.getTracer('cds-plugin-axios-telemetry') + + const url = new URL(config.url, config.baseURL || 'http://localhost') + const spanName = `HTTP ${config.method?.toUpperCase()} ${url.pathname}` + + const span = tracer.startSpan(spanName, { + kind: SpanKind.CLIENT, + attributes: { + 'http.method': config.method?.toUpperCase(), + 'http.url': url.href, + 'http.target': url.pathname + url.search, + 'http.host': url.host, + 'http.scheme': url.protocol.replace(':', ''), + 'net.peer.name': url.hostname, + 'net.peer.port': url.port || (url.protocol === 'https:' ? 443 : 80) + } + }) + + return span + } catch (e) { + // OpenTelemetry nicht verfügbar - graceful degradation + return null + } +} + +/** + * Beendet den Span mit Success Status + */ +function _endSpan(span, statusCode, duration) { + if (!span) return + + try { + const { SpanStatusCode } = require('@opentelemetry/api') + + span.setAttribute('http.status_code', statusCode) + span.setAttribute('http.response_time_ms', duration) + + if (statusCode >= 400) { + span.setStatus({ code: SpanStatusCode.ERROR }) + } else { + span.setStatus({ code: SpanStatusCode.OK }) + } + + span.end() + } catch (e) { + // Ignore errors + } +} + +/** + * Beendet den Span mit Error Status + */ +function _endSpanWithError(span, error, duration) { + if (!span) return + + try { + const { SpanStatusCode } = require('@opentelemetry/api') + + span.setAttribute('http.response_time_ms', duration) + span.setAttribute('error', true) + span.setAttribute('error.message', error.message) + span.setAttribute('error.type', error.name || 'Error') + + if (error.response) { + span.setAttribute('http.status_code', error.response.status) + } + + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + + span.recordException(error) + span.end() + } catch (e) { + // Ignore errors + } +} + +/** + * Injiziert W3C Trace Context Header für Distributed Tracing + */ +function _injectTraceContext(config) { + try { + const { trace, context, propagation } = require('@opentelemetry/api') + + config.headers = config.headers || {} + propagation.inject(context.active(), config.headers) + } catch (e) { + // Ignore if OpenTelemetry not available + } +} + +/** + * Fügt globale Interceptors zur default axios Instanz hinzu + */ +function _addGlobalInterceptors() { + wrapAxiosInstance(axios) +} + +module.exports = { + setupAxiosTracing, + wrapAxiosInstance +} \ No newline at end of file diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 7d735ddf..95e7fe61 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -179,5 +179,14 @@ module.exports = resource => { require('./cds')() require('./cloud_sdk')() + try { + // only setup axios tracing if axios-debug-log is present, as it is an optional dependency and we don't want to force users to install it if they don't want axios tracing + require.resolve("axios-debug-log") + LOG._info && LOG.info('axios-debug-log found, setting up axios tracing') + require('./axios-debug').setupAxiosTracing() + } catch (e) { + LOG._info && LOG.info('axios-debug-log not found, skipping axios tracing setup') + } + return tracerProvider } From 59c2059383a90a6861d3ba4943c90ee3972a1a86 Mon Sep 17 00:00:00 2001 From: ankul <116072312+kulliax@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:18:25 +0000 Subject: [PATCH 2/2] fix: ensure proper span handling --- lib/tracing/axios-debug.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/tracing/axios-debug.js b/lib/tracing/axios-debug.js index 935277fc..7f91dc0e 100644 --- a/lib/tracing/axios-debug.js +++ b/lib/tracing/axios-debug.js @@ -1,5 +1,6 @@ const cds = require('@sap/cds') const axios = require('axios') +const LOG = cds.log('axios-telemetry') let _initialized = false @@ -10,8 +11,6 @@ function setupAxiosTracing() { if (_initialized) return _initialized = true - const LOG = cds.log('axios-telemetry') - // Dynamisch axios-debug-log laden und konfigurieren require('axios-debug-log')({ request: (debug, config) => { @@ -60,8 +59,6 @@ function setupAxiosTracing() { * @returns {import('axios').AxiosInstance} */ function wrapAxiosInstance(instance) { - const LOG = cds.log('axios-telemetry') - instance.interceptors.request.use( (config) => { const span = _startSpan(config) @@ -133,7 +130,7 @@ function _startSpan(config) { * Beendet den Span mit Success Status */ function _endSpan(span, statusCode, duration) { - if (!span) return + if (!span || span._ended) return try { const { SpanStatusCode } = require('@opentelemetry/api') @@ -157,7 +154,7 @@ function _endSpan(span, statusCode, duration) { * Beendet den Span mit Error Status */ function _endSpanWithError(span, error, duration) { - if (!span) return + if (!span || span._ended) return try { const { SpanStatusCode } = require('@opentelemetry/api')