logs-interceptor
Version:
High-performance, production-ready log interceptor for Node.js applications with Loki integration. Built with Clean Architecture principles. Supports Node.js, Browser, and Node-RED.
153 lines • 5.26 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LokiTransport = void 0;
const axios_1 = __importDefault(require("axios"));
const perf_hooks_1 = require("perf_hooks");
class LokiTransport {
constructor(config, circuitBreaker) {
this.config = config;
this.circuitBreaker = circuitBreaker;
this.consecutiveFailures = 0;
this.health = {
healthy: true,
consecutiveFailures: 0,
};
this.setupHttpClient();
}
setupHttpClient() {
const headers = {
'Content-Type': 'application/json',
'X-Scope-OrgID': this.config.tenantId,
'User-Agent': 'logs-interceptor/2.0.0',
};
if (this.config.authToken) {
headers['Authorization'] = `Bearer ${this.config.authToken}`;
}
this.httpClient = axios_1.default.create({
baseURL: this.config.url,
timeout: this.config.timeout ?? 5000,
headers,
maxContentLength: 100 * 1024 * 1024,
maxBodyLength: 100 * 1024 * 1024,
});
this.httpClient.interceptors.request.use((config) => {
config.metadata = { startTime: perf_hooks_1.performance.now() };
return config;
});
}
async send(entries) {
if (entries.length === 0) {
return;
}
const operation = async () => {
const payload = this.formatForLoki(entries);
let data = JSON.stringify(payload);
const headers = {};
if (this.config.compression) {
const zlib = require('zlib');
data = zlib.gzipSync(data, {
level: this.config.compressionLevel ?? 6,
});
headers['Content-Encoding'] = 'gzip';
}
await this.httpClient.post('', data, { headers });
};
if (this.circuitBreaker) {
await this.circuitBreaker.execute(operation);
}
else {
await this.retryOperation(operation);
}
this.recordSuccess();
}
async retryOperation(operation, maxRetries = this.config.maxRetries ?? 3, delay = this.config.retryDelay ?? 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
}
catch (error) {
lastError = error;
if (attempt === maxRetries) {
throw lastError;
}
const backoffDelay = delay * Math.pow(2, attempt - 1);
const jitter = Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, backoffDelay + jitter));
}
}
throw lastError;
}
formatForLoki(entries) {
const streamMap = new Map();
entries.forEach((entry) => {
const streamKey = JSON.stringify(entry.labels ?? {});
const timestamp = String(Date.parse(entry.timestamp) * 1000000);
const logData = {
id: entry.id,
level: entry.level,
message: entry.message,
context: entry.context,
};
if (entry.traceId && entry.traceId !== 'undefined') {
logData.traceId = entry.traceId;
}
if (entry.spanId && entry.spanId !== 'undefined') {
logData.spanId = entry.spanId;
}
if (entry.requestId && entry.requestId !== 'undefined') {
logData.requestId = entry.requestId;
}
if (entry.metadata) {
logData.metadata = entry.metadata;
}
const logLine = JSON.stringify(logData);
if (!streamMap.has(streamKey)) {
streamMap.set(streamKey, []);
}
streamMap.get(streamKey).push([timestamp, logLine]);
});
return {
streams: Array.from(streamMap.entries()).map(([streamKey, values]) => ({
stream: JSON.parse(streamKey),
values: values.sort((a, b) => a[0].localeCompare(b[0])),
})),
};
}
async isAvailable() {
try {
// Simple health check - could be improved
return this.health.healthy;
}
catch {
return false;
}
}
getHealth() {
return { ...this.health };
}
recordSuccess() {
this.consecutiveFailures = 0;
this.health = {
healthy: true,
consecutiveFailures: 0,
lastSuccessfulSend: Date.now(),
};
}
recordFailure(error) {
this.consecutiveFailures++;
this.health = {
healthy: false,
consecutiveFailures: this.consecutiveFailures,
errorMessage: error.message,
};
}
async destroy() {
// Cleanup if needed
}
}
exports.LokiTransport = LokiTransport;
//# sourceMappingURL=LokiTransport.js.map