UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

237 lines (236 loc) 8.42 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OtelHttpExporter = void 0; const deep_1 = require("@valkyriestudios/utils/deep"); const function_1 = require("@valkyriestudios/utils/function"); const number_1 = require("@valkyriestudios/utils/number"); const Scrambler_1 = require("../../../utils/Scrambler"); const LEVELSMAP = { debug: 'DEBUG', error: 'ERROR', info: 'INFO', log: 'LOG', warn: 'WARN', }; function convertObjectToAttributes(obj, prefix = '') { const acc = []; for (const key in obj) { const val = obj[key]; switch (typeof val) { case 'string': acc.push({ key: prefix + key, value: { stringValue: val } }); break; case 'number': if (Number.isFinite(val)) { acc.push({ key: prefix + key, value: Number.isInteger(val) ? { intValue: val } : { doubleValue: val } }); } break; case 'boolean': acc.push({ key: prefix + key, value: { boolValue: val } }); break; default: if (Object.prototype.toString.call(val) === '[object Object]' || Array.isArray(val)) { acc.push({ key: prefix + key, value: { stringValue: JSON.stringify(val) } }); } break; } } return acc; } class OtelHttpExporter { logEndpoint; spanEndpoint; headers; /** * Internal buffer for logs */ buffer = []; /** * Internal buffer for spans */ spanBuffer = []; /** * Max size per batch that we send through to source system */ maxBatchSize = 20; /** * Max internal buffer size */ maxBufferSize = 10_000; /** * Max retries when sending batch to source system */ maxRetries = 3; resourceAttributes = []; /** * Scrambler based on omit pattern provided */ scramble; constructor(options) { this.logEndpoint = options.logEndpoint; this.spanEndpoint = options.spanEndpoint || options.logEndpoint; this.headers = { 'Content-Type': 'application/json', ...(options.headers || {}), }; /* Configure max batch size */ if ((0, number_1.isIntGt)(options.maxBatchSize, 0)) this.maxBatchSize = options.maxBatchSize; /* Configure max buffer size */ if ((0, number_1.isIntGt)(options.maxBufferSize, 0)) this.maxBufferSize = options.maxBufferSize; /* Cap max batch size to max buffer size */ if (this.maxBatchSize > this.maxBufferSize) this.maxBatchSize = this.maxBufferSize; /* Configure max retries */ if ((0, number_1.isIntGt)(options.maxRetries, 0)) this.maxRetries = options.maxRetries; /* Configure scrambler */ this.scramble = (0, Scrambler_1.createScrambler)({ checks: Array.isArray(options?.omit) ? (0, deep_1.deepFreeze)([...options.omit]) : Scrambler_1.OMIT_PRESETS.default, }); } init(trifrost) { this.resourceAttributes = convertObjectToAttributes(this.scramble(trifrost)); } async pushLog(log) { this.buffer.push(log); if (this.buffer.length >= this.maxBatchSize) await this.flushLogs(); } async pushSpan(span) { this.spanBuffer.push(span); if (this.spanBuffer.length >= this.maxBatchSize) await this.flushSpans(); } async flush() { await Promise.all([this.flushLogs(), this.flushSpans()]); } /** * Flushes Otel Logs */ async flushLogs() { if (this.buffer.length === 0) return; /* swap out buffer */ const batch = this.buffer; this.buffer = []; /* Convert logs */ const logRecords = []; for (let i = 0; i < batch.length; i++) { const log = batch[i]; /* Scramble sensitive values and convert to attributes */ const attributes = [ ...(log.ctx ? convertObjectToAttributes(this.scramble(log.ctx), 'ctx.') : []), ...(log.data ? convertObjectToAttributes(this.scramble(log.data), 'data.') : []), ]; if (log.trace_id) attributes.push({ key: 'trace_id', value: { stringValue: log.trace_id } }); if (log.span_id) attributes.push({ key: 'span_id', value: { stringValue: log.span_id } }); logRecords.push({ timeUnixNano: log.time.getTime() * 1_000_000, severityText: LEVELSMAP[log.level], body: this.scramble({ stringValue: log.message }), attributes, }); } const success = await this.sendWithRetry(this.logEndpoint, { resourceLogs: [ { resource: { attributes: this.resourceAttributes, }, scopeLogs: [ { scope: { name: 'trifrost.logger', version: '1.0.0' }, logRecords, }, ], }, ], }); /* If failed, requeue batch */ if (!success) { const newSize = this.buffer.length + batch.length; /* Only add if new size does not go over buffer max size */ if (newSize <= this.maxBufferSize) this.buffer.unshift(...batch); } } /** * Flushes Otel Spans */ async flushSpans() { if (this.spanBuffer.length === 0) return; /* swap out buffer */ const batch = this.spanBuffer; this.spanBuffer = []; /* Convert to otel format */ const otelSpans = []; for (let i = 0; i < batch.length; i++) { const span = batch[i]; otelSpans.push({ name: span.name, traceId: span.traceId, spanId: span.spanId, startTimeUnixNano: span.start * 1_000_000, endTimeUnixNano: span.end * 1_000_000, attributes: convertObjectToAttributes(this.scramble(span.ctx)), ...(span.parentSpanId && { parentSpanId: span.parentSpanId }), ...(span.status && { status: span.status }), }); } const success = await this.sendWithRetry(this.spanEndpoint, { resourceSpans: [ { resource: { attributes: this.resourceAttributes, }, scopeSpans: [ { scope: { name: 'trifrost.logger', version: '1.0.0' }, spans: otelSpans, }, ], }, ], }); /* If failed, requeue batch */ if (!success) { const newSize = this.spanBuffer.length + batch.length; /* Only add if new size does not go over buffer max size */ if (newSize <= this.maxBufferSize) this.spanBuffer.unshift(...batch); } } async sendWithRetry(endpoint, body) { let attempt = 0; let delay = 100; while (attempt < this.maxRetries) { try { const res = await globalThis.fetch(endpoint, { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); if (res.ok) return true; throw new Error(`Transport received HTTP ${res.status}`); } catch (err) { attempt++; if (attempt >= this.maxRetries) { console.error('[Logger] Transport failed after retries', err); return false; /* We return true here to prevent memory overflows */ } /* Jittered exponential backoff */ const jitter = delay * 0.5 * Math.random(); await (0, function_1.sleep)(delay + jitter); delay *= 2; } } } } exports.OtelHttpExporter = OtelHttpExporter;