UNPKG

@loglayer/transport-new-relic

Version:

New Relic transport for the LogLayer logging library.

239 lines (229 loc) 8.71 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/NewRelicTransport.ts var _transport = require('@loglayer/transport'); var MAX_PAYLOAD_SIZE = 1e6; var MAX_ATTRIBUTES = 255; var MAX_ATTRIBUTE_NAME_LENGTH = 255; var MAX_ATTRIBUTE_VALUE_LENGTH = 4094; var ValidationError = class extends Error { constructor(message) { super(message); this.name = "ValidationError"; } }; var RateLimitError = class extends Error { constructor(message, retryAfter) { super(message); this.retryAfter = retryAfter; this.name = "RateLimitError"; } }; function validateLogEntry(logEntry) { if (logEntry.attributes) { const attributeCount = Object.keys(logEntry.attributes).length; if (attributeCount > MAX_ATTRIBUTES) { throw new ValidationError( `Log entry exceeds maximum number of attributes (${MAX_ATTRIBUTES}). Found: ${attributeCount}` ); } for (const [key, value] of Object.entries(logEntry.attributes)) { if (key.length > MAX_ATTRIBUTE_NAME_LENGTH) { throw new ValidationError( `Attribute name '${key}' exceeds maximum length (${MAX_ATTRIBUTE_NAME_LENGTH}). Length: ${key.length}` ); } if (typeof value === "string" && value.length > MAX_ATTRIBUTE_VALUE_LENGTH) { logEntry.attributes[key] = value.slice(0, MAX_ATTRIBUTE_VALUE_LENGTH); } } } return logEntry; } var NewRelicTransport = class extends _transport.LoggerlessTransport { /** * Creates a new instance of NewRelicTransport. * * @param config - Configuration options for the transport * @param config.apiKey - New Relic API key for authentication * @param config.endpoint - Optional custom endpoint URL (defaults to New Relic's Log API endpoint) * @param config.onError - Optional error callback for handling errors * @param config.onDebug - Optional callback for debugging log entries before they are sent * @param config.useCompression - Whether to use gzip compression (defaults to true) * @param config.maxRetries - Maximum number of retry attempts (defaults to 3) * @param config.retryDelay - Base delay between retries in milliseconds (defaults to 1000) * @param config.respectRateLimit - Whether to honor rate limiting headers (defaults to true) */ constructor(config) { super(config); this.apiKey = config.apiKey; this.endpoint = _nullishCoalesce(config.endpoint, () => ( "https://log-api.newrelic.com/log/v1")); this.onError = config.onError; this.onDebug = config.onDebug; this.useCompression = _nullishCoalesce(config.useCompression, () => ( true)); this.maxRetries = _nullishCoalesce(config.maxRetries, () => ( 3)); this.retryDelay = _nullishCoalesce(config.retryDelay, () => ( 1e3)); this.respectRateLimit = _nullishCoalesce(config.respectRateLimit, () => ( true)); } /** * Processes and ships log entries to New Relic. * * This method: * 1. Validates the message size * 2. Creates and validates the log entry * 3. Validates the final payload size * 4. Asynchronously sends the log entry to New Relic * * The actual sending is done asynchronously in a fire-and-forget manner to maintain * compatibility with the base transport class while still providing retry and error handling. * * @param params - Log parameters including level, messages, and metadata * @param params.logLevel - The severity level of the log * @param params.messages - Array of message strings to be joined * @param params.data - Optional metadata to include with the log * @param params.hasData - Whether metadata is present * @returns The original messages array * @throws {ValidationError} If the payload exceeds size limits or validation fails */ shipToLogger({ logLevel, messages, data, hasData }) { try { const message = messages.join(" "); const messageBytes = new TextEncoder().encode(message).length; if (messageBytes > MAX_PAYLOAD_SIZE) { throw new ValidationError( `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${messageBytes} bytes` ); } const logEntry = { timestamp: Date.now(), level: logLevel, log: message }; if (data && hasData) { Object.assign(logEntry, { attributes: data }); } const validatedEntry = validateLogEntry(logEntry); if (this.onDebug) { this.onDebug(validatedEntry); } const payload = JSON.stringify([validatedEntry]); const payloadBytes = new TextEncoder().encode(payload).length; if (payloadBytes > MAX_PAYLOAD_SIZE) { throw new ValidationError( `Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes` ); } (async () => { try { await sendWithRetry( this.endpoint, this.apiKey, payload, this.useCompression, this.maxRetries, this.retryDelay, this.respectRateLimit ); } catch (error) { if (this.onError) { this.onError(error instanceof Error ? error : new Error(String(error))); } if (error instanceof ValidationError) { throw error; } } })(); } catch (error) { if (this.onError) { this.onError(error instanceof Error ? error : new Error(String(error))); } } return messages; } }; async function compressData(data) { const stream = new CompressionStream("gzip"); const writer = stream.writable.getWriter(); const encoder = new TextEncoder(); const chunks = []; await writer.write(encoder.encode(data)); await writer.close(); const reader = stream.readable.getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; chunks.push(value); } const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; } async function sendWithRetry(endpoint, apiKey, payload, useCompression, maxRetries, retryDelay, respectRateLimit = true) { const payloadBytes = new TextEncoder().encode(payload).length; if (payloadBytes > MAX_PAYLOAD_SIZE) { throw new ValidationError(`Payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${payloadBytes} bytes`); } let lastError; let compressedPayload; if (useCompression) { compressedPayload = await compressData(payload); if (compressedPayload.length > MAX_PAYLOAD_SIZE) { throw new ValidationError( `Compressed payload size exceeds maximum of ${MAX_PAYLOAD_SIZE} bytes. Size: ${compressedPayload.length} bytes` ); } } const headers = { "Content-Type": "application/json", "Api-Key": apiKey }; if (useCompression) { headers["Content-Encoding"] = "gzip"; } for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetch(endpoint, { method: "POST", headers, body: useCompression ? compressedPayload : payload }); if (response.status === 429) { const retryAfter = Number.parseInt(response.headers.get("Retry-After") || "60", 10); if (respectRateLimit) { await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3)); attempt--; continue; } throw new RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds`, retryAfter); } if (!response.ok) { throw new Error(`Failed to send logs to New Relic: ${response.statusText}`); } return response; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (error instanceof ValidationError) { throw error; } if (!respectRateLimit && error instanceof RateLimitError) { throw error; } if (attempt === maxRetries) { throw new Error(`Failed to send logs after ${maxRetries} retries: ${lastError.message}`); } if (!(error instanceof RateLimitError)) { const jitter = Math.random() * 200; const delay = retryDelay * 2 ** attempt + jitter; await new Promise((resolve) => setTimeout(resolve, delay)); } } } throw lastError; } exports.NewRelicTransport = NewRelicTransport; //# sourceMappingURL=index.cjs.map