Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
bcf5eb2
initial implmentation
liustve Sep 19, 2025
f6ba80a
formatting
liustve Sep 19, 2025
0b2c14c
Merge branch 'main' into emf-exporter
liustve Sep 19, 2025
cd1475d
add emf exporter to changelog
liustve Sep 19, 2025
5276407
add emf exporter to changelog
liustve Sep 20, 2025
3ec3aa5
merge main
liustve Sep 20, 2025
7cd68df
modify message
liustve Sep 20, 2025
e6a5283
add further unit tests
liustve Sep 23, 2025
8067773
Merge branch 'main' into emf-exporter
liustve Sep 23, 2025
b686dc1
refactoring + more unit tests for all exporters
liustve Sep 24, 2025
af3b570
Merge branch 'main' into emf-exporter
liustve Sep 24, 2025
48e9453
add unit tests for configuring emf exporter in adot
liustve Sep 25, 2025
0e3e77a
Merge branch 'main' into emf-exporter
liustve Sep 25, 2025
48ecda1
Merge branch 'emf-exporter' of https://github.com/aws-observability/a…
liustve Sep 25, 2025
fcbd7a0
delete duplicate customizer test
liustve Sep 25, 2025
746e989
add further emf + auto instrumentation unit tests and better java docs
liustve Sep 25, 2025
ae10f71
add compact log record exporter for lambda environment
liustve Sep 26, 2025
4236220
move emf exporter files, removed need to set x-aws-metric-namespace h…
liustve Sep 27, 2025
37192bb
Merge remote-tracking branch 'origin/emf-exporter' into console-log-e…
liustve Sep 27, 2025
b7d0506
update CHANGELOG.md
liustve Sep 27, 2025
82e0b46
plug emf and compact console log exporter to lambda
liustve Sep 29, 2025
d5be8c8
test
liustve Oct 1, 2025
2f8a463
Merge remote-tracking branch 'origin/emf-exporter' into console-log-e…
liustve Oct 2, 2025
3ddfa79
add more unit tests
liustve Oct 2, 2025
be68b2a
remove otel logging dependency
liustve Oct 2, 2025
0557993
remove change to lambda alyer
liustve Oct 2, 2025
aeaf838
polish unit tests
liustve Oct 2, 2025
a5b223f
polish unit tests
liustve Oct 3, 2025
369501a
Merge branch 'main' into emf-exporter
liustve Oct 15, 2025
84dbe92
Merge branch 'main' into emf-exporter
liustve Oct 15, 2025
216841e
Merge branch 'main' into emf-exporter
liustve Oct 23, 2025
096c9cb
Merge branch 'emf-exporter' into console-log-exporter
liustve Oct 23, 2025
d1ccad6
Merge branch 'main' into emf-exporter
liustve Oct 24, 2025
a95a598
Merge branch 'main' into emf-exporter
liustve Oct 27, 2025
178085c
Merge branch 'main' into emf-exporter
liustve Oct 29, 2025
64e6202
Merge branch 'main' into emf-exporter
liustve Nov 1, 2025
8923525
Merge branch 'main' into emf-exporter
liustve Nov 9, 2025
b0f84f7
Merge branch 'emf-exporter' into console-log-exporter
liustve Nov 9, 2025
05ad879
Merge branch 'main' into emf-exporter
liustve Nov 24, 2025
07a0a80
fix emf metric grouping logic
liustve Dec 4, 2025
12a070a
add comment for removeEmfIfEnabled logic
liustve Dec 5, 2025
d233371
Merge branch 'emf-exporter' into console-log-exporter
liustve Dec 5, 2025
fcf659c
merge origin/
liustve Dec 5, 2025
7f53ea4
add defensive checks for instrumention scopes
liustve Dec 5, 2025
5f5866c
move changelog entries
liustve Dec 5, 2025
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
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t

## Unreleased

### Enhancements

