@loglayer/transport-new-relic
Version:
New Relic transport for the LogLayer logging library.
239 lines (229 loc) • 8.71 kB
JavaScript
;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