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.
780 lines • 33.2 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.AWSCloudWatchEMFExporter = 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
const CW_MAX_EVENT_PAYLOAD_BYTES = 256 * 1024; // 256KB
const CW_MAX_REQUEST_EVENT_COUNT = 10000;
const CW_PER_EVENT_HEADER_BYTES = 26;
const BATCH_FLUSH_INTERVAL = 60 * 1000;
const CW_MAX_REQUEST_PAYLOAD_BYTES = 1 * 1024 * 1024; // 1MB
const CW_TRUNCATED_SUFFIX = '[Truncated...]';
const CW_EVENT_TIMESTAMP_LIMIT_PAST = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds
const CW_EVENT_TIMESTAMP_LIMIT_FUTURE = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
/**
* 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 AWSCloudWatchEMFExporter {
/**
* Initialize the CloudWatch EMF exporter.
*
* @param namespace CloudWatch namespace for metrics
* @param logGroupName CloudWatch log group name
* @param logStreamName Optional CloudWatch log stream name (auto-generated if not provided)
* @param AggregationTemporality Optional AggregationTemporality to indicate the way additive quantities are expressed
* @param cloudwatchLogsConfig Optional CloudWatch Logs Client Configuration. Configure region here if needed explicitly.
*/
constructor(namespace = 'default', logGroupName, logStreamName, aggregationTemporality = sdk_metrics_1.AggregationTemporality.DELTA, cloudwatchLogsConfig = {}) {
this.EMF_SUPPORTED_UNITS = new Set([
'Seconds',
'Microseconds',
'Milliseconds',
'Bytes',
'Kilobytes',
'Megabytes',
'Gigabytes',
'Terabytes',
'Bits',
'Kilobits',
'Megabits',
'Gigabits',
'Terabits',
'Percent',
'Count',
'Bytes/Second',
'Kilobytes/Second',
'Megabytes/Second',
'Gigabytes/Second',
'Terabytes/Second',
'Bits/Second',
'Kilobits/Second',
'Megabits/Second',
'Gigabits/Second',
'Terabits/Second',
'Count/Second',
'None',
]);
// OTel to CloudWatch unit mapping
this.UNIT_MAPPING = new Map(Object.entries({
'1': '',
ns: '',
ms: 'Milliseconds',
s: 'Seconds',
us: 'Microseconds',
By: 'Bytes',
bit: 'Bits',
}));
this.namespace = namespace;
this.logGroupName = logGroupName;
this.logStreamName = logStreamName || this.generateLogStreamName();
this.aggregationTemporality = aggregationTemporality;
this.logsClient = new client_cloudwatch_logs_1.CloudWatchLogs(cloudwatchLogsConfig);
// Determine that Log group/stream exists asynchronously. The Constructor cannot wait on async
// operations, so whether or not the group/stream actually exists will be determined later.
this.logStreamExists = false;
this.logStreamExistsPromise = this.ensureLogGroupExists().then(async () => {
await this.ensureLogStreamExists();
});
}
/**
* 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 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.info(`Log group ${this.logStreamName} already exists.`);
}
else {
api_1.diag.error(`Error occurred when creating log group ${this.logGroupName}: ${e}`);
throw e;
}
}
}
/**
* Ensure the log stream exists, create if it doesn't.
*/
async ensureLogStreamExists() {
try {
await this.logsClient.createLogStream({
logGroupName: this.logGroupName,
logStreamName: this.logStreamName,
});
api_1.diag.info(`Created log stream: ${this.logStreamName}`);
this.logStreamExists = true;
}
catch (e) {
if (e instanceof Error && e.name === 'ResourceAlreadyExistsException') {
api_1.diag.info(`Log stream ${this.logStreamName} already exists.`);
}
else {
api_1.diag.error(`Error occurred when creating log stream "${this.logStreamName}": ${e}`);
throw e;
}
}
}
/**
* Get CloudWatch unit from unit in MetricRecord
*
* @param record Metric Record
* @returns {string | undefined}
*/
getUnit(record) {
const unit = record.unit;
if (this.EMF_SUPPORTED_UNITS.has(unit)) {
return unit;
}
return this.UNIT_MAPPING.get(unit);
}
/**
* Extract dimension names from attributes.
* For now, use all attributes as dimensions for the dimension selection logic.
*
* @param attributes OpenTelemetry Attributes to extract Dimension Names from
* @returns {string[]}
*/
getDimensionNames(attributes) {
return Object.keys(attributes);
}
/**
* Create a hashable key from attributes for grouping metrics.
*
* @param attributes OpenTelemetry Attributes used to create an attributes key
* @returns {string}
*/
getAttributesKey(attributes) {
// Sort the attributes to ensure consistent keys
const sortedAttrs = Object.entries(attributes).sort();
// Create a string representation of the attributes
return sortedAttrs.toString();
}
/**
* Normalize an OpenTelemetry timestamp to milliseconds for CloudWatch.
*
* @param hrTime Datapoint timestamp
* @returns {number} Timestamp in milliseconds
*/
normalizeTimestamp(hrTime) {
// Convert from second and nanoseconds to milliseconds
const secondsToMillis = hrTime[0] * 1000;
const nanosToMillis = Math.floor(hrTime[1] / 1000000);
return secondsToMillis + nanosToMillis;
}
/**
* Create a base metric record with instrument information.
*
* @param metricName Name of the metric
* @param metricUnit Unit of the metric
* @param metricDescription Description of the metric
* @param timestamp Normalized end epoch timestamp when metric data was collected
* @param attributes Attributes of the metric data
* @returns {MetricRecord}
*/
createMetricRecord(metricName, metricUnit, metricDescription, timestamp, attributes) {
const record = {
name: metricName,
unit: metricUnit,
description: metricDescription,
timestamp,
attributes,
};
return record;
}
/**
* Convert a Gauge metric datapoint to a metric record.
*
* @param metric Gauge Metric Data
* @param dataPoint The datapoint to convert
* @returns {MetricRecord}
*/
convertGauge(metric, dataPoint) {
const timestampMs = this.normalizeTimestamp(dataPoint.endTime);
// Create base record
const metricRecord = this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description, timestampMs, dataPoint.attributes);
metricRecord.value = dataPoint.value; // For Gauge, set the value directly
return metricRecord;
}
/**
* Convert a Sum metric datapoint to a metric record.
*
* @param metric The metric object
* @param dataPoint The datapoint to convert
* @returns {MetricRecord}
*/
convertSum(metric, dataPoint) {
const timestampMs = this.normalizeTimestamp(dataPoint.endTime);
// Create base record
const record = this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description, timestampMs, dataPoint.attributes);
record.sumData = dataPoint.value;
return record;
}
/**
* Convert a Histogram metric datapoint to a metric record.
*
* @param metric The metric object
* @param dataPoint The datapoint to convert
* @returns {MetricRecord}
*/
convertHistogram(metric, dataPoint) {
var _a, _b, _c;
const timestampMs = this.normalizeTimestamp(dataPoint.endTime);
// Create base record
const record = this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description, timestampMs, dataPoint.attributes);
record.histogramData = {
// For Histogram, set the histogram_data
Count: dataPoint.value.count,
Sum: (_a = dataPoint.value.sum) !== null && _a !== void 0 ? _a : 0,
Min: (_b = dataPoint.value.min) !== null && _b !== void 0 ? _b : 0,
Max: (_c = dataPoint.value.max) !== null && _c !== void 0 ? _c : 0,
};
return record;
}
/**
* 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.
*
* @param metric The metric object
* @param dataPoint The datapoint to convert
* @returns {MetricRecord}
*/
convertExpHistogram(metric, dataPoint) {
var _a, _b, _c, _d, _e;
// Set timestamp
const timestampMs = this.normalizeTimestamp(dataPoint.endTime);
// Initialize arrays for values and counts
const arrayValues = [];
const arrayCounts = [];
// Get scale
const scale = dataPoint.value.scale;
// Calculate base using the formula: 2^(2^(-scale))
const base = Math.pow(2, Math.pow(2, -scale));
// Process positive buckets
if ((_b = (_a = dataPoint.value) === null || _a === void 0 ? void 0 : _a.positive) === null || _b === void 0 ? void 0 : _b.bucketCounts) {
const positiveOffset = dataPoint.value.positive.offset;
const positiveBucketCounts = dataPoint.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 = dataPoint.value.zeroCount;
if (zeroCount > 0) {
arrayValues.push(0);
arrayCounts.push(zeroCount);
}
// Process negative buckets
if (dataPoint.value.negative.bucketCounts) {
const negativeOffset = dataPoint.value.negative.offset;
const negativeBucketCounts = dataPoint.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 metricRecord = this.createMetricRecord(metric.descriptor.name, metric.descriptor.unit, metric.descriptor.description, timestampMs, dataPoint.attributes);
metricRecord.expHistogramData = {
// Set the histogram data in the format expected by CloudWatch EMF
Values: arrayValues,
Counts: arrayCounts,
Count: dataPoint.value.count,
Sum: (_c = dataPoint.value.sum) !== null && _c !== void 0 ? _c : 0,
Max: (_d = dataPoint.value.max) !== null && _d !== void 0 ? _d : 0,
Min: (_e = dataPoint.value.min) !== null && _e !== void 0 ? _e : 0,
};
return metricRecord;
}
/**
* Group metric record by attributes and timestamp.
*
* @param record The metric record
* @param timestampMs The timestamp in milliseconds
* @returns {[string, number]} Values for the key to group metrics
*/
groupByAttributesAndTimestamp(record) {
// Create a key for grouping based on attributes
const attrsKey = this.getAttributesKey(record.attributes);
return [attrsKey, record.timestamp];
}
/**
* Create EMF log from metric records.
* metricRecords is already grouped by attributes, so this
* function creates a single EMF Log for these records.
*
* @param metricRecords List of MetricRecords
* @param resource
* @param timestamp
* @returns {EMFLog}
*/
createEmfLog(metricRecords, resource, timestamp = undefined) {
var _a, _b;
// 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[`otel.resource.${key}`] = (_a = value === null || value === void 0 ? void 0 : value.toString()) !== null && _a !== void 0 ? _a : 'undefined';
}
}
// Initialize collections for dimensions and metrics
// Attributes of each record in the list should be the same
const allAttributes = metricRecords.length > 0 ? metricRecords[0].attributes : {};
const metricDefinitions = [];
// Process each metric record
for (const record of metricRecords) {
const metricName = record.name;
// Skip processing if metric name is falsy
if (!metricName) {
continue;
}
// Process different types of aggregations
if (record.expHistogramData) {
// Base2 Exponential Histogram - Store value directly in emfLog
emfLog[metricName] = record.expHistogramData;
}
else if (record.histogramData) {
// Regular Histogram metrics - Store value directly in emfLog
emfLog[metricName] = record.histogramData;
}
else if (record.sumData) {
// Counter/UpDownCounter - Store value directly in emfLog
emfLog[metricName] = record.sumData;
}
else {
// Other aggregations (e.g., LastValue)
if (record.value) {
// Store value directly in emfLog
emfLog[metricName] = record.value;
}
else {
api_1.diag.debug(`Skipping metric ${metricName} as it does not have valid metric value`);
continue;
}
}
// Create metric data
const metricData = {
Name: metricName,
};
const unit = this.getUnit(record);
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;
}
/**
* Method to handle safely pushing a MetricRecord into a Map of a Map of a list of MetricRecords
*
* @param groupedMetrics
* @param groupAttribute
* @param groupTimestamp
* @param record
*/
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);
}
/**
* Export metrics as EMF logs to CloudWatch.
* Groups metrics by attributes and timestamp before creating EMF logs.
*
* @param resourceMetrics Resource Metrics data containing scope metrics
* @param resultCallback callback for when the export has completed
* @returns {Promise<void>}
*/
async export(resourceMetrics, resultCallback) {
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*/) {
// Map of maps to group metrics by attributes and timestamp
// Keys: (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
// Process metric.dataPoints for different metric types
if (metric.dataPointType === sdk_metrics_1.DataPointType.GAUGE) {
for (const dataPoint of metric.dataPoints) {
const record = this.convertGauge(metric, dataPoint);
const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record);
this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record);
}
}
else if (metric.dataPointType === sdk_metrics_1.DataPointType.SUM) {
for (const dataPoint of metric.dataPoints) {
const record = this.convertSum(metric, dataPoint);
const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record);
this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record);
}
}
else if (metric.dataPointType === sdk_metrics_1.DataPointType.HISTOGRAM) {
for (const dataPoint of metric.dataPoints) {
const record = this.convertHistogram(metric, dataPoint);
const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record);
this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record);
}
}
else if (metric.dataPointType === sdk_metrics_1.DataPointType.EXPONENTIAL_HISTOGRAM) {
for (const dataPoint of metric.dataPoints) {
const record = this.convertExpHistogram(metric, dataPoint);
const [groupAttribute, groupTimestamp] = this.groupByAttributesAndTimestamp(record);
this.pushMetricRecordIntoGroupedMetrics(groupedMetrics, groupAttribute, groupTimestamp, record);
}
}
else {
// This else block should never run, all metric types are accounted for above
api_1.diag.debug(`Unsupported Metric Type in metric: ${metric}`);
}
}
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));
}
});
});
await Promise.all(sendLogEventPromises);
}
resultCallback({ code: core_1.ExportResultCode.SUCCESS });
}
catch (e) {
api_1.diag.error(`Failed to export metrics: ${e}`);
const exportResult = { code: core_1.ExportResultCode.FAILED };
if (e instanceof Error) {
exportResult.error = e;
}
resultCallback(exportResult);
}
}
/**
* Validate the log event according to CloudWatch Logs constraints.
* Implements the same validation logic as the Go version.
*
* @param logEvent The log event to validate
* @returns {boolean}
*/
validateLogEvent(logEvent) {
// Check message size
const messageSize = logEvent.message.length + CW_PER_EVENT_HEADER_BYTES;
if (messageSize > 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 = CW_MAX_EVENT_PAYLOAD_BYTES - CW_PER_EVENT_HEADER_BYTES - CW_TRUNCATED_SUFFIX.length;
logEvent.message = logEvent.message.substring(0, maxMessageSize) + 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 = logEvent.timestamp;
// Calculate the time difference
const timeDiff = currentTime - eventTime;
// Check if too old or too far in the future
if (timeDiff > CW_EVENT_TIMESTAMP_LIMIT_PAST || timeDiff < -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;
}
/**
* Create a new log event batch
*
* @returns {EventBatch}
*/
createEventBatch() {
return {
logEvents: [],
byteTotal: 0,
minTimestampMs: 0,
maxTimestampMs: 0,
createdTimestampMs: Date.now(),
};
}
/**
* Check if adding the next event would exceed CloudWatch Logs limits.
*
* @param batch The current batch
* @param nextEventSize Size of the next event in bytes CW_MAX_REQUEST_EVENT_COUNT
* @returns {boolean} true if adding the next event would exceed limits
*/
eventBatchExceedsLimit(batch, nextEventSize) {
return (batch.logEvents.length >= CW_MAX_REQUEST_EVENT_COUNT ||
batch.byteTotal + nextEventSize > 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 >= BATCH_FLUSH_INTERVAL) {
return false;
}
return true;
}
/**
* Append a log event to the batch.
*
* @param batch The event batch
* @param logEvent The log event to append
* @param eventSize Size of the event in bytes
*/
appendToBatch(batch, logEvent, eventSize) {
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;
}
}
/**
* Sort log events in the batch by timestamp.
*
* @param batch The event batch
*/
sortLogEvents(batch) {
batch.logEvents = batch.logEvents.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* Send a batch of log events to CloudWatch Logs.
*
* @param batch The event batch
* @returns {Promise<void>}
*/
async sendLogBatch(batch) {
if (!batch.logEvents || batch.logEvents.length === 0) {
return;
}
// Sort log events by timestamp
this.sortLogEvents(batch);
// Prepare the PutLogEvents request
const putLogEventsInput = {
logStreamName: this.logStreamName,
logEvents: batch.logEvents,
logGroupName: this.logGroupName,
};
const startTime = Date.now();
try {
if (!this.logStreamExists) {
// Must perform logGroupExists check here because promises cannot be "awaited" in constructor
await this.logStreamExistsPromise;
}
// Make the PutLogEvents call
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`);
}
catch (e) {
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.
*
* @param logEvent The log event to send
* @returns {Promise<void>}
*/
async sendLogEvent(logEvent) {
try {
// Validate the log event
if (!this.validateLogEvent(logEvent)) {
return;
}
// Calculate event size
const eventSize = logEvent.message.length + 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;
}
}
/**
* Force flush any pending metrics.
*
* @param timeoutMillis Timeout in milliseconds
*/
async forceFlush(timeoutMillis = 10000) {
var _a;
if (this.eventBatch !== undefined && ((_a = this.eventBatch.logEvents) === null || _a === void 0 ? void 0 : _a.length) > 0) {
const currentBatch = this.eventBatch;
this.eventBatch = this.createEventBatch();
await this.sendLogBatch(currentBatch);
}
api_1.diag.debug('AWSCloudWatchEMFExporter force flushes the bufferred metrics');
}
/**
* Shutdown the exporter after force flush.
*
* @returns {Promise<void>}
*/
async shutdown() {
await this.forceFlush();
api_1.diag.debug('AWSCloudWatchEMFExporter 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.AWSCloudWatchEMFExporter = AWSCloudWatchEMFExporter;
//# sourceMappingURL=otlp-aws-emf-exporter.js.map