@aws/aws-distro-opentelemetry-node-autoinstrumentation
Version:
This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.
317 lines • 12.8 kB
JavaScript
"use strict";
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudWatchLogsClient = exports.LogEventBatch = void 0;
const api_1 = require("@opentelemetry/api");
const core_1 = require("@opentelemetry/core");
const Crypto = require("crypto");
/**
* Container for a batch of CloudWatch log events with metadata.
*/
class LogEventBatch {
constructor() {
this.logEvents = [];
this.byteTotal = 0;
this.minTimestampMs = 0;
this.maxTimestampMs = 0;
this.createdTimestampMs = Date.now();
}
/**
* Add a log event to the batch.
*
* @param logEvent The log event to add
* @param eventSize The byte size of the event
*/
addEvent(logEvent, eventSize) {
this.logEvents.push(logEvent);
this.byteTotal += eventSize;
// Update timestamp tracking
const timestamp = logEvent.timestamp;
if (this.minTimestampMs === 0 || timestamp < this.minTimestampMs) {
this.minTimestampMs = timestamp;
}
if (timestamp > this.maxTimestampMs) {
this.maxTimestampMs = timestamp;
}
}
/**
* Check if the batch is empty.
*
* @returns {boolean}
*/
isEmpty() {
return this.logEvents.length === 0;
}
/**
* Get the number of events in the batch
*
* @returns {number}
*/
size() {
return this.logEvents.length;
}
clear() {
this.logEvents = [];
this.byteTotal = 0;
this.minTimestampMs = 0;
this.maxTimestampMs = 0;
this.createdTimestampMs = Date.now();
}
}
exports.LogEventBatch = LogEventBatch;
/**
* CloudWatch Logs client for batching and sending log events.
*/
class CloudWatchLogsClient {
constructor(logGroupName, logStreamName, cloudwatchLogsConfig = {}) {
this.logGroupName = logGroupName;
this.logStreamName = logStreamName || this.generateLogStreamName();
// Require CloudWatchLogs Client during runtime so AWS SDK isn't
// loaded before any OpenTelemetry patching is done on AWS SDK.
const { CloudWatchLogs } = require('@aws-sdk/client-cloudwatch-logs');
this.logsClient = new CloudWatchLogs(cloudwatchLogsConfig);
}
/**
* Generate a unique log stream name.
*
* @returns {string}
*/
generateLogStreamName() {
const uniqueId = Crypto.randomUUID().substring(0, 8);
return `otel-js-${uniqueId}`;
}
/**
* Ensure the log group exists, create if it doesn't.
*/
async ensureLogGroupExists() {
try {
await api_1.context.with((0, core_1.suppressTracing)(api_1.context.active()), async () => {
await this.logsClient.createLogGroup({
logGroupName: this.logGroupName,
});
});
api_1.diag.info(`Created log group: ${this.logGroupName}`);
}
catch (e) {
if (e instanceof Error && e.name === 'ResourceAlreadyExistsException') {
api_1.diag.debug(`Log group ${this.logGroupName} already exists`);
}
else {
api_1.diag.error(`Failed to create log group ${this.logGroupName}: ${e}`);
throw e;
}
}
}
/**
* Ensure the log stream exists, create if it doesn't.
*/
async ensureLogStreamExists() {
try {
await api_1.context.with((0, core_1.suppressTracing)(api_1.context.active()), async () => {
await this.logsClient.createLogStream({
logGroupName: this.logGroupName,
logStreamName: this.logStreamName,
});
});
api_1.diag.info(`Created log stream: ${this.logStreamName}`);
}
catch (e) {
if (e instanceof Error && e.name === 'ResourceAlreadyExistsException') {
api_1.diag.debug(`Log stream ${this.logStreamName} already exists`);
}
else {
api_1.diag.error(`Failed to create log stream ${this.logStreamName}: ${e}`);
throw e;
}
}
}
/**
* Validate the log event according to CloudWatch Logs constraints.
*/
validateLogEvent(logEvent) {
if (!logEvent.message || !logEvent.message.trim()) {
api_1.diag.error('Empty log event message');
return false;
}
// Check message size
const messageSize = logEvent.message.length + CloudWatchLogsClient.CW_PER_EVENT_HEADER_BYTES;
if (messageSize > CloudWatchLogsClient.CW_MAX_EVENT_PAYLOAD_BYTES) {
api_1.diag.warn(`Log event size ${messageSize} exceeds maximum allowed size ${CloudWatchLogsClient.CW_MAX_EVENT_PAYLOAD_BYTES}. Truncating.`);
const maxMessageSize = CloudWatchLogsClient.CW_MAX_EVENT_PAYLOAD_BYTES -
CloudWatchLogsClient.CW_PER_EVENT_HEADER_BYTES -
CloudWatchLogsClient.CW_TRUNCATED_SUFFIX.length;
logEvent.message = logEvent.message.substring(0, maxMessageSize) + CloudWatchLogsClient.CW_TRUNCATED_SUFFIX;
}
// Check timestamp constraints
const currentTime = Date.now();
const timeDiff = currentTime - logEvent.timestamp;
if (timeDiff > CloudWatchLogsClient.CW_EVENT_TIMESTAMP_LIMIT_PAST ||
timeDiff < -CloudWatchLogsClient.CW_EVENT_TIMESTAMP_LIMIT_FUTURE) {
api_1.diag.error(`Log event timestamp ${logEvent.timestamp} is either older than 14 days or more than 2 hours in the future. ` +
`Current time: ${currentTime}`);
return false;
}
return true;
}
/**
* Create a new log event batch.
*/
createEventBatch() {
return new LogEventBatch();
}
/**
* Check if adding the next event would exceed CloudWatch Logs limits.
*/
eventBatchExceedsLimit(batch, nextEventSize) {
return (batch.size() >= CloudWatchLogsClient.CW_MAX_REQUEST_EVENT_COUNT ||
batch.byteTotal + nextEventSize > CloudWatchLogsClient.CW_MAX_REQUEST_PAYLOAD_BYTES);
}
/**
* Check if the event batch spans more than 24 hours.
*
* @param batch The event batch
* @param targetTimestampMs The timestamp of the event to add
* @returns {boolean} true if the batch is active and can accept the event
*/
isBatchActive(batch, targetTimestampMs) {
// New log event batch
if (batch.minTimestampMs === 0 || batch.maxTimestampMs === 0) {
return true;
}
// Check if adding the event would make the batch span more than 24 hours
if (targetTimestampMs - batch.minTimestampMs > 24 * 3600 * 1000) {
return false;
}
if (batch.maxTimestampMs - targetTimestampMs > 24 * 3600 * 1000) {
return false;
}
// Flush the event batch when reached 60s interval
const currentTime = Date.now();
if (currentTime - batch.createdTimestampMs >= CloudWatchLogsClient.BATCH_FLUSH_INTERVAL) {
return false;
}
return true;
}
/**
* Sort log events in the batch by timestamp.
*/
sortLogEvents(batch) {
batch.logEvents.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* Send a batch of log events to CloudWatch Logs.
*/
async sendLogBatch(batch) {
if (batch.isEmpty()) {
return;
}
this.sortLogEvents(batch);
const putLogEventsInput = {
logGroupName: this.logGroupName,
logStreamName: this.logStreamName,
logEvents: batch.logEvents,
};
const startTime = Date.now();
try {
const response = await api_1.context.with((0, core_1.suppressTracing)(api_1.context.active()), async () => {
const res = await this.logsClient.putLogEvents(putLogEventsInput);
const elapsedMs = Date.now() - startTime;
api_1.diag.debug(`Successfully sent ${batch.size()} log events ` +
`(${(batch.byteTotal / 1024).toFixed(2)} KB) in ${elapsedMs} ms`);
return res;
});
return response;
}
catch (e) {
// Handle resource not found errors by creating log group/stream
if (e instanceof Error && e.name === 'ResourceNotFoundException') {
api_1.diag.info('Log group or stream not found, creating resources and retrying');
try {
// Create log group first then log stream
await this.ensureLogGroupExists();
await this.ensureLogStreamExists();
// Retry the PutLogEvents call
const response = await api_1.context.with((0, core_1.suppressTracing)(api_1.context.active()), async () => {
const res = await this.logsClient.putLogEvents(putLogEventsInput);
const elapsedMs = Date.now() - startTime;
api_1.diag.debug(`Successfully sent ${batch.size()} log events ` +
`(${(batch.byteTotal / 1024).toFixed(2)} KB) in ${elapsedMs} ms after creating resources`);
return res;
});
return response;
}
catch (e) {
api_1.diag.error(`Failed to create log resources or failed to send log events: ${e}`);
throw e;
}
}
else {
api_1.diag.error(`Failed to send log events: ${e}`);
throw e;
}
}
}
/**
* Send a log event to CloudWatch Logs.
*
* This function implements the same logic as the Go version in the OTel Collector.
* It batches log events according to CloudWatch Logs constraints and sends them
* when the batch is full or spans more than 24 hours.
*/
async sendLogEvent(logEvent) {
try {
if (!this.validateLogEvent(logEvent)) {
return;
}
// Calculate event size
const eventSize = logEvent.message.length + CloudWatchLogsClient.CW_PER_EVENT_HEADER_BYTES;
// Initialize event batch if needed
if (!this.eventBatch) {
this.eventBatch = this.createEventBatch();
}
// Check if we need to send the current batch and create a new one
let currentBatch = this.eventBatch;
if (this.eventBatchExceedsLimit(currentBatch, eventSize) ||
!this.isBatchActive(currentBatch, logEvent.timestamp)) {
// Send the current batch
await this.sendLogBatch(currentBatch);
// Create a new batch
this.eventBatch = this.createEventBatch();
currentBatch = this.eventBatch;
}
// Add the log event to the batch
currentBatch.addEvent(logEvent, eventSize);
}
catch (e) {
api_1.diag.error(`Failed to process log event: ${e}`);
throw e;
}
}
/**
* Force flush any pending log events.
*/
async flushPendingEvents() {
if (this.eventBatch && !this.eventBatch.isEmpty()) {
const currentBatch = this.eventBatch;
this.eventBatch = this.createEventBatch();
await this.sendLogBatch(currentBatch);
}
api_1.diag.debug('CloudWatchLogsClient flushed the buffered log events');
}
}
// Constants for CloudWatch Logs limits
// http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html
// http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
CloudWatchLogsClient.CW_MAX_EVENT_PAYLOAD_BYTES = 256 * 1024; // 256KB
CloudWatchLogsClient.CW_MAX_REQUEST_EVENT_COUNT = 10000;
CloudWatchLogsClient.CW_PER_EVENT_HEADER_BYTES = 26;
CloudWatchLogsClient.BATCH_FLUSH_INTERVAL = 60 * 1000;
CloudWatchLogsClient.CW_MAX_REQUEST_PAYLOAD_BYTES = 1 * 1024 * 1024; // 1MB
CloudWatchLogsClient.CW_TRUNCATED_SUFFIX = '[Truncated...]';
// None of the log events in the batch can be older than 14 days
CloudWatchLogsClient.CW_EVENT_TIMESTAMP_LIMIT_PAST = 14 * 24 * 60 * 60 * 1000;
// None of the log events in the batch can be more than 2 hours in the future.
CloudWatchLogsClient.CW_EVENT_TIMESTAMP_LIMIT_FUTURE = 2 * 60 * 60 * 1000;
exports.CloudWatchLogsClient = CloudWatchLogsClient;
//# sourceMappingURL=cloudwatch-logs-client.js.map