- Configure EMF and CompactLog Exporters for Lambda Environment
([#1222](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1222))
- feat: [Java] EMF Exporter Implementation
([#1209](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1209))

## v2.20.0 - 2025-10-29

### Enhancements

- Add CloudWatch EMF metrics exporter with auto instrumentation configuration
([#1209](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1209))
- Support X-Ray Trace Id extraction from Lambda Context object, and respect user-configured OTEL_PROPAGATORS in AWS Lamdba instrumentation
([#1191](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1191)) ([#1218](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1218))
- Adaptive Sampling improvements: Ensure propagation of sampling rule across services and AWS accounts. Remove unnecessary B3 propagator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.concurrent.Immutable;
import software.amazon.opentelemetry.javaagent.providers.exporter.aws.logs.CompactConsoleLogRecordExporter;
import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.AwsCloudWatchEmfExporter;
import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.ConsoleEmfExporter;
import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporterBuilder;
import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogRecordExporterBuilder;
import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporterBuilder;

/**
Expand All @@ -91,7 +92,10 @@ public final class AwsApplicationSignalsCustomizerProvider
// https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-envvars.html
static final String AWS_REGION = "aws.region";
static final String AWS_DEFAULT_REGION = "aws.default.region";
// TODO: We should clean up and get rid of using AWS_LAMBDA_FUNCTION_NAME and default to
// upstream config property implementation.
static final String AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME";
static final String AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG = "aws.lambda.function.name";
static final String LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT =
"LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT";

Expand Down Expand Up @@ -181,6 +185,10 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) {
autoConfiguration.addMetricExporterCustomizer(this::customizeMetricExporter);
}

static boolean isLambdaEnvironment(ConfigProperties props) {
return props.getString(AWS_LAMBDA_FUNCTION_NAME_PROP_CONFIG) != null;
}

private static Optional<String> getAwsRegionFromConfig(ConfigProperties configProps) {
String region = configProps.getString(AWS_REGION);
if (region != null) {
Expand Down Expand Up @@ -515,7 +523,7 @@ LogRecordExporter customizeLogsExporter(
configProps.getString(OTEL_EXPORTER_OTLP_COMPRESSION_CONFIG, "none"));

try {
return OtlpAwsLogsExporterBuilder.create(
return OtlpAwsLogRecordExporterBuilder.create(
(OtlpHttpLogRecordExporter) logsExporter,
configProps.getString(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT))
.setCompression(compression)
Expand All @@ -528,34 +536,41 @@ LogRecordExporter customizeLogsExporter(
e);
}
}
String logsExporterConfig = configProps.getString(OTEL_LOGS_EXPORTER);

if (isLambdaEnvironment(configProps)
&& logsExporterConfig != null
&& logsExporterConfig.equals("console")) {
return new CompactConsoleLogRecordExporter();
}

return logsExporter;
}

MetricExporter customizeMetricExporter(
MetricExporter metricExporter, ConfigProperties configProps) {

if (isEmfExporterEnabled) {
Map<String, String> headers =
AwsApplicationSignalsConfigUtils.parseOtlpHeaders(
configProps.getString(OTEL_EXPORTER_OTLP_LOGS_HEADERS));
Optional<String> awsRegion = getAwsRegionFromConfig(configProps);
String namespace = headers.get(AWS_EMF_METRICS_NAMESPACE);

if (awsRegion.isPresent()) {
String namespace = headers.get(AWS_EMF_METRICS_NAMESPACE);

if (headers.containsKey(AWS_OTLP_LOGS_GROUP_HEADER)
&& headers.containsKey(AWS_OTLP_LOGS_STREAM_HEADER)) {
String logGroup = headers.get(AWS_OTLP_LOGS_GROUP_HEADER);
String logStream = headers.get(AWS_OTLP_LOGS_STREAM_HEADER);
return new AwsCloudWatchEmfExporter(namespace, logGroup, logStream, awsRegion.get());
}
if (isLambdaEnvironment()) {

if (isLambdaEnvironment(configProps)) {
return new ConsoleEmfExporter(namespace);
}
logger.warning(
String.format(
"Improper EMF Exporter configuration: Please configure the environment variable %s to have values for %s, %s, and %s",
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
"Improper EMF Exporter configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to have values for %s, %s, and %s",
AWS_OTLP_LOGS_GROUP_HEADER,
AWS_OTLP_LOGS_STREAM_HEADER,
AWS_EMF_METRICS_NAMESPACE));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/*
* Copyright Amazon.com, Inc. or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.opentelemetry.javaagent.providers.exporter.aws.logs;

/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*
* Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*/

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.opentelemetry.exporter.internal.otlp.IncubatingUtil;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import io.opentelemetry.sdk.resources.Resource;
import java.io.PrintStream;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* A {@link LogRecordExporter} that prints {@link LogRecordData} to standard out based on upstream's
* implementation of SystemOutLogRecordExporter, see: <a
* href="https://github.com/open-telemetry/opentelemetry-java/blob/5ab0a65675e5a06d13b293a758ef495d797e6d04/exporters/logging/src/main/java/io/opentelemetry/exporter/logging/SystemOutLogRecordExporter.java#L66">...</a>
*/
@SuppressWarnings("SystemOut")
public class CompactConsoleLogRecordExporter implements LogRecordExporter {
private static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter.ISO_INSTANT;
private static final ObjectMapper MAPPER =
new ObjectMapper().disable(SerializationFeature.INDENT_OUTPUT);
private final AtomicBoolean isShutdown = new AtomicBoolean();
private final PrintStream printStream;

public CompactConsoleLogRecordExporter() {
this(System.out);
}

public CompactConsoleLogRecordExporter(PrintStream printStream) {
this.printStream = printStream;
}

@Override
public CompletableResultCode export(Collection<LogRecordData> logs) {
if (isShutdown.get()) {
return CompletableResultCode.ofFailure();
}

for (LogRecordData log : logs) {
this.printStream.println(this.toCompactJson(log));
this.printStream.flush();
}
return CompletableResultCode.ofSuccess();
}

@Override
public CompletableResultCode flush() {
this.printStream.flush();
return CompletableResultCode.ofSuccess();
}

@Override
public CompletableResultCode shutdown() {
if (!this.isShutdown.compareAndSet(false, true)) {
this.printStream.println("Calling shutdown() multiple times.");
}
return CompletableResultCode.ofSuccess();
}

@Override
public String toString() {
return "CompactConsoleLogRecordExporter{}";
}

/**
* Converts OpenTelemetry log data to compact JSON format. OTel Java's SystemOutLogRecordExporter
* uses a concise text format, this implementation outputs a compact JSON representation based on
* OTel JavaScript's _exportInfo: <a
* href="https://github.com/open-telemetry/opentelemetry-js/blob/09bf31eb966bab627e76a6c5c05c6e51ccd2f387/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts#L58">...</a>
*
* <p>Example output:
*
* <pre>
* {"body":"This is a test log","severityNumber":9,"severityText":"INFO","attributes":{},"droppedAttributes":0,"timestamp":"2025-09-30T22:37:56.724Z","observedTimestamp":"2025-09-30T22:37:56.724Z","traceId":"","spanId":"","traceFlags":0,"resource":{}}
* </pre>
*
* @param log the log record data to convert
* @return compact JSON string representation of the log record
*/
private String toCompactJson(LogRecordData log) {
LogRecordDataTemplate template = LogRecordDataTemplate.parse(log);

try {
return MAPPER.writeValueAsString(template);
} catch (Exception e) {
this.printStream.println("Error serializing log record: " + e.getMessage());
return "{}";
}
}

/** Data object that structures OTel log record data for JSON serialization. */
@SuppressWarnings("unused")
private static final class LogRecordDataTemplate {
@JsonProperty("resource")
private final ResourceTemplate resourceTemplate;

@JsonProperty("body")
private final String body;

@JsonProperty("severityNumber")
private final int severityNumber;

@JsonProperty("severityText")
private final String severityText;

@JsonProperty("attributes")
private final Map<String, String> attributes;

@JsonProperty("droppedAttributes")
private final int droppedAttributes;

@JsonProperty("timestamp")
private final String timestamp;

@JsonProperty("observedTimestamp")
private final String observedTimestamp;

@JsonProperty("traceId")
private final String traceId;

@JsonProperty("spanId")
private final String spanId;

@JsonProperty("traceFlags")
private final int traceFlags;

@JsonProperty("instrumentationScope")
private final InstrumentationScopeTemplate instrumentationScope;

private LogRecordDataTemplate(
String body,
int severityNumber,
String severityText,
Map<String, String> attributes,
int droppedAttributes,
String timestamp,
String observedTimestamp,
String traceId,
String spanId,
int traceFlags,
ResourceTemplate resourceTemplate,
InstrumentationScopeTemplate instrumentationScope) {
this.resourceTemplate = resourceTemplate;
this.body = body;
this.severityNumber = severityNumber;
this.severityText = severityText;
this.attributes = attributes;
this.droppedAttributes = droppedAttributes;
this.timestamp = timestamp;
this.observedTimestamp = observedTimestamp;
this.traceId = traceId;
this.spanId = spanId;
this.traceFlags = traceFlags;
this.instrumentationScope = instrumentationScope;
}

private static LogRecordDataTemplate parse(LogRecordData log) {
// https://github.com/open-telemetry/opentelemetry-java/blob/48684d6d33048030b133b4f6479d45afddcdc313/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/logs/LogMarshaler.java#L59
Map<String, String> attributes = new HashMap<>();
log.getAttributes()
.forEach((key, value) -> attributes.put(key.getKey(), String.valueOf(value)));

int attributeSize =
IncubatingUtil.isExtendedLogRecordData(log)
? IncubatingUtil.extendedAttributesSize(log)
: log.getAttributes().size();

return new LogRecordDataTemplate(
log.getBodyValue() != null ? log.getBodyValue().asString() : null,
log.getSeverity().getSeverityNumber(),
log.getSeverity().name(),
attributes,
log.getTotalAttributeCount() - attributeSize,
formatTimestamp(log.getTimestampEpochNanos()),
formatTimestamp(log.getObservedTimestampEpochNanos()),
log.getSpanContext().isValid() ? log.getSpanContext().getTraceId() : "",
log.getSpanContext().isValid() ? log.getSpanContext().getSpanId() : "",
log.getSpanContext().getTraceFlags().asByte(),
log.getResource() != null
? ResourceTemplate.parse(log.getResource())
: new ResourceTemplate(new HashMap<>(), ""),
log.getInstrumentationScopeInfo() != null
? InstrumentationScopeTemplate.parse(log.getInstrumentationScopeInfo())
: new InstrumentationScopeTemplate("", "", ""));
}

private static String formatTimestamp(long nanos) {
return nanos != 0
? ISO_FORMAT.format(
Instant.ofEpochMilli(TimeUnit.NANOSECONDS.toMillis(nanos)).atZone(ZoneOffset.UTC))
: null;
}
}

@SuppressWarnings("unused")
private static final class ResourceTemplate {
@JsonProperty("attributes")
private final Map<String, String> attributes;

@JsonProperty("schemaUrl")
private final String schemaUrl;

private ResourceTemplate(Map<String, String> attributes, String schemaUrl) {
this.attributes = attributes;
this.schemaUrl = schemaUrl != null ? schemaUrl : "";
}

private static ResourceTemplate parse(Resource resource) {
Map<String, String> attributes = new HashMap<>();
if (resource == null) {
return new ResourceTemplate(attributes, "");
}
resource
.getAttributes()
.forEach((key, value) -> attributes.put(key.getKey(), String.valueOf(value)));
return new ResourceTemplate(attributes, resource.getSchemaUrl());
}
}

@SuppressWarnings("unused")
private static final class InstrumentationScopeTemplate {
@JsonProperty("name")
private final String name;

@JsonProperty("version")
private final String version;

@JsonProperty("schemaUrl")
private final String schemaUrl;

private InstrumentationScopeTemplate(String name, String version, String schemaUrl) {
this.name = name != null ? name : "";
this.version = version != null ? version : "";
this.schemaUrl = schemaUrl != null ? schemaUrl : "";
}

private static InstrumentationScopeTemplate parse(InstrumentationScopeInfo scope) {
if (scope == null) {
return new InstrumentationScopeTemplate("", "", "");
}
return new InstrumentationScopeTemplate(
scope.getName(), scope.getVersion(), scope.getSchemaUrl());
}
}
}
Loading
Loading