UNPKG

@mojaloop/event-sdk

Version:
537 lines 26.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createW3CTracestate = exports.setHttpHeader = exports.Span = void 0; const lodash_1 = __importDefault(require("lodash")); const serialize_error_1 = require("serialize-error"); const traceparent_1 = __importDefault(require("traceparent")); const stringify = require('safe-stable-stringify'); const Logger = require('@mojaloop/central-services-logger'); const EventMessage_1 = require("./model/EventMessage"); const Recorder_1 = require("./Recorder"); const EventLoggingServiceClient_1 = require("./transport/EventLoggingServiceClient"); const config_1 = __importDefault(require("./lib/config")); const util_1 = __importDefault(require("./lib/util")); const defaultRecorder = config_1.default.EVENT_LOGGER_SIDECAR_DISABLED ? new Recorder_1.DefaultLoggerRecorder() : new Recorder_1.DefaultSidecarRecorder(new EventLoggingServiceClient_1.EventLoggingServiceClient(config_1.default.EVENT_LOGGER_SERVER_HOST, config_1.default.EVENT_LOGGER_SERVER_PORT, config_1.default.EVENT_LOGGER_KAFKA)); const nullRecorder = { recorder: () => null, preProcess: (event) => event, record: async () => ({ status: EventMessage_1.LogResponseStatus.accepted }) }; let consoleRecorder = config_1.default.EVENT_LOGGER_SIDECAR_DISABLED && defaultRecorder; let kafkaRecorder = !config_1.default.EVENT_LOGGER_SIDECAR_DISABLED && !!config_1.default.EVENT_LOGGER_KAFKA && defaultRecorder; let sidecarRecorder = !config_1.default.EVENT_LOGGER_SIDECAR_DISABLED && !config_1.default.EVENT_LOGGER_KAFKA && defaultRecorder; const eventRecorder = (eventType) => { switch (config_1.default[eventType]) { case 'sidecar': sidecarRecorder ||= new Recorder_1.DefaultSidecarRecorder(new EventLoggingServiceClient_1.EventLoggingServiceClient(config_1.default.EVENT_LOGGER_SERVER_HOST, config_1.default.EVENT_LOGGER_SERVER_PORT)); return sidecarRecorder; case 'kafka': kafkaRecorder ||= new Recorder_1.DefaultSidecarRecorder(new EventLoggingServiceClient_1.EventLoggingServiceClient(config_1.default.EVENT_LOGGER_SERVER_HOST, config_1.default.EVENT_LOGGER_SERVER_PORT, config_1.default.EVENT_LOGGER_KAFKA)); return kafkaRecorder; case 'console': consoleRecorder ||= new Recorder_1.DefaultLoggerRecorder(); return consoleRecorder; case 'off': return nullRecorder; } }; const defaultRecorders = { defaultRecorder, logRecorder: eventRecorder('EVENT_LOGGER_LOG'), auditRecorder: eventRecorder('EVENT_LOGGER_AUDIT'), traceRecorder: eventRecorder('EVENT_LOGGER_TRACE') }; /** * A dict containing EventTypes which should be treated asynchronously * */ const asyncOverrides = util_1.default.eventAsyncOverrides(config_1.default.ASYNC_OVERRIDE_EVENTS); class Span { spanContext; recorders; isFinished = false; /** * Creates new span. Normally this is not used directly, but by a Tracer.createSpan method * @param spanContext context of the new span. Service is obligatory. Depending on the rest provided values, the new span will be created as a parent or child span * @param {Recorders} recorders different recorders to be used for different logging methods * @param defaultTagsSetter the tags setter method can be passed here */ constructor(spanContext, recorders, defaultTagsSetter) { this.defaultTagsSetter = defaultTagsSetter ? defaultTagsSetter : this.defaultTagsSetter; this.recorders = recorders || defaultRecorders; if (!!spanContext.tags && !!spanContext.tags.tracestate) { spanContext.tracestates = util_1.default.getTracestateMap(config_1.default.EVENT_LOGGER_VENDOR_PREFIX, spanContext.tags.tracestate).tracestates; if (!spanContext.tracestates[config_1.default.EVENT_LOGGER_VENDOR_PREFIX]) { spanContext.tracestates[config_1.default.EVENT_LOGGER_VENDOR_PREFIX] = { spanId: spanContext.spanId }; } } this.spanContext = spanContext; this.defaultTagsSetter(); this.spanContext = Object.freeze(this.spanContext); return this; } /** * A method to set tags by default. * @param message the message which tags will be extracted from */ defaultTagsSetter(message) { const w3cHeaders = getTracestate(this.spanContext); if (w3cHeaders) { this.spanContext.tags && this.setTags(Object.assign(this.spanContext.tags, w3cHeaders)); if (!(config_1.default.EVENT_LOGGER_VENDOR_PREFIX in this.getTracestates())) { this.spanContext.tracestates && this.setTracestates(Object.assign(this.spanContext.tracestates, util_1.default.getTracestateMap(config_1.default.EVENT_LOGGER_VENDOR_PREFIX, w3cHeaders.tracestate).tracestates)); } } return this; } setTracestates(tracestates) { let newContext = new EventMessage_1.EventTraceMetadata(this.getContext()); for (let key in tracestates) { newContext.tracestates[key] = tracestates[key]; } this.spanContext = Object.freeze(new EventMessage_1.EventTraceMetadata(newContext)); return this; } /** * Gets trace context from the current span */ getContext() { return Object.assign({}, this.spanContext, { tags: JSON.parse(stringify(this.spanContext.tags)) }); } /** * Creates and returns new child span of the current span and changes the span service name * @param service the name of the service of the new child span * @param recorders the recorders which are be set to the child span. If omitted, the recorders of the parent span are used */ getChild(service, recorders = this.recorders) { try { let inputTraceContext = this.getContext(); return new Span(new EventMessage_1.EventTraceMetadata(Object.assign({}, inputTraceContext, { service, spanId: undefined, startTimestamp: undefined, finishTimestamp: undefined, parentSpanId: inputTraceContext.spanId })), recorders, this.defaultTagsSetter); } catch (e) { throw (e); } } /** * Injects trace context into a carrier with optional path. * @param carrier any kind of message or other object with keys of type String. * @param injectOptions type and path of the carrier. Type is not implemented yet. Path is the path to the trace context. */ injectContextToMessage(carrier, injectOptions = {}) { let result = lodash_1.default.cloneDeep(carrier); let { path } = injectOptions; // type not implemented yet if (carrier instanceof EventMessage_1.EventMessage || (('metadata' in carrier))) path = 'metadata'; else if (carrier instanceof EventMessage_1.EventTraceMetadata) { return Promise.resolve(this.spanContext); } if (!path) { Object.assign(result, { trace: this.spanContext }); } else { lodash_1.default.merge(lodash_1.default.get(result, path), { trace: this.spanContext }); } return result; } /** * Injects trace context into a http request headers. * @param request HTTP request. * @param type type of the headers that will be created - 'w3c' or 'xb3'. */ injectContextToHttpRequest(request, type = EventMessage_1.HttpRequestOptions.w3c) { let result = lodash_1.default.cloneDeep(request); result.headers = setHttpHeader(this.spanContext, type, result.headers); return result; } /** * Sets tags to the current span. If child span is created, the tags are passed on. * @param tags key value pairs of tags. Tags can be changed on different child spans */ setTags(tags) { let newContext = new EventMessage_1.EventTraceMetadata(this.getContext()); for (let key in tags) { if (key === 'tracestate' || key === 'traceparent') continue; newContext.tags[key] = tags[key]; } this.spanContext = Object.freeze(new EventMessage_1.EventTraceMetadata(newContext)); return this; } _setTagTracestate(tags) { let newContext = new EventMessage_1.EventTraceMetadata(this.getContext()); newContext.tags.tracestate = tags.tracestate; this.spanContext = Object.freeze(new EventMessage_1.EventTraceMetadata(newContext)); return this; } /** * Returns tags values */ getTags() { const { tags } = this.getContext(); return !!tags ? tags : {}; } /** * Sets tags, persisted in the tracestate header as key value pairs as base64 encoded string * @param tags key-value pairs with tags */ setTracestateTags(tags) { this.spanContext.tracestates[config_1.default.EVENT_LOGGER_VENDOR_PREFIX] = Object.assign(this.spanContext.tracestates[config_1.default.EVENT_LOGGER_VENDOR_PREFIX], tags); const { ownTraceStateString, restTraceStateString } = encodeTracestate(this.spanContext); this._setTagTracestate({ tracestate: `${ownTraceStateString}${restTraceStateString}` }); return this; } /** * Returns the tracestates object per vendor, as configured vendor tracestate is decoded key value pair with tags */ getTracestates() { return this.spanContext.tracestates; } /** * Returns the tracestate tags for the configured vendor as key value pairs */ getTracestateTags() { if (config_1.default.EVENT_LOGGER_VENDOR_PREFIX in this.spanContext.tracestates) { return this.spanContext.tracestates[config_1.default.EVENT_LOGGER_VENDOR_PREFIX]; } else { return {}; } } /** * Finishes the current span and its trace and sends the data to the tracing framework. * @param message optional parameter for a message to be passed to the tracing framework. * @param finishTimestamp optional parameter for the finish time. If omitted, current time is used. */ async finish(message, state, finishTimestamp) { if (this.spanContext.finishTimestamp) { return Promise.reject(new Error('span already finished')); } let spanContext = this._finishSpan(finishTimestamp).getContext(); await this.trace(message, spanContext, state); return Promise.resolve(this); } /** * Finishes the trace by adding finish timestamp to the current span. * @param finishTimestamp optional parameter for the finish time. If omitted, current time is used. */ _finishSpan(finishTimestamp) { let newContext = Object.assign({}, this.spanContext); if (finishTimestamp instanceof Date) { newContext.finishTimestamp = finishTimestamp.toISOString(); // ISO 8601 } else if (!finishTimestamp) { newContext.finishTimestamp = (new Date()).toISOString(); // ISO 8601 } else { newContext.finishTimestamp = finishTimestamp; } this.spanContext = Object.freeze(new EventMessage_1.EventTraceMetadata(newContext)); return this; } /** * Sends trace message to the tracing framework * @param message * @param spanContext optional parameter. Can be used to trace previous span. If not set, the current span context is used. * @param action optional parameter for action. Defaults to 'span' * @param state optional parameter for state. Defaults to 'success' */ async trace(message, spanContext = this.spanContext, state, action) { if (!message) { message = new EventMessage_1.EventMessage({ type: 'application/json', content: spanContext }); } await this.recordMessage(message, EventMessage_1.TraceEventTypeAction.getType(), action, state); this.isFinished = this.spanContext.finishTimestamp ? true : false; return this; } /** * Sends audit type message to the event logging framework. * @param message message to be recorded as audit event * @param action optional parameter for action. Defaults to 'default' * @param state optional parameter for state. Defaults to 'success' */ async audit(message, action = EventMessage_1.AuditEventAction.default, state) { let result = await this.recordMessage(message, EventMessage_1.AuditEventTypeAction.getType(), action, state); return result; } /** * Logs INFO type message. * @param message if message is a string, the message is added to a message property of context of an event message. * If message is not following the event framework message format, the message is added as it is to the context of an event message. * If message follows the event framework message format, only the metadata is updated and if message lacks an UUID it is created. * @param state optional parameter for state. Defaults to 'success' */ async info(message, state) { let { action, type } = new EventMessage_1.LogEventTypeAction(EventMessage_1.LogEventAction.info); await this.recordMessage(message, type, action, state); } /** * Logs DEBUG type message. * @param message if message is a string, the message is added to a message property of context of an event message. * If message is not following the event framework message format, the message is added as it is to the context of an event message. * If message follows the event framework message format, only the metadata is updated and if message lacks an UUID it is created. * @param state optional parameter for state. Defaults to 'success' */ async debug(message, state) { let { action, type } = new EventMessage_1.LogEventTypeAction(EventMessage_1.LogEventAction.debug); await this.recordMessage(message, type, action, state); } /** * Logs VERBOSE type message. * @param message if message is a string, the message is added to a message property of context of an event message. * If message is not following the event framework message format, the message is added as it is to the context of an event message. * If message follows the event framework message format, only the metadata is updated and if message lacks an UUID it is created. * @param state optional parameter for state. Defaults to 'success' */ async verbose(message, state) { let { action, type } = new EventMessage_1.LogEventTypeAction(EventMessage_1.LogEventAction.verbose); await this.recordMessage(message, type, action, state); } /** * Logs PERFORMANCE type message. * @param message if message is a string, the message is added to a message property of context of an event message. * If message is not following the event framework message format, the message is added as it is to the context of an event message. * If message follows the event framework message format, only the metadata is updated and if message lacks an UUID it is created. * @param state optional parameter for state. Defaults to 'success' */ async performance(message, state) { let { action, type } = new EventMessage_1.LogEventTypeAction(EventMessage_1.LogEventAction.performance); await this.recordMessage(message, type, action, state); } /** * Logs WARNING type message. * @param message if message is a string, the message is added to a message property of context of an event message. * If message is not following the event framework message format, the message is added as it is to the context of an event message. * If message follows the event framework message format, only the metadata is updated and if message lacks an UUID it is created. * @param state optional parameter for state. Defaults to 'success' */ async warning(message, state) { let { action, type } = new EventMessage_1.LogEventTypeAction(EventMessage_1.LogEventAction.warning); await this.recordMessage(message, type, action, state); } /** * Logs ERROR type message. * @param message if message is a string, the message is added to a message property of context of an event message. * If message is not following the event framework message format, the message is added as it is to the context of an event message. * If message follows the event framework message format, only the metadata is updated and if message lacks an UUID it is created. * @param state optional parameter for state. Defaults to 'success' */ async error(message, state) { let { action, type } = new EventMessage_1.LogEventTypeAction(EventMessage_1.LogEventAction.error); await this.recordMessage(message, type, action, state); } /** * Sends Event message to recorders * @param message the Event message that needs to be recorded * @param type type of Event * @param action optional parameter for action. The default is based on type defaults * @param state optional parameter for state. Defaults to 'success' */ async recordMessage(message, type, action, state) { try { if (this.isFinished) { throw new Error('span finished. no further actions allowed'); } let newEnvelope = this.createEventMessage(message, type, action, state); let key = `${type}Recorder`; let recorder = this.recorders.defaultRecorder; if (this.recorders[key]) { recorder = this.recorders[key]; } if (util_1.default.shouldOverrideEvent(asyncOverrides, type)) { //Don't wait for .record() to resolve, return straight away recorder.record(newEnvelope, util_1.default.shouldLogToConsole(type, action)); return true; } const logResult = await recorder.record(newEnvelope, util_1.default.shouldLogToConsole(type, action)); if (logResult.status !== EventMessage_1.LogResponseStatus.accepted) { throw new Error(`Error when recording ${type}-${action} event. status: ${logResult.status}`); } return logResult; } catch (err) { Logger.error(`error in span.recordMessage: ${err?.stack}`); } } /** * Helper function to create event message, based on message and event types, action and state. */ createEventMessage = (message, type, _action, state = EventMessage_1.EventStateMetadata.success()) => { let defaults = getDefaults(type); let action = _action ? _action : defaults.action; let messageToLog; if (message instanceof Error) { // const callsites = ErrorCallsites(message) // message.__error_callsites = callsites messageToLog = new EventMessage_1.EventMessage({ content: { error: (0, serialize_error_1.serializeError)(message) }, type: 'application/json' }); } else if (typeof message === 'string') { messageToLog = new EventMessage_1.EventMessage({ content: { payload: message }, type: 'application/json' }); } else { // if ((typeof message === 'object') && (!(message.hasOwnProperty('content')) || !(message.hasOwnProperty('type')))) { messageToLog = new EventMessage_1.EventMessage({ content: message, type: 'application/json' }); // } else { // messageToLog = new EventMessage(<TypeEventMessage>message) } return Object.assign(messageToLog, { metadata: { event: defaults.eventMetadataCreator({ action, state }), trace: this.spanContext } }); }; } exports.Span = Span; const getDefaults = (type) => { switch (type) { case EventMessage_1.EventType.audit: { return { action: EventMessage_1.AuditEventAction.default, eventMetadataCreator: EventMessage_1.EventMetadata.audit }; } case EventMessage_1.EventType.trace: { return { action: EventMessage_1.TraceEventAction.span, eventMetadataCreator: EventMessage_1.EventMetadata.trace }; } case EventMessage_1.EventType.log: { return { action: EventMessage_1.LogEventAction.info, eventMetadataCreator: EventMessage_1.EventMetadata.log }; } } return { action: EventMessage_1.NullEventAction.undefined, eventMetadataCreator: EventMessage_1.EventMetadata.log }; }; const setHttpHeader = (context, type, headers) => { const { traceId, parentSpanId, spanId, flags, sampled } = context; switch (type) { case EventMessage_1.HttpRequestOptions.xb3: { let XB3headers = { 'X-B3-TraceId': traceId, 'X-B3-SpanId': spanId, 'X-B3-Sampled': sampled, 'X-B3-Flags': flags, 'X-B3-Version': '0' }; let result = parentSpanId ? Object.assign({ 'X-B3-ParentSpanId': parentSpanId }, XB3headers) : XB3headers; return Object.assign(headers, result); } case EventMessage_1.HttpRequestOptions.w3c: default: { const tracestate = headers.tracestate ? createW3CTracestate(context, headers.tracestate) : (context.tags && context.tags.tracestate) ? context.tags.tracestate : null; return lodash_1.default.pickBy({ ...headers, ...{ traceparent: createW3Ctreaceparent(context), tracestate } }, lodash_1.default.identity); } } }; exports.setHttpHeader = setHttpHeader; const encodeTracestate = (context) => { const { spanId } = context; let tracestatesMap = {}; tracestatesMap[config_1.default.EVENT_LOGGER_VENDOR_PREFIX] = {}; let ownTraceStateString = ''; let restTraceStateString = ''; if ((!!context.tags && !!context.tags.tracestate)) { const { tracestates, ownTraceState, restTraceState } = util_1.default.getTracestateMap(config_1.default.EVENT_LOGGER_VENDOR_PREFIX, context.tags.tracestate); tracestatesMap = tracestates; ownTraceStateString = ownTraceState; restTraceStateString = restTraceState; } if (context.tracestates && context.tracestates[config_1.default.EVENT_LOGGER_VENDOR_PREFIX]) tracestatesMap = context.tracestates; const newOpaqueValueMap = ((typeof tracestatesMap[config_1.default.EVENT_LOGGER_VENDOR_PREFIX]) === 'object') ? Object.assign(tracestatesMap[config_1.default.EVENT_LOGGER_VENDOR_PREFIX], { spanId }) : null; let opaqueValue = newOpaqueValueMap ? stringify(newOpaqueValueMap) : `{"spanId":"${spanId}"}`; return { ownTraceStateString: `${config_1.default.EVENT_LOGGER_VENDOR_PREFIX}=${Buffer.from(opaqueValue).toString('base64')}`, restTraceStateString }; }; const createW3CTracestate = (spanContext, tracestate) => { const newTracestate = encodeTracestate(spanContext).ownTraceStateString; if (!tracestate && config_1.default.EVENT_LOGGER_TRACESTATE_HEADER_ENABLED) { return newTracestate; } let tracestateArray = (tracestate.split(',')); let resultMap = new Map(); let resultArray = []; let result; for (let rawStates of tracestateArray) { let states = rawStates.trim(); let [vendorRaw] = states.split('='); resultMap.set(vendorRaw.trim(), states); } if (resultMap.has(config_1.default.EVENT_LOGGER_VENDOR_PREFIX)) { resultMap.delete(config_1.default.EVENT_LOGGER_VENDOR_PREFIX); for (let entry of resultMap.values()) { resultArray.push(entry); } resultArray.unshift(newTracestate); result = resultArray.join(','); } else { tracestateArray.unshift(newTracestate); result = tracestateArray.join(','); } return result; }; exports.createW3CTracestate = createW3CTracestate; const createW3Ctreaceparent = (spanContext) => { const { traceId, parentSpanId, spanId, flags, sampled } = spanContext; const version = Buffer.alloc(1).fill(0); const flagsForBuff = (flags && sampled) ? (flags | sampled) : flags ? flags : sampled ? sampled : 0x00; const flagsBuffer = Buffer.alloc(1).fill(flagsForBuff); const traceIdBuff = Buffer.from(traceId, 'hex'); const spanIdBuff = Buffer.from(spanId, 'hex'); const parentSpanIdBuff = parentSpanId && Buffer.from(parentSpanId, 'hex'); let W3CHeaders = parentSpanIdBuff ? new traceparent_1.default(Buffer.concat([version, traceIdBuff, spanIdBuff, flagsBuffer, parentSpanIdBuff])) : new traceparent_1.default(Buffer.concat([version, traceIdBuff, spanIdBuff, flagsBuffer])); return W3CHeaders.toString(); }; const getTracestate = (spanContext) => { let tracestate; if (!!config_1.default.EVENT_LOGGER_TRACESTATE_HEADER_ENABLED || (!!spanContext.tags && !!spanContext.tags.tracestate)) { let currentTracestate = undefined; if (!!spanContext.tags && !!spanContext.tags.tracestate) currentTracestate = spanContext.tags.tracestate; tracestate = createW3CTracestate(spanContext, currentTracestate); return { tracestate }; } return false; }; //# sourceMappingURL=Span.js.map