Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t

## Unreleased

### Enhancements

- Add Service and Environment dimensions to EMF metrics when `OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS` is enabled.
Supports platform-aware environment defaults (Lambda, EC2, ECS, EKS).
([#299](https://github.com/aws-observability/aws-otel-js-instrumentation/pull/299))

## v0.8.0 - 2025-10-08

### Enhancements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ import {
ResourceMetrics,
SumMetricData,
} from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { defaultServiceName, Resource } from '@opentelemetry/resources';
import {
SEMRESATTRS_CLOUD_PLATFORM,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_NAME,
CLOUDPLATFORMVALUES_AWS_EC2,
CLOUDPLATFORMVALUES_AWS_ECS,
CLOUDPLATFORMVALUES_AWS_EKS,
CLOUDPLATFORMVALUES_AWS_LAMBDA,
} from '@opentelemetry/semantic-conventions';
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
import type { LogEvent } from '@aws-sdk/client-cloudwatch-logs';

Expand Down Expand Up @@ -87,6 +96,17 @@ interface Metric {
*
* https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html
*/
// Constants for Application Signals EMF dimensions
const SERVICE_DIMENSION = 'Service';
const ENVIRONMENT_DIMENSION = 'Environment';

// Platform-specific default environments
const LAMBDA_DEFAULT = 'lambda:default';
const EC2_DEFAULT = 'ec2:default';
const ECS_DEFAULT = 'ecs:default';
const EKS_DEFAULT = 'eks:default';
const UNKNOWN_ENVIRONMENT = 'generic:default';

export abstract class EMFExporterBase implements PushMetricExporter {
private namespace: string;
private aggregationTemporalitySelector: AggregationTemporalitySelector;
Expand Down Expand Up @@ -188,6 +208,101 @@ export abstract class EMFExporterBase implements PushMetricExporter {
return Object.keys(attributes);
}

/**
* Check if Application Signals EMF dimensions should be added.
*
* Returns true when OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS is set to 'true'.
*/
private static shouldAddApplicationSignalsDimensions(): boolean {
return process.env['OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS']?.toLowerCase() === 'true';
}

/**
* Get the default environment based on the cloud platform.
*/
private getDefaultEnvironmentForPlatform(resource: Resource): string {
if (!resource?.attributes) {
return UNKNOWN_ENVIRONMENT;
}

const cloudPlatform = resource.attributes[SEMRESATTRS_CLOUD_PLATFORM];

if (cloudPlatform) {
switch (String(cloudPlatform)) {
case CLOUDPLATFORMVALUES_AWS_LAMBDA:
return LAMBDA_DEFAULT;
case CLOUDPLATFORMVALUES_AWS_EC2:
return EC2_DEFAULT;
case CLOUDPLATFORMVALUES_AWS_ECS:
return ECS_DEFAULT;
case CLOUDPLATFORMVALUES_AWS_EKS:
return EKS_DEFAULT;
}
}

return UNKNOWN_ENVIRONMENT;
}

/**
* Check if dimension already exists (case-insensitive match).
*/
private hasDimensionCaseInsensitive(dimensionNames: string[], dimensionToCheck: string): boolean {
const dimensionLower = dimensionToCheck.toLowerCase();
return dimensionNames.some(dim => dim.toLowerCase() === dimensionLower);
}

/**
* Add Service and Environment dimensions if Application Signals dimensions are enabled
* and the dimensions are not already present (case-insensitive check).
*/
private addApplicationSignalsDimensions(dimensionNames: string[], emfLog: EMFLog, resource: Resource): void {
if (!EMFExporterBase.shouldAddApplicationSignalsDimensions()) {
return;
}

// Add Service dimension if not already set by user
if (!this.hasDimensionCaseInsensitive(dimensionNames, SERVICE_DIMENSION)) {
let serviceName: string = 'UnknownService';
if (resource?.attributes) {
const serviceAttr = resource.attributes[SEMRESATTRS_SERVICE_NAME];
if (serviceAttr && serviceAttr !== defaultServiceName()) {
serviceName = String(serviceAttr);
}
}
dimensionNames.push(SERVICE_DIMENSION);
emfLog[SERVICE_DIMENSION] = serviceName;
}

// Add Environment dimension if not already set by user
if (!this.hasDimensionCaseInsensitive(dimensionNames, ENVIRONMENT_DIMENSION)) {
let environment: string | undefined;

if (resource?.attributes) {
// First check deployment.environment.name (newer semantic convention)
const envNameAttr = resource.attributes['deployment.environment.name'];
if (envNameAttr) {
environment = String(envNameAttr);
}

// Then check deployment.environment (older semantic convention)
if (!environment) {
const envAttr = resource.attributes[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT];
if (envAttr) {
environment = String(envAttr);
}
}
}

// Fall back to platform-specific default
if (!environment) {
environment = this.getDefaultEnvironmentForPlatform(resource);
}

dimensionNames.push(ENVIRONMENT_DIMENSION);
emfLog[ENVIRONMENT_DIMENSION] = environment;
}
}

/**
* Create a hashable key from attributes for grouping metrics.
*/
Expand Down Expand Up @@ -448,6 +563,9 @@ export abstract class EMFExporterBase implements PushMetricExporter {
emfLog[name] = value?.toString() ?? 'undefined';
}

// Add Application Signals dimensions (Service and Environment) if enabled
this.addApplicationSignalsDimensions(dimensionNames, emfLog, resource);

// Add CloudWatch Metrics if we have metrics, include dimensions only if they exist
if (metricDefinitions.length > 0) {
const cloudWatchMetric: CloudWatchMetric = {
Expand Down
Loading
Loading