UNPKG

@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
"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