@logtape/cloudwatch-logs
Version:
AWS CloudWatch Logs sink for LogTape
114 lines (112 loc) • 3.96 kB
JavaScript
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
const __aws_sdk_client_cloudwatch_logs = require_rolldown_runtime.__toESM(require("@aws-sdk/client-cloudwatch-logs"));
const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape"));
//#region src/sink.ts
const MAX_BATCH_SIZE_EVENTS = 1e4;
const MAX_BATCH_SIZE_BYTES = 1048576;
const OVERHEAD_PER_EVENT = 26;
/**
* Gets a CloudWatch Logs sink that sends log records to AWS CloudWatch Logs.
*
* @param options Configuration options for the CloudWatch Logs sink.
* @returns A sink that sends log records to CloudWatch Logs.
* @since 1.0.0
*/
function getCloudWatchLogsSink(options) {
const ownClient = options.client == null;
const client = options.client ?? new __aws_sdk_client_cloudwatch_logs.CloudWatchLogsClient({
region: options.region ?? "us-east-1",
credentials: options.credentials
});
const batchSize = Math.min(Math.max(options.batchSize ?? 1e3, 1), MAX_BATCH_SIZE_EVENTS);
const flushInterval = options.flushInterval ?? 1e3;
const maxRetries = Math.max(options.maxRetries ?? 3, 0);
const retryDelay = Math.max(options.retryDelay ?? 100, 0);
const defaultFormatter = (record) => {
let result = "";
for (let i = 0; i < record.message.length; i++) if (i % 2 === 0) result += record.message[i];
else result += JSON.stringify(record.message[i]);
return result;
};
const formatter = options.formatter ?? defaultFormatter;
const logEvents = [];
let currentBatchSize = 0;
let flushTimer = null;
let disposed = false;
function scheduleFlush() {
if (flushInterval <= 0 || flushTimer !== null) return;
flushTimer = setTimeout(() => {
flushTimer = null;
if (logEvents.length > 0) flushEvents();
}, flushInterval);
}
async function flushEvents() {
if (logEvents.length === 0 || disposed) return;
const events = logEvents.splice(0);
currentBatchSize = 0;
if (flushTimer !== null) {
clearTimeout(flushTimer);
flushTimer = null;
}
await sendEventsWithRetry(events, maxRetries);
}
async function sendEventsWithRetry(events, remainingRetries) {
try {
const command = new __aws_sdk_client_cloudwatch_logs.PutLogEventsCommand({
logGroupName: options.logGroupName,
logStreamName: options.logStreamName,
logEvents: events
});
await client.send(command);
} catch (error) {
if (remainingRetries > 0) {
const attemptNumber = maxRetries - remainingRetries;
const delay = retryDelay * Math.pow(2, attemptNumber);
await new Promise((resolve) => setTimeout(resolve, delay));
await sendEventsWithRetry(events, remainingRetries - 1);
} else {
const metaLogger = (0, __logtape_logtape.getLogger)([
"logtape",
"meta",
"cloudwatch-logs"
]);
metaLogger.error("Failed to send log events to CloudWatch Logs after {maxRetries} retries: {error}", {
maxRetries,
error
});
}
}
}
function formatLogMessage(record) {
return formatter(record);
}
const sink = (record) => {
if (disposed) return;
if (record.category[0] === "logtape" && record.category[1] === "meta" && record.category[2] === "cloudwatch-logs") return;
const message = formatLogMessage(record);
const messageBytes = new TextEncoder().encode(message).length;
const eventSize = messageBytes + OVERHEAD_PER_EVENT;
const logEvent = {
timestamp: record.timestamp,
message
};
logEvents.push(logEvent);
currentBatchSize += eventSize;
const shouldFlushBySize = currentBatchSize > MAX_BATCH_SIZE_BYTES;
const shouldFlushByCount = logEvents.length >= batchSize;
if (shouldFlushBySize || shouldFlushByCount) flushEvents();
else scheduleFlush();
};
sink[Symbol.asyncDispose] = async () => {
if (flushTimer !== null) {
clearTimeout(flushTimer);
flushTimer = null;
}
await flushEvents();
disposed = true;
if (ownClient) client.destroy();
};
return sink;
}
//#endregion
exports.getCloudWatchLogsSink = getCloudWatchLogsSink;