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
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.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