Skip to content

Commit 5276407

Browse files
committed
add emf exporter to changelog
1 parent cd1475d commit 5276407

File tree

6 files changed

+180
-20
lines changed

6 files changed

+180
-20
lines changed

awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsConfigUtils.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ public final class AwsApplicationSignalsConfigUtils {
3232
* Removes "awsemf" from OTEL_METRICS_EXPORTER if present.
3333
*
3434
* @param configProps the configuration properties
35-
* @return string with "awsemf" removed if the original OTEL_METRICS_EXPORTER contains "awsemf",
36-
* otherwise null if "awsemf" is not found
35+
* @return Optional containing string with "awsemf" removed if the original OTEL_METRICS_EXPORTER
36+
* contains "awsemf", otherwise empty Optional if "awsemf" is not found
3737
*/
38-
static String removeEmfExporterIfEnabled(ConfigProperties configProps) {
38+
static Optional<String> removeEmfExporterIfEnabled(ConfigProperties configProps) {
3939
String metricExporters = configProps.getString(OTEL_METRICS_EXPORTER);
4040

4141
if (metricExporters == null || !metricExporters.contains("awsemf")) {
42-
return null;
42+
return Optional.empty();
4343
}
4444

4545
String[] exporters = metricExporters.split(",");
@@ -49,7 +49,10 @@ static String removeEmfExporterIfEnabled(ConfigProperties configProps) {
4949
.filter(exp -> !exp.equals("awsemf"))
5050
.collect(java.util.stream.Collectors.toList());
5151

52-
return filtered.isEmpty() ? "" : String.join(",", filtered);
52+
// Return empty string instead of "none" because upstream will not call
53+
// customizeMetricExporter if OTEL_METRICS_EXPORTER is set to "none" as it assumes
54+
// no metrics exporter is configured
55+
return Optional.of(filtered.isEmpty() ? "" : String.join(",", filtered));
5356
}
5457

5558
/**

awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,11 @@ private Map<String, String> customizeProperties(ConfigProperties configProps) {
210210
boolean isLambdaEnvironment = isLambdaEnvironment();
211211

212212
// Check if awsemf was specified and remove it from OTEL_METRICS_EXPORTER
213-
String filteredExporters =
213+
Optional<String> filteredExporters =
214214
AwsApplicationSignalsConfigUtils.removeEmfExporterIfEnabled(configProps);
215-
if (filteredExporters != null) {
215+
if (filteredExporters.isPresent()) {
216216
this.isEmfExporterEnabled = true;
217-
propsOverride.put(OTEL_METRICS_EXPORTER, filteredExporters);
217+
propsOverride.put(OTEL_METRICS_EXPORTER, filteredExporters.get());
218218
}
219219

220220
// Enable AWS Resource Providers
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.opentelemetry.javaagent.providers.exporter.aws.common;
17+
18+
import java.util.Map;
19+
20+
/** Interface for emitting log events. */
21+
public interface LogEventEmitter {
22+
/**
23+
* Emit a log event.
24+
*
25+
* @param logEvent The log event to emit
26+
*/
27+
void emit(Map<String, Object> logEvent);
28+
}

awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/aws/metrics/AwsCloudWatchEmfExporter.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
import java.util.UUID;
2525
import java.util.logging.Logger;
2626
import software.amazon.awssdk.awscore.exception.AwsServiceException;
27+
import software.amazon.awssdk.regions.Region;
2728
import software.amazon.awssdk.retries.StandardRetryStrategy;
2829
import software.amazon.awssdk.retries.api.BackoffStrategy;
2930
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient;
3031
import software.amazon.awssdk.services.cloudwatchlogs.model.*;
3132
import software.amazon.opentelemetry.javaagent.providers.exporter.aws.common.BaseEmfExporter;
33+
import software.amazon.opentelemetry.javaagent.providers.exporter.aws.common.LogEventEmitter;
3234

3335
/**
3436
* OpenTelemetry metrics exporter for CloudWatch EMF format.
@@ -42,26 +44,32 @@
4244
public class AwsCloudWatchEmfExporter extends BaseEmfExporter {
4345
private static final Logger logger = Logger.getLogger(AwsCloudWatchEmfExporter.class.getName());
4446

45-
private final CloudWatchLogsClientWrapper logsClientWrapper;
47+
private final LogEventEmitter emitter;
4648

4749
/**
4850
* Initialize the CloudWatch EMF exporter.
4951
*
5052
* @param namespace CloudWatch namespace for metrics (default: "default")
5153
* @param logGroupName CloudWatch log group name
5254
* @param logStreamName CloudWatch log stream name (auto-generated if null)
53-
* @param awsRegion AWS region (auto-detected if null)
55+
* @param awsRegion AWS region
5456
*/
5557
public AwsCloudWatchEmfExporter(
5658
String namespace, String logGroupName, String logStreamName, String awsRegion) {
5759
super(namespace);
58-
this.logsClientWrapper =
59-
new CloudWatchLogsClientWrapper(logGroupName, logStreamName, awsRegion);
60+
this.emitter = new CloudWatchLogsClientWrapper(logGroupName, logStreamName, awsRegion);
61+
}
62+
63+
public AwsCloudWatchEmfExporter(String namespace, LogEventEmitter emitter) {
64+
super(namespace);
65+
this.emitter = emitter;
6066
}
6167

6268
@Override
6369
public CompletableResultCode flush() {
64-
this.logsClientWrapper.flushPendingEvents();
70+
if (emitter instanceof CloudWatchLogsClientWrapper) {
71+
((CloudWatchLogsClientWrapper) emitter).flushPendingEvents();
72+
}
6573
logger.fine("AwsCloudWatchEmfExporter force flushes the buffered metrics");
6674
return CompletableResultCode.ofSuccess();
6775
}
@@ -75,7 +83,7 @@ public CompletableResultCode shutdown() {
7583

7684
@Override
7785
protected void emit(Map<String, Object> logEvent) {
78-
this.logsClientWrapper.sendLogEvent(logEvent);
86+
this.emitter.emit(logEvent);
7987
}
8088

8189
private static StandardRetryStrategy createExponentialBackoffRetryStrategy() {
@@ -100,7 +108,7 @@ private static StandardRetryStrategy createExponentialBackoffRetryStrategy() {
100108
* <p>This class handles the batching logic and CloudWatch Logs API interactions for sending EMF
101109
* logs efficiently while respecting CloudWatch Logs constraints.
102110
*/
103-
private static class CloudWatchLogsClientWrapper {
111+
private static class CloudWatchLogsClientWrapper implements LogEventEmitter {
104112
private static final Logger logger = Logger.getLogger(AwsCloudWatchEmfExporter.class.getName());
105113

106114
// Constants for CloudWatch Logs limits
@@ -110,7 +118,7 @@ private static class CloudWatchLogsClientWrapper {
110118
private static final int CW_MAX_REQUEST_EVENT_COUNT = 10000;
111119
private static final int CW_PER_EVENT_HEADER_BYTES = 26;
112120
private static final long BATCH_FLUSH_INTERVAL = 60 * 1000; // 60 seconds
113-
private static final int CW_MAX_REQUEST_PAYLOAD_BYTES = 1 * 1024 * 1024; // 1MB
121+
private static final int CW_MAX_REQUEST_PAYLOAD_BYTES = 1024 * 1024; // 1MB
114122
private static final String CW_TRUNCATED_SUFFIX = "[Truncated...]";
115123
// None of the log events in the batch can be older than 14 days
116124
private static final long CW_EVENT_TIMESTAMP_LIMIT_PAST = 14 * 24 * 60 * 60 * 1000L;
@@ -120,19 +128,21 @@ private static class CloudWatchLogsClientWrapper {
120128
private CloudWatchLogsClient logsClient;
121129
private final String logGroupName;
122130
private final String logStreamName;
131+
private final String awsRegion;
123132
private LogEventBatch eventBatch;
124133

125134
/**
126135
* Initialize the CloudWatch Logs client wrapper.
127136
*
128137
* @param logGroupName CloudWatch log group name
129138
* @param logStreamName CloudWatch log stream name (auto-generated if null)
130-
* @param awsRegion AWS region (auto-detected if null)
139+
* @param awsRegion AWS region
131140
*/
132-
public CloudWatchLogsClientWrapper(
141+
private CloudWatchLogsClientWrapper(
133142
String logGroupName, String logStreamName, String awsRegion) {
134143
this.logGroupName = logGroupName;
135144
this.logStreamName = logStreamName != null ? logStreamName : generateLogStreamName();
145+
this.awsRegion = awsRegion;
136146
}
137147

138148
private CloudWatchLogsClient getLogsClient() {
@@ -145,6 +155,7 @@ private CloudWatchLogsClient getLogsClient() {
145155
CloudWatchLogsClient.builder()
146156
.overrideConfiguration(
147157
config -> config.retryStrategy(createExponentialBackoffRetryStrategy()).build())
158+
.region(Region.of(this.awsRegion))
148159
.build();
149160
}
150161
return this.logsClient;
@@ -191,7 +202,8 @@ private void createLogStreamIfNeeded() {
191202
}
192203
}
193204

194-
private void sendLogEvent(Map<String, Object> logEvent) {
205+
@Override
206+
public void emit(Map<String, Object> logEvent) {
195207
try {
196208
if (!isValidLogEvent(logEvent)) {
197209
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
20+
21+
import io.opentelemetry.api.common.Attributes;
22+
import io.opentelemetry.sdk.metrics.data.DoublePointData;
23+
import io.opentelemetry.sdk.metrics.data.GaugeData;
24+
import io.opentelemetry.sdk.metrics.data.MetricData;
25+
import io.opentelemetry.sdk.metrics.data.SumData;
26+
import io.opentelemetry.sdk.metrics.export.MetricExporter;
27+
import io.opentelemetry.sdk.resources.Resource;
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import java.util.Map;
31+
import org.junit.jupiter.api.BeforeEach;
32+
import org.junit.jupiter.api.Test;
33+
import org.junit.jupiter.api.extension.ExtendWith;
34+
import org.mockito.junit.jupiter.MockitoExtension;
35+
import org.mockito.junit.jupiter.MockitoSettings;
36+
import org.mockito.quality.Strictness;
37+
import software.amazon.opentelemetry.javaagent.providers.exporter.aws.common.LogEventEmitter;
38+
39+
@ExtendWith(MockitoExtension.class)
40+
@MockitoSettings(strictness = Strictness.LENIENT)
41+
public class AwsCloudWatchEmfExporterTest {
42+
private static final String NAMESPACE = "test-namespace";
43+
private MetricExporter exporter;
44+
private LogEventEmitter mockEmitter;
45+
private List<Map<String, Object>> capturedLogEvents;
46+
47+
@BeforeEach
48+
void setup() {
49+
mockEmitter = mock(LogEventEmitter.class);
50+
capturedLogEvents = new ArrayList<>();
51+
52+
doAnswer(
53+
invocation -> {
54+
capturedLogEvents.add(invocation.getArgument(0));
55+
return null;
56+
})
57+
.when(mockEmitter)
58+
.emit(any());
59+
60+
exporter = new AwsCloudWatchEmfExporter(NAMESPACE, mockEmitter);
61+
}
62+
63+
@Test
64+
void testExporterCreation() {}
65+
66+
@Test
67+
void testGaugeAndSumMetricProcessing() {
68+
MetricData gaugeMetric = createGaugeMetricWithPoint("test.gauge", 42.0);
69+
MetricData sumMetric = createSumMetricWithPoint("test.sum", 100L);
70+
71+
exporter.export(List.of(gaugeMetric, sumMetric));
72+
73+
// Validate that log events were emitted
74+
assertEquals(2, capturedLogEvents.size());
75+
76+
for (Map<String, Object> logEvent : capturedLogEvents) {
77+
assertNotNull(logEvent.get("message"));
78+
assertNotNull(logEvent.get("timestamp"));
79+
}
80+
}
81+
82+
private MetricData createGaugeMetricWithPoint(String name, double value) {
83+
MetricData metricData = mock(MetricData.class);
84+
GaugeData gaugeData = mock(GaugeData.class);
85+
86+
DoublePointData pointData = mock(DoublePointData.class);
87+
88+
when(metricData.getName()).thenReturn(name);
89+
when(metricData.getUnit()).thenReturn("1");
90+
when(metricData.getData()).thenReturn(gaugeData);
91+
when(metricData.getResource()).thenReturn(Resource.getDefault());
92+
when(gaugeData.getPoints()).thenReturn(List.of(pointData));
93+
94+
when(pointData.getValue()).thenReturn(value);
95+
when(pointData.getAttributes()).thenReturn(Attributes.empty());
96+
when(pointData.getEpochNanos()).thenReturn(System.nanoTime());
97+
98+
return metricData;
99+
}
100+
101+
private MetricData createSumMetricWithPoint(String name, long value) {
102+
MetricData metricData = mock(MetricData.class);
103+
SumData sumData = mock(SumData.class);
104+
DoublePointData pointData = mock(DoublePointData.class);
105+
106+
when(metricData.getName()).thenReturn(name);
107+
when(metricData.getUnit()).thenReturn("1");
108+
when(metricData.getData()).thenReturn(sumData);
109+
when(metricData.getResource()).thenReturn(Resource.getDefault());
110+
when(sumData.getPoints()).thenReturn(List.of(pointData));
111+
when(pointData.getValue()).thenReturn((double) value);
112+
when(pointData.getAttributes()).thenReturn(Attributes.empty());
113+
when(pointData.getEpochNanos()).thenReturn(System.nanoTime());
114+
115+
return metricData;
116+
}
117+
}

awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/OtlpAwsExporterTest.java renamed to awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/OtlpAwsExporterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* permissions and limitations under the License.
1414
*/
1515

16-
package software.amazon.opentelemetry.javaagent.providers;
16+
package software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws;
1717

1818
import static org.junit.jupiter.api.Assertions.*;
1919
import static org.mockito.ArgumentMatchers.any;

0 commit comments

Comments
 (0)