UNPKG

@cap-js-community/event-queue

Version:

An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.

192 lines (167 loc) 5.64 kB
"use strict"; const _resilientRequire = (module) => { try { return require(module); } catch { // ignore } }; const cds = require("@sap/cds"); const otel = _resilientRequire("@opentelemetry/api"); const config = require("../config"); const eventQueueStats = require("./eventQueueStats"); const { getEnvInstance } = require("./env"); const COMPONENT_NAME = "/shared/openTelemetry"; let _statsSnapshot = null; let _metricsInitialized = false; const trace = async (context, label, fn, { attributes = {}, newRootSpan = false, traceContext } = {}) => { if (!config.enableTelemetry || !otel) { return fn(); } const tracerProvider = otel.trace.getTracerProvider(); if ((!tracerProvider || tracerProvider === otel.trace.NOOP_TRACER_PROVIDER) && !process.env.DT_NODE_PRELOAD_OPTIONS) { return fn(); } const tracer = otel.trace.getTracer("@cap-js-community/event-queue"); const extractedContext = traceContext ? otel.propagation.extract(otel.context.active(), traceContext) : otel.context.active(); const span = tracer.startSpan( `eventqueue-${label}`, { kind: otel.SpanKind.INTERNAL, root: newRootSpan, }, extractedContext ); _setAttributes(context, span, attributes); const ctxWithSpan = otel.trace.setSpan(extractedContext, span); return await _startOtelTrace(ctxWithSpan, traceContext, span, fn); }; const _startOtelTrace = async (ctxWithSpan, traceContext, span, fn) => { return otel.context.with(ctxWithSpan, async () => { const onSuccess = (res) => { span.setStatus({ code: otel.SpanStatusCode.OK }); return res; }; const onFailure = (e) => { span.recordException(e); span.setStatus( Object.assign({ code: otel.SpanStatusCode.ERROR }, e.message ? { message: e.message } : undefined) ); throw e; }; const onDone = () => { try { if (span.status?.code !== otel.SpanStatusCode.UNSET && !span.ended) { span.end?.(); } } catch (err) { cds.log(COMPONENT_NAME).error("error in tracing", err, { span, }); } }; try { const res = fn(); if (res instanceof Promise) { return res.then(onSuccess).catch(onFailure).finally(onDone); } return onSuccess(res); } catch (e) { onFailure(e); } finally { onDone(); } }); }; const _setAttributes = (context, span, attributes) => { span.setAttribute("sap.tenancy.tenant_id", context.tenant); span.setAttribute("sap.correlation_id", context.id); _sanitizeAttributes(attributes); for (const attributeKey in attributes) { span.setAttribute(attributeKey, attributes[attributeKey]); } }; const _sanitizeAttributes = (attributes = {}) => { for (const attributeKey in attributes) { attributes[attributeKey] = typeof attributes[attributeKey] !== "string" ? JSON.stringify(attributes[attributeKey]) : attributes[attributeKey]; } return attributes; }; const getCurrentTraceContext = () => { if (!otel) { return null; } const carrier = {}; otel.propagation.inject(otel.context.active(), carrier); return carrier; }; const _refreshStats = async () => { try { const namespaces = await eventQueueStats.getAllNamespaceStats(); _statsSnapshot = { namespaces, lastRefreshedAt: Date.now() }; } catch (err) { cds.log(COMPONENT_NAME).error("failed to refresh queue stats for metrics", err); } }; const initMetrics = () => { if ( _metricsInitialized || !config.collectEventQueueMetrics || !config.enableTelemetry || !config.redisEnabled || !config.registerAsEventProcessor || (getEnvInstance().applicationInstance !== undefined && getEnvInstance().applicationInstance !== 0) || !otel?.metrics ) { return; } const meterProvider = otel.metrics.getMeterProvider?.(); if (!meterProvider) { return; } _metricsInitialized = true; eventQueueStats .resetInProgressCounters() .catch((err) => cds.log(COMPONENT_NAME).error("failed to reset inProgress counters", err)); const meter = otel.metrics.getMeter("@cap-js-community/event-queue"); const pendingGauge = meter.createObservableGauge("cap.event_queue.jobs.pending", { description: "Current number of jobs waiting to be processed.", unit: "1", }); const inProgressGauge = meter.createObservableGauge("cap.event_queue.jobs.in_progress", { description: "Current number of jobs actively being processed by workers.", unit: "1", }); const refreshAgeGauge = meter.createObservableGauge("cap.event_queue.stats.refresh_age", { description: "Age of the most recent queue statistics snapshot.", unit: "s", }); _statsSnapshot = { lastRefreshedAt: Date.now(), namespaces: Object.fromEntries( config.processingNamespaces.map((namespace) => [namespace, { pending: 0, inProgress: 0 }]) ), }; _refreshStats(); meter.addBatchObservableCallback( (observableResult) => { if (!_statsSnapshot) { return; } observableResult.observe(refreshAgeGauge, (Date.now() - _statsSnapshot.lastRefreshedAt) / 1000); for (const [namespace, stats] of Object.entries(_statsSnapshot.namespaces)) { const attrs = { "queue.namespace": namespace }; observableResult.observe(pendingGauge, stats.pending, attrs); observableResult.observe(inProgressGauge, stats.inProgress, attrs); } }, [pendingGauge, inProgressGauge, refreshAgeGauge] ); setInterval(_refreshStats, 30_000).unref(); }; module.exports = { trace, getCurrentTraceContext, initMetrics };