UNPKG

enhanced-adot-node-autoinstrumentation

Version:

This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.

871 lines (834 loc) 39.3 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.createEmfExporter = exports.CloudWatchEMFExporter = exports.RECORD_DATA_TYPES = exports.CW_EVENT_TIMESTAMP_LIMIT_FUTURE = exports.CW_EVENT_TIMESTAMP_LIMIT_PAST = exports.CW_TRUNCATED_SUFFIX = exports.CW_MAX_REQUEST_PAYLOAD_BYTES = exports.BATCH_FLUSH_INTERVAL = exports.CW_PER_EVENT_HEADER_BYTES = exports.CW_MAX_REQUEST_EVENT_COUNT = exports.CW_MAX_EVENT_PAYLOAD_BYTES = void 0; /** * OpenTelemetry EMF (Embedded Metric Format) Exporter for CloudWatch. * This exporter converts OTel metrics into CloudWatch EMF format. */ const api_1 = require("@opentelemetry/api"); const client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs"); const sdk_metrics_1 = require("@opentelemetry/sdk-metrics"); const core_1 = require("@opentelemetry/core"); const Crypto = require("crypto"); // Constants for CloudWatch Logs limits exports.CW_MAX_EVENT_PAYLOAD_BYTES = 256 * 1024; // 256KB exports.CW_MAX_REQUEST_EVENT_COUNT = 10000; exports.CW_PER_EVENT_HEADER_BYTES = 26; exports.BATCH_FLUSH_INTERVAL = 60 * 1000; exports.CW_MAX_REQUEST_PAYLOAD_BYTES = 1 * 1024 * 1024; // 1MB exports.CW_TRUNCATED_SUFFIX = '[Truncated...]'; exports.CW_EVENT_TIMESTAMP_LIMIT_PAST = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds exports.CW_EVENT_TIMESTAMP_LIMIT_FUTURE = 2 * 60 * 60 * 1000; // 2 hours in milliseconds exports.RECORD_DATA_TYPES = { SUM_DATA: 'SUM_DATA', HISTOGRAM_DATA: 'HISTOGRAM_DATA', EXP_HISTOGRAM_DATA: 'EXP_HISTOGRAM_DATA', }; /** * OpenTelemetry metrics exporter for CloudWatch EMF format. * * This exporter converts OTel metrics into CloudWatch EMF logs which are then * sent to CloudWatch Logs. CloudWatch Logs automatically extracts the metrics * from the EMF logs. */ class CloudWatchEMFExporter { /** * Initialize the CloudWatch EMF exporter. * * @param namespace CloudWatch namespace for metrics * @param logGroupName CloudWatch log group name * @param logStreamName CloudWatch log stream name (auto-generated if None) * @param awsRegion AWS region (auto-detected if None) * @param AggregationTemporality Optional AggregationTemporality to indicate the way additive quantities are expressed * @param cloudwatchLogsConfig Additional arguments passed to boto3 client */ constructor(namespace = 'default', logGroupName, logStreamName, awsRegion, //[] preferred_temporality: Record<type, AggregationTemporality> | undefined, aggregationTemporality = sdk_metrics_1.AggregationTemporality.DELTA, //[] **kwargs cloudwatchLogsConfig = {}) { //[]super().__init__(preferred_temporality) // Event batch to store logs before sending to CloudWatch this.eventBatch = undefined; // OTel to CloudWatch unit mapping this.UNIT_MAPPING = new Map(Object.entries({ ms: 'Milliseconds', s: 'Seconds', us: 'Microseconds', ns: 'Nanoseconds', By: 'Bytes', KiBy: 'Kilobytes', MiBy: 'Megabytes', GiBy: 'Gigabytes', TiBy: 'Terabytes', Bi: 'Bits', KiBi: 'Kilobits', MiBi: 'Megabits', GiBi: 'Gigabits', TiBi: 'Terabits', '%': 'Percent', '1': 'Count', '{count}': 'Count', })); this.namespace = namespace; this.logGroupName = logGroupName; this.logStreamName = logStreamName || this.generateLogStreamName(); //[] this.metricDeclarations = metricDeclarations || []; // this.parseJsonEncodedAttrValues = parseJsonEncodedAttrValues; this.aggregationTemporality = aggregationTemporality; // Initialize CloudWatch Logs client //[] session = boto3.Session(region_name=aws_region) // this.logs_client = session.client("logs", **kwargs) this.logsClient = new client_cloudwatch_logs_1.CloudWatchLogs(cloudwatchLogsConfig); //[] aws region? // Ensure log group exists this.logGroupExists = false; this.logGroupExistsPromise = this.ensureLogGroupExists(); } generateLogStreamName() { /* Generate a unique log stream name. */ // import uuid // unique_id = str(uuid.uuid4())[:8] const uniqueId = Crypto.randomUUID().substring(0, 8); return `otel-javascript-${uniqueId}`; } async ensureLogGroupExists() { if (this.logGroupExists) { return true; } /* Ensure the log group exists, create if it doesn't. */ //[] try { //[][][][] Should this be non blocking??????? const res = await this.logsClient.describeLogGroups({ logGroupNamePrefix: this.logGroupName, limit: 1, }); // } catch (e) { if (!res.logGroups || res.logGroups.length === 0) { try { await this.logsClient.createLogGroup({ logGroupName: this.logGroupName, }); api_1.diag.info(`Created log group: ${this.logGroupName}`); } catch (e) { api_1.diag.error(`Failed to create log group ${this.logGroupName}: ${e}`); this.logGroupExists = false; throw e; } } // } this.logGroupExists = true; return true; } getMetricName(record) { var _a; /* Get the metric name from the metric record or data point. */ if (record.name) { //[][] Confirm if this is still needed // For metrics in MetricsData format return record.name; } else if ((_a = record.instrument) === null || _a === void 0 ? void 0 : _a.name) { // For compatibility with older record format return record.instrument.name; } else { // Fallback with generic name return 'unknown_metric'; } } //[][] instrument_or_metric not a good name because is only of type Instrument? getUnit(instrumentOrMetric) { var _a; /* Get CloudWatch unit from OTel instrument or metric unit. */ // Check if we have an Instrument object or a metric with unit attribute //[] let unit; //[][][Do I really need this?] if (instrument_or_metric instanceof Instrument){ const unit = instrumentOrMetric.unit; //[][] }else{ //[][] unit = instrument_or_metric['unit'] ?? undefined //[][] } if (!unit) { return undefined; } return (_a = this.UNIT_MAPPING.get(unit)) !== null && _a !== void 0 ? _a : unit; } getDimensionNames(attributes) { /* Extract dimension names from attributes. */ // Implement dimension selection logic // For now, use all attributes as dimensions return Object.keys(attributes); } getAttributesKey(attributes) { /* Create a hashable key from attributes for grouping metrics. Args: attributes: The attributes dictionary Returns: A string representation of sorted attributes key-value pairs */ // Sort the attributes to ensure consistent keys const sortedAttrs = Object.entries(attributes).sort(); // Create a string representation of the attributes return sortedAttrs.toString(); } normalizeTimestamp(hrTime) { /* Normalize a nanosecond timestamp to milliseconds for CloudWatch. Args: timestamp_ns: Timestamp in nanoseconds Returns: Timestamp in milliseconds */ // Convert from nanoseconds to milliseconds const secondsToMillis = hrTime[0] * 1000; const nanosToMillis = Math.floor(hrTime[1] / 1000000); // Converting a double to an int in Javascript without rounding return secondsToMillis + nanosToMillis; } createMetricRecord(metricName, metricUnit, metricDescription) { /*Create a base metric record with instrument information. Args: metric_name: Name of the metric metric_unit: Unit of the metric metric_description: Description of the metric Returns: A base metric record object */ //[][] let record = type('MetricRecord', (), {})() // record.instrument = type('Instrument', (), {})() // record.instrument.name = metric_name // record.instrument.unit = metric_unit // record.instrument.description = metric_description const record = { instrument: { name: metricName, unit: metricUnit, description: metricDescription, }, }; return record; } convertGauge(metric, dp) { /*Convert a Gauge metric datapoint to a metric record. Args: metric: The metric object dp: The datapoint to convert Returns: Tuple of (metric record, timestamp in ms) */ //[] dp.endTime? seems like the only one... same goes for all other use of normalizeTimestamp //[] let timestamp_ms = this.normalize_timestamp(dp.time_unix_nano) if 'time_unix_nano' in dp else int(time.time() * 1000) const timestampMs = this.normalizeTimestamp(dp.endTime); // Create base record const record = Object.assign(Object.assign({}, this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description)), { timestamp: timestampMs, attributes: dp.attributes, value: dp.value }); return [record, timestampMs]; } convertSum(metric, dp) { /*Convert a Sum metric datapoint to a metric record. Args: metric: The metric object dp: The datapoint to convert Returns: Tuple of (metric record, timestamp in ms) */ const timestampMs = this.normalizeTimestamp(dp.endTime); // Create base record const record = Object.assign(Object.assign({}, this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description)), { timestamp: timestampMs, attributes: dp.attributes, sumData: { // For Sum, set the sumData value: dp.value, type: exports.RECORD_DATA_TYPES.SUM_DATA, } }); return [record, timestampMs]; } convertHistogram(metric, dp) { /*Convert a Histogram metric datapoint to a metric record. https://github.com/mircohacker/opentelemetry-collector-contrib/blob/main/exporter/awsemfexporter/datapoint.go#L148 Args: metric: The metric object dp: The datapoint to convert Returns: Tuple of (metric record, timestamp in ms) */ var _a, _b, _c; const timestampMs = this.normalizeTimestamp(dp.endTime); // Create base record const record = Object.assign(Object.assign({}, this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description)), { timestamp: timestampMs, attributes: dp.attributes, histogramData: { value: { // For Histogram, set the histogram_data Count: dp.value.count, Sum: (_a = dp.value.sum) !== null && _a !== void 0 ? _a : 0, Min: (_b = dp.value.min) !== null && _b !== void 0 ? _b : 0, Max: (_c = dp.value.max) !== null && _c !== void 0 ? _c : 0, }, type: exports.RECORD_DATA_TYPES.HISTOGRAM_DATA, } }); return [record, timestampMs]; } convertExpHistogram(metric, dp) { /* Convert an ExponentialHistogram metric datapoint to a metric record. This function follows the logic of CalculateDeltaDatapoints in the Go implementation, converting exponential buckets to their midpoint values. Args: metric: The metric object dp: The datapoint to convert Returns: Tuple of (metric record, timestamp in ms) */ var _a, _b, _c, _d, _e, _f, _g, _h; // Set timestamp const timestampMs = this.normalizeTimestamp(dp.endTime); // Initialize arrays for values and counts const arrayValues = []; const arrayCounts = []; // Get scale const scale = dp.value.scale; // Calculate base using the formula: 2^(2^(-scale)) const base = Math.pow(2, Math.pow(2, -scale)); // Process positive buckets // if ('positive' in dp && 'bucketCounts' in dp.value.positive && dp.positive.bucket_counts) { if ((_b = (_a = dp.value) === null || _a === void 0 ? void 0 : _a.positive) === null || _b === void 0 ? void 0 : _b.bucketCounts) { const positiveOffset = (_c = dp.value.positive.offset) !== null && _c !== void 0 ? _c : 0; const positiveBucketCounts = dp.value.positive.bucketCounts; let bucketBegin = 0; let bucketEnd = 0; for (const [i, count] of positiveBucketCounts.entries()) { const index = i + positiveOffset; if (bucketBegin === 0) { bucketBegin = Math.pow(base, index); } else { bucketBegin = bucketEnd; } bucketEnd = Math.pow(base, index + 1); // Calculate midpoint value of the bucket const metricVal = (bucketBegin + bucketEnd) / 2; // Only include buckets with positive counts if (count > 0) { arrayValues.push(metricVal); arrayCounts.push(count); } } } // Process zero bucket const zeroCount = (_d = dp.value.zeroCount) !== null && _d !== void 0 ? _d : 0; if (zeroCount > 0) { arrayValues.push(0); arrayCounts.push(zeroCount); } // Process negative buckets if (dp.value.negative.bucketCounts) { const negativeOffset = (_e = dp.value.negative.offset) !== null && _e !== void 0 ? _e : 0; const negativeBucketCounts = dp.value.negative.bucketCounts; let bucketBegin = 0; let bucketEnd = 0; for (const [i, count] of Object.entries(negativeBucketCounts)) { const index = parseInt(i) + negativeOffset; if (bucketEnd === 0) { bucketEnd = -Math.pow(base, index); } else { bucketEnd = bucketBegin; } bucketBegin = -Math.pow(base, index + 1); // Calculate midpoint value of the bucket const metricVal = (bucketBegin + bucketEnd) / 2; // Only include buckets with positive counts if (count > 0) { arrayValues.push(metricVal); arrayCounts.push(count); } } } // Create base record const record = Object.assign(Object.assign({}, this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description)), { timestamp: timestampMs, attributes: dp.attributes, expHistogramData: { value: { // Set the histogram data in the format expected by CloudWatch EMF Values: arrayValues, Counts: arrayCounts, Count: dp.value.count, Sum: (_f = dp.value.sum) !== null && _f !== void 0 ? _f : 0, Max: (_g = dp.value.max) !== null && _g !== void 0 ? _g : 0, Min: (_h = dp.value.min) !== null && _h !== void 0 ? _h : 0, }, type: exports.RECORD_DATA_TYPES.HISTOGRAM_DATA, } }); return [record, timestampMs]; } groupByAttributesAndTimestamp(record, timestampMs) { /*Group metric record by attributes and timestamp. Args: record: The metric record timestamp_ms: The timestamp in milliseconds Returns: A tuple key for grouping */ // Create a key for grouping based on attributes const attrsKey = this.getAttributesKey(record.attributes); return [attrsKey, timestampMs]; } createEmfLog(metricRecords, resource, timestamp = undefined) { var _a, _b; /* Create EMF log dictionary from metric records. Since metric_records is already grouped by attributes, this function creates a single EMF log for all records. */ // Start with base structure const emfLog = { _aws: { Timestamp: timestamp || Date.now(), CloudWatchMetrics: [], }, Version: '1', }; // Add resource attributes to EMF log but not as dimensions if (resource && resource.attributes) { for (const [key, value] of Object.entries(resource.attributes)) { emfLog[`resource.${key}`] = (_a = value === null || value === void 0 ? void 0 : value.toString()) !== null && _a !== void 0 ? _a : 'undefined'; // Python assumes not undefinable } } // Initialize collections for dimensions and metrics const allAttributes = {}; const metricDefinitions = []; // Process each metric record for (const record of metricRecords) { // Collect attributes from all records (they should be the same for all records in the group) if ('attributes' in record && record.attributes) { for (const [key, value] of Object.entries(record.attributes)) { allAttributes[key] = value; } } const metricName = this.getMetricName(record); const unit = this.getUnit(record.instrument); // Process different types of aggregations if (record.expHistogramData) { // Base2 Exponential Histogram - Store value directly in emf_log emfLog[metricName] = record.expHistogramData.value; } else if (record.histogramData) { // Regular Histogram metrics - Store value directly in emf_log emfLog[metricName] = record.histogramData.value; } else if (record.sumData) { // Counter/UpDownCounter - Store value directly in emf_log emfLog[metricName] = record.sumData.value; } else { // Other aggregations (e.g., LastValue) if (record.value) { // Store value directly in emf_log emfLog[metricName] = record.value; } } // Create metric data const metricData = { Name: metricName, }; if (unit) { metricData['Unit'] = unit; } // Add to metric definitions list metricDefinitions.push(metricData); } // Get dimension names from collected attributes const dimensionNames = this.getDimensionNames(allAttributes); // Add attribute values to the root of the EMF log for (const [name, value] of Object.entries(allAttributes)) { emfLog[name] = (_b = value === null || value === void 0 ? void 0 : value.toString()) !== null && _b !== void 0 ? _b : 'undefined'; } // Add the single dimension set to CloudWatch Metrics if we have dimensions and metrics if (dimensionNames && metricDefinitions) { emfLog['_aws']['CloudWatchMetrics'].push({ Namespace: this.namespace, Dimensions: [dimensionNames], Metrics: metricDefinitions, }); } return emfLog; } pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record) { let metricsGroupedByAttribute = groupedMetrics.get(groupAttribute); if (!metricsGroupedByAttribute) { metricsGroupedByAttribute = new Map(); groupedMetrics.set(groupAttribute, metricsGroupedByAttribute); } let metricsGroupedByAttributeAndTimestamp = metricsGroupedByAttribute.get(groupTimestamp); if (!metricsGroupedByAttributeAndTimestamp) { metricsGroupedByAttributeAndTimestamp = []; metricsGroupedByAttribute.set(groupTimestamp, metricsGroupedByAttributeAndTimestamp); } metricsGroupedByAttributeAndTimestamp.push(record); } async export(resourceMetrics, resultCallback) { /* Export metrics as EMF logs to CloudWatch. Groups metrics by attributes and timestamp before creating EMF logs. Args: resourceMetrics: MetricsData containing resource metrics and scope metrics timeout_millis: Optional timeout in milliseconds **kwargs: Additional keyword arguments Returns: MetricExportResult indicating success or failure */ try { if (!resourceMetrics) { resultCallback({ code: core_1.ExportResultCode.SUCCESS }); return; } // Process all metrics from resource metrics their scope metrics // The resource is now part of each resource_metrics object const resource = resourceMetrics.resource; for (const scopeMetrics of resourceMetrics.scopeMetrics /*resource_metrics.scope_metrics*/) { // Dictionary to group metrics by attributes and timestamp // Key: (attributes_key, timestamp_ms) // Value: list of metric records const groupedMetrics = new Map(); // Process all metrics in this scope for (const metric of scopeMetrics.metrics) { // Convert metrics to a format compatible with create_emf_log // Access data points through metric.data.data_points if ('dataPoints' in metric) { // Process different metric types if (metric.dataPointType === sdk_metrics_1.DataPointType.GAUGE) { for (const dp of metric.dataPoints) { const [record, timestampMs] = this.convertGauge(metric, dp); const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record, timestampMs); this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record); } } else if (metric.dataPointType === sdk_metrics_1.DataPointType.SUM) { for (const dp of metric.dataPoints) { const [record, timestampMs] = this.convertSum(metric, dp); const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record, timestampMs); this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record); } } else if (metric.dataPointType === sdk_metrics_1.DataPointType.HISTOGRAM) { for (const dp of metric.dataPoints) { const [record, timestampMs] = this.convertHistogram(metric, dp); const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record, timestampMs); this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record); } } else if (metric.dataPointType === sdk_metrics_1.DataPointType.EXPONENTIAL_HISTOGRAM) { for (const dp of metric.dataPoints) { const [record, timestampMs] = this.convertExpHistogram(metric, dp); const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record, timestampMs); this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record); } } else { api_1.diag.warn('Unsupported Metric Type: %s', metric.dataPointType); continue; // Skip this metric but continue processing others } } } const sendLogEventPromises = []; // Now process each group separately to create one EMF log per group groupedMetrics.forEach((metricsRecordsGroupedByAttribute, attrsKey) => { // metricRecords is grouped by attribute and timestamp metricsRecordsGroupedByAttribute.forEach((metricRecords, timestampMs) => { if (metricRecords) { api_1.diag.debug(`Creating EMF log for group with ${metricRecords.length} metrics. Timestamp: ${timestampMs}, Attributes: ${attrsKey.substring(0, 100)}...`); // Create EMF log for this batch of metrics with the group's timestamp const emfLog = this.createEmfLog(metricRecords, resource, Number(timestampMs)); // Convert to JSON const logEvent = { message: JSON.stringify(emfLog), timestamp: timestampMs, }; // Send to CloudWatch Logs sendLogEventPromises.push(this.sendLogEvent(logEvent)); } }); }); //[][] must we await all "sendLogEvents"? await Promise.all(sendLogEventPromises); } resultCallback({ code: core_1.ExportResultCode.SUCCESS }); return; } catch (e) { api_1.diag.error(`Failed to export metrics: ${e}`); resultCallback({ code: core_1.ExportResultCode.FAILED, error: e }); return; } } validateLogEvent(logEvent) { /* Validate the log event according to CloudWatch Logs constraints. Implements the same validation logic as the Go version. Args: log_event: The log event to validate Returns: bool: true if valid, false otherwise */ const message = logEvent.message; const timestamp = logEvent.timestamp; // Check message size const messageSize = message.length + exports.CW_PER_EVENT_HEADER_BYTES; if (messageSize > exports.CW_MAX_EVENT_PAYLOAD_BYTES) { api_1.diag.warn(`Log event size ${messageSize} exceeds maximum allowed size {CW_MAX_EVENT_PAYLOAD_BYTES}. Truncating.`); const maxMessageSize = exports.CW_MAX_EVENT_PAYLOAD_BYTES - exports.CW_PER_EVENT_HEADER_BYTES - exports.CW_TRUNCATED_SUFFIX.length; logEvent['message'] = message.substring(0, maxMessageSize) + exports.CW_TRUNCATED_SUFFIX; } // Check empty message if (logEvent.message === '') { api_1.diag.error('Empty log event message'); return false; } // Check timestamp constraints const currentTime = Date.now(); // Current time in milliseconds const eventTime = timestamp; // Calculate the time difference const timeDiff = currentTime - eventTime; // Check if too old or too far in the future if (timeDiff > exports.CW_EVENT_TIMESTAMP_LIMIT_PAST || timeDiff < -exports.CW_EVENT_TIMESTAMP_LIMIT_FUTURE) { api_1.diag.error(`Log event timestamp ${eventTime} is either older than 14 days or more than 2 hours in the future. Current time: ${currentTime}`); return false; } return true; } createEventBatch() { /* Create a new log event batch. Returns: Dict: A new event batch */ return { logEvents: [], byteTotal: 0, minTimestampMs: 0, maxTimestampMs: 0, createdTimestampMs: Date.now(), }; } eventBatchExceedsLimit(batch, nextEventSize) { /* Check if adding the next event would exceed CloudWatch Logs limits. Args: batch: The current batch nextEventSize: Size of the next event in bytes CW_MAX_REQUEST_EVENT_COUNT Returns: bool: true if adding the next event would exceed limits */ //[][] console.log(`${batch.logEvents.length} ${batch.byteTotal}`) return ( //[][] batch.logEvents.length >= 5 || //[] batch.logEvents.length >= exports.CW_MAX_REQUEST_EVENT_COUNT || batch.byteTotal + nextEventSize > exports.CW_MAX_REQUEST_PAYLOAD_BYTES); } isBatchActive(batch, targetTimestampMs) { /* Check if the event batch spans more than 24 hours. Args: batch: The event batch targetTimestampMs: The timestamp of the event to add Returns: bool: true if the batch is active and can accept the event */ // 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(); // console.log(`Diff interval ${currentTime - batch.createdTimestampMs} >= ${BATCH_FLUSH_INTERVAL}`); if (currentTime - batch.createdTimestampMs >= exports.BATCH_FLUSH_INTERVAL) { return false; } return true; } appendToBatch(batch, logEvent, eventSize) { /* Append a log event to the batch. Args: batch: The event batch logEvent: The log event to append eventSize: Size of the event in bytes */ batch.logEvents.push(logEvent); batch.byteTotal += eventSize; const timestamp = logEvent.timestamp; if (batch.minTimestampMs === 0 || batch.minTimestampMs > timestamp) { batch.minTimestampMs = timestamp; } if (batch.maxTimestampMs === 0 || batch.maxTimestampMs < timestamp) { batch.maxTimestampMs = timestamp; } } sortLogEvents(batch) { /* Sort log events in the batch by timestamp. Args: batch: The event batch */ //[]batch["logEvents"] = sorted(batch["logEvents"], key=lambda x: x["timestamp"]) batch.logEvents = batch.logEvents.sort((a, b) => a.timestamp - b.timestamp); } async sendLogBatch(batch) { /* Send a batch of log events to CloudWatch Logs. Args: batch: The event batch */ if (!batch['logEvents'] || batch['logEvents'].length === 0) { return; } if (!this.logGroupExists) { // Must perform logGroupExists check here because promises cannot be "awaited" in constructor await this.logGroupExistsPromise; } // Sort log events by timestamp this.sortLogEvents(batch); // Prepare the PutLogEvents request const putLogEventsInput = { logStreamName: this.logStreamName, logEvents: batch['logEvents'], logGroupName: this.logGroupName, }; // let start_time = time.time() const startTime = Date.now(); try { // Create log group and stream if they don't exist //[]don't need this try catch try { await this.logsClient.createLogGroup({ logGroupName: this.logGroupName, }); // need to only print this debug statement depending on result of createLogGroupCommand api_1.diag.debug(`Created log group: ${this.logGroupName}`); } catch (e) { api_1.diag.debug(`Error when creating log group "${this.logGroupName}": ${e}`); } // Create log stream if it doesn't exist try { await this.logsClient.createLogStream({ logGroupName: this.logGroupName, logStreamName: this.logStreamName, }); api_1.diag.debug(`Created log stream: ${this.logStreamName}`); } catch (e) { api_1.diag.debug(`Error when creating log stream "${this.logStreamName}": ${e}`); } // Make the PutLogEvents call const response = await this.logsClient.putLogEvents(putLogEventsInput); const elapsedMs = Date.now() - startTime; api_1.diag.debug(`Successfully sent ${batch['logEvents'].length} log events (${(batch['byteTotal'] / 1024).toFixed(2)} KB) in ${elapsedMs} ms`); return response; } catch (e) { api_1.diag.error(`Failed to send log events: ${e}`); throw e; } } async sendLogEvent(logEvent) { /* 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. Args: logEvent: The log event to send */ try { // Validate the log event if (!this.validateLogEvent(logEvent)) { return; } // Calculate event size const eventSize = logEvent['message'].length + exports.CW_PER_EVENT_HEADER_BYTES; // Initialize event batch if needed if (this.eventBatch === undefined) { 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'])) { // Create a new batch this.eventBatch = this.createEventBatch(); // Send the current batch await this.sendLogBatch(currentBatch); currentBatch = this.eventBatch; } // Add the log event to the batch this.appendToBatch(currentBatch, logEvent, eventSize); } catch (e) { api_1.diag.error(`Failed to process log event: ${e}`); throw e; } } async forceFlush(timeoutMillis = 10000) { /* Force flush any pending metrics. Args: timeoutMillis: Timeout in milliseconds Returns: true if successful, false otherwise */ if (this.eventBatch !== undefined && this.eventBatch['logEvents'].length > 0) { const currentBatch = this.eventBatch; this.eventBatch = this.createEventBatch(); await this.sendLogBatch(currentBatch); } api_1.diag.debug('CloudWatchEMFExporter force flushes the bufferred metrics'); } async shutdown() { /* Shutdown the exporter. Override to handle timeout and other keyword arguments, but do nothing. Args: timeout_millis: Ignored timeout in milliseconds **kwargs: Ignored additional keyword arguments */ // Intentionally do nothing await this.forceFlush(); api_1.diag.debug('CloudWatchEMFExporter shutdown called'); return Promise.resolve(); } selectAggregationTemporality(instrumentType) { return this.aggregationTemporality; } selectAggregation(instrumentType) { switch (instrumentType) { case sdk_metrics_1.InstrumentType.HISTOGRAM: { return sdk_metrics_1.Aggregation.ExponentialHistogram(); } } return sdk_metrics_1.Aggregation.Default(); } } exports.CloudWatchEMFExporter = CloudWatchEMFExporter; /** * Convenience function to create a CloudWatch EMF exporter with DELTA temporality. * * @param namespace CloudWatch namespace for metrics * @param logGroupName CloudWatch log group name * @param logStreamName CloudWatch log stream name (auto-generated if not provided) * @param awsRegion AWS region (auto-detected if not provided) * @returns {CloudWatchEMFExporter} Configured CloudWatchEMFExporter instance */ function createEmfExporter(namespace = 'OTelJavaScript', logGroupName = '/aws/otel/javascript', logStreamName, awsRegion, //[][] **kwargs, cloudwatchLogsConfig = {}) { return new CloudWatchEMFExporter(namespace, logGroupName, logStreamName, awsRegion, sdk_metrics_1.AggregationTemporality.DELTA, // Set up temporality preference - always use DELTA for CloudWatch cloudwatchLogsConfig); } exports.createEmfExporter = createEmfExporter; //# sourceMappingURL=otlp-aws-emf-exporter.js.map