@mojaloop/event-sdk
Version:
Shared code for Event Logging
537 lines • 26.1 kB
JavaScript
"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