"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { MikroTrace: () => MikroTrace }); module.exports = __toCommonJS(src_exports); // src/entities/MikroTrace.ts var import_aws_metadata_utils = require("aws-metadata-utils"); // src/frameworks/getRandomBytes.ts var import_crypto = require("crypto"); function getRandomBytes(length) { const bytes = Math.floor(length / 2); return (0, import_crypto.randomBytes)(bytes).toString("hex"); } // src/entities/Span.ts var Span = class { tracer; configuration; constructor(input) { const { tracer } = input; this.tracer = tracer; this.configuration = this.produceSpan(input); } /** * @description Produce a `Span`. */ produceSpan(input) { const { spanName, parentSpanName, parentSpanId, parentTraceId, correlationId, service, staticMetadata, dynamicMetadata } = input; const timeNow = Date.now(); const id = getRandomBytes(16); return this.filterMetadata({ ...dynamicMetadata, ...staticMetadata, timestamp: new Date(timeNow).toISOString(), timestampEpoch: `${timeNow}`, startTime: `${timeNow}`, durationMs: 0, spanName, spanParent: parentSpanName, spanParentId: parentSpanId || "", spanId: id, traceId: parentTraceId, attributes: {}, correlationId: correlationId || "", service, isEnded: false }); } /** * @description Set a single attribute by key and value. */ setAttribute(key, value) { this.configuration["attributes"][key] = value; } /** * @description Set one or more attributes through an object. * Merges and replaces any existing keys. */ setAttributes(attributeObject) { const combinedAttributes = Object.assign(this.configuration["attributes"], attributeObject); this.configuration["attributes"] = combinedAttributes; } /** * @description Get the span's full configuration object. */ getConfiguration() { return this.configuration; } /** * @description End the trace. Perform some configuration modification * to ensure logs looks right and don't contain unnecessary information. * Finally, call the tracer so it can remove its representation of this span. */ end() { const config = this.configuration; config["durationMs"] = Math.floor(Date.now() - parseInt(config.startTime)); config["isEnded"] = true; delete config["startTime"]; if (!config["spanParentId"]) delete config["spanParentId"]; process.stdout.write(JSON.stringify(this.sortOutput(config)) + "\n"); this.tracer.removeSpan(config["spanName"]); } /** * @description Alphabetically sort the fields in the log object. */ sortOutput(input) { const sortedOutput = {}; Object.entries(input).sort().forEach(([key, value]) => sortedOutput[key] = value); return sortedOutput; } /** * @description Filter metadata from empties. */ filterMetadata(metadata) { const filteredMetadata = {}; Object.entries(metadata).forEach((entry) => { const [key, value] = entry; if (value || value === 0 || value === false) filteredMetadata[key] = value; }); return filteredMetadata; } }; // src/application/errors/errors.ts var MissingParentSpanError = class extends Error { constructor(parentSpanName) { super(parentSpanName); this.name = "MissingParentSpanError"; const message = `No parent span found by the name "${parentSpanName}"!`; this.message = message; process.stdout.write(JSON.stringify(message) + "\n"); } }; var SpanAlreadyExistsError = class extends Error { constructor(spanName) { super(spanName); this.name = "SpanAlreadyExistsError"; const message = `A span with the name "${spanName}" already exists!`; this.message = message; process.stdout.write(JSON.stringify(message) + "\n"); } }; var MissingSpanNameError = class extends Error { constructor() { super(); this.name = "MissingSpanNameError"; const message = `Missing "spanName" input when tying to create a new span!`; this.message = message; process.stdout.write(JSON.stringify(message) + "\n"); } }; // src/entities/MikroTrace.ts var MikroTrace = class _MikroTrace { static instance; static metadataConfig = {}; static serviceName; static spans; static correlationId; static parentContext = ""; static traceId; static event; static context; static samplingLevel; static isTraceSampled; constructor(event, context) { _MikroTrace.metadataConfig = {}; _MikroTrace.spans = []; _MikroTrace.serviceName = ""; _MikroTrace.correlationId = ""; _MikroTrace.parentContext = ""; _MikroTrace.traceId = getRandomBytes(32); _MikroTrace.event = event; _MikroTrace.context = context; _MikroTrace.samplingLevel = this.initSampleLevel(); _MikroTrace.isTraceSampled = true; } /** * @description This instantiates MikroTrace. In order to be able * to "remember" event and context we use a singleton pattern to * reuse the same logical instance. * * If the `start` method receives any input, that input will * overwrite any existing metadata. * * If you want to "add" to these, you should instead call * `enrich()` and pass in your additional data there. * * Running this without input will also force a new `traceId`. */ static start(input) { const serviceName = input?.serviceName || _MikroTrace.serviceName || ""; const correlationId = input?.correlationId || _MikroTrace.correlationId || ""; const parentContext = input?.parentContext || _MikroTrace.parentContext || ""; const event = input?.event || _MikroTrace.event || {}; const context = input?.context || _MikroTrace.context || {}; if (!_MikroTrace.instance) _MikroTrace.instance = new _MikroTrace(event, context); _MikroTrace.metadataConfig = input?.metadataConfig || {}; _MikroTrace.serviceName = serviceName; _MikroTrace.correlationId = correlationId; _MikroTrace.parentContext = parentContext; _MikroTrace.traceId = getRandomBytes(32); _MikroTrace.event = event; _MikroTrace.context = context; return _MikroTrace.instance; } /** * @description Returns the current instance of MikroTrace without * resetting anything or otherwise affecting the current state. */ static continue() { return _MikroTrace.instance; } /** * @description Enrich MikroTrace with values post-initialization. */ static enrich(input) { if (input.serviceName) _MikroTrace.serviceName = input.serviceName; if (input.correlationId) _MikroTrace.setCorrelationId(input.correlationId); if (input.parentContext) _MikroTrace.parentContext = input.parentContext; } /** * @description Start a new trace. This will typically be automatically * assigned to the parent trace if one exists. Optionally you can pass in * the name of a parent span to link it to its trace ID. * * @see https://docs.honeycomb.io/getting-data-in/tracing/send-trace-data/ * ``` * A root span, the first span in a trace, does not have a parent. As you * instrument your code, make sure every span propagates its `trace.trace_id` * and` trace.span_id` to any child spans it calls, so that the child span can * use those values as its `trace.trace_id` and `trace.parent_id`. Honeycomb uses * these relationships to determine the order spans execute and construct the * waterfall diagram. * ``` * * @param parentSpanName If provided, this will override any existing parent context * for this particular trace. */ start(spanName, parentSpanName) { if (!spanName) throw new MissingSpanNameError(); const spanExists = this.getSpan(spanName); if (spanExists) throw new SpanAlreadyExistsError(spanName); if (parentSpanName) { const parentSpan2 = this.getSpan(parentSpanName); if (!parentSpan2) throw new MissingParentSpanError(parentSpanName); } const dynamicMetadata = (0, import_aws_metadata_utils.getMetadata)(_MikroTrace.event, _MikroTrace.context); const parentSpan = this.getSpan(_MikroTrace.parentContext); const span = this.createSpan(spanName, dynamicMetadata, parentSpanName, parentSpan); if (this.shouldSampleTrace()) this.addSpan(span); this.setParentContext(spanName); return span; } /** * @description An emergency mechanism if you absolutely need to * reset the instance to its empty default state. */ static reset() { _MikroTrace.instance = new _MikroTrace({}, {}); } /** * @description Initialize the sample rate level. * Only accepts numbers or strings that can convert to numbers. * The default is to use all traces (i.e. `100` percent). */ initSampleLevel() { const envValue = process.env.MIKROTRACE_SAMPLE_RATE; if (envValue) { const isNumeric = !Number.isNaN(envValue) && !Number.isNaN(parseFloat(envValue)); if (isNumeric) return parseFloat(envValue); } return 100; } /** * @description Check if MicroTrace has sampled the last trace. * Will only return true value _after_ having output an actual trace. */ isTraceSampled() { return _MikroTrace.isTraceSampled; } /** * @description Set sampling rate of traces as a number between 0 and 100. */ setSamplingRate(samplingPercent) { if (typeof samplingPercent !== "number") return _MikroTrace.samplingLevel; if (samplingPercent < 0) samplingPercent = 0; if (samplingPercent > 100) samplingPercent = 100; _MikroTrace.samplingLevel = samplingPercent; return samplingPercent; } /** * @description Utility to check if a log should be sampled (written) based * on the currently set `samplingLevel`. This uses a 0-100 scale. * * If the random number is lower than (or equal to) the sampling level, * then we may sample the log. */ shouldSampleTrace() { const logWillBeSampled = Math.random() * 100 <= _MikroTrace.samplingLevel; _MikroTrace.isTraceSampled = logWillBeSampled; return logWillBeSampled; } /** * @description Set correlation ID. Make use of this if you * were not able to set the correlation ID at the point of * instantiating `MikroTrace`. * * This value will be propagated to all future spans. */ static setCorrelationId(correlationId) { _MikroTrace.correlationId = correlationId; } /** * @description Set the parent context. Use this if you * want to automatically assign a span as the parent for * any future spans. * * Call it with an empty string to reset it. * * @example tracer.setParentContext('FullSpan') * @example tracer.setParentContext('') * * This value will be propagated to all future spans. */ setParentContext(parentContext) { _MikroTrace.parentContext = parentContext; } /** * @description Output the tracer configuration, for * example for debugging needs. */ getConfiguration() { return { serviceName: _MikroTrace.serviceName, spans: _MikroTrace.spans, correlationId: _MikroTrace.correlationId, parentContext: _MikroTrace.parentContext, traceId: _MikroTrace.traceId }; } /** * @description Returns a string that can be used as the * content of a W3C `traceparent` HTTP header. * @see https://www.w3.org/TR/trace-context/#traceparent-header */ getTraceHeader(spanConfig) { const version = "00"; const traceId = _MikroTrace.traceId; const parentId = (() => { const parentSpan = this.getSpan(_MikroTrace.parentContext); return parentSpan && parentSpan.parentSpanId ? parentSpan.parentSpanId : spanConfig.spanId; })(); const traceFlags = this.isTraceSampled() ? "01" : "00"; return `${version}-${traceId}-${parentId}-${traceFlags}`; } /** * @description Request to create a valid Span. */ createSpan(spanName, dynamicMetadata, parentSpanName, parentSpan) { return new Span({ dynamicMetadata, staticMetadata: _MikroTrace.metadataConfig, tracer: this, correlationId: _MikroTrace.correlationId || dynamicMetadata.correlationId, service: _MikroTrace.serviceName || _MikroTrace.metadataConfig.service, spanName, parentSpanId: parentSpan?.spanId || "", parentSpanName: parentSpanName || parentSpan?.spanName || "", parentTraceId: _MikroTrace.traceId }); } /** * @description Store local representation so we can make lookups for relations. */ addSpan(span) { const { spanName, spanId, traceId, spanParentId } = span.getConfiguration(); _MikroTrace.spans.push({ spanName, spanId, traceId, parentSpanId: spanParentId, reference: span }); } /** * @description Get an individual span by name. */ getSpan(spanName) { const span = _MikroTrace.spans.filter((span2) => span2.spanName === spanName)[0] || null; return span; } /** * @description Get an individual span by ID. */ getSpanById(spanId) { const span = _MikroTrace.spans.filter((span2) => span2.spanId === spanId)[0] || null; return span; } /** * @description Remove an individual span. * * Avoid calling this manually as the `Span` class will * make the necessary call when having ended a span. */ removeSpan(spanName) { const parentSpanId = _MikroTrace.spans.filter( (span) => span.spanName === spanName )[0]?.parentSpanId; const parentSpan = this.getSpanById(parentSpanId)?.spanName || ""; const spans = _MikroTrace.spans.filter((span) => span.spanName !== spanName); _MikroTrace.spans = spans; this.setParentContext(parentSpan); } /** * @description Closes all spans. * * Only use this sparingly and in relevant cases, such as * when you need to close all spans in case of an error. */ endAll() { _MikroTrace.spans.forEach((spanRep) => spanRep.reference.end()); _MikroTrace.spans = []; _MikroTrace.traceId = getRandomBytes(32); this.setParentContext(""); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MikroTrace });