Skip to content

Commit bcf5eb2

Browse files
committed
initial implmentation
1 parent 8e6889d commit bcf5eb2

File tree

7 files changed

+1269
-22
lines changed

7 files changed

+1269
-22
lines changed

awsagentprovider/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ dependencies {
5353
runtimeOnly("software.amazon.awssdk:sts")
5454
implementation("software.amazon.awssdk:auth")
5555
implementation("software.amazon.awssdk:http-auth-aws")
56+
// For EMF exporter
57+
implementation("software.amazon.awssdk:cloudwatchlogs")
5658

5759
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
5860
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,40 @@
1818
import static software.amazon.opentelemetry.javaagent.providers.AwsApplicationSignalsCustomizerProvider.*;
1919

2020
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
21-
import java.util.Arrays;
21+
import java.util.*;
2222
import java.util.logging.Level;
2323
import java.util.logging.Logger;
2424
import java.util.regex.Pattern;
2525

2626
/** Utilities class to validate ADOT environment variable configuration. */
27-
public final class AwsApplicationSignalsConfigValidator {
27+
public final class AwsApplicationSignalsConfigUtils {
2828
private static final Logger logger =
2929
Logger.getLogger(AwsApplicationSignalsCustomizerProvider.class.getName());
3030

31+
/**
32+
* Removes "awsemf" from OTEL_METRICS_EXPORTER if present.
33+
*
34+
* @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
37+
*/
38+
static String removeEmfExporterIfEnabled(ConfigProperties configProps) {
39+
String metricExporters = configProps.getString(OTEL_METRICS_EXPORTER);
40+
41+
if (metricExporters == null || !metricExporters.contains("awsemf")) {
42+
return null;
43+
}
44+
45+
String[] exporters = metricExporters.split(",");
46+
List<String> filtered =
47+
Arrays.stream(exporters)
48+
.map(String::trim)
49+
.filter(exp -> !exp.equals("awsemf"))
50+
.collect(java.util.stream.Collectors.toList());
51+
52+
return filtered.isEmpty() ? "" : String.join(",", filtered);
53+
}
54+
3155
/**
3256
* Is the given configuration correct to enable SigV4 for Logs?
3357
*
@@ -61,27 +85,21 @@ static boolean isSigV4EnabledLogs(ConfigProperties config) {
6185

6286
if (logsHeaders == null || logsHeaders.isEmpty()) {
6387
logger.warning(
64-
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to include x-aws-log-group and x-aws-log-stream");
88+
String.format(
89+
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to include %s and %s",
90+
AWS_OTLP_LOGS_GROUP_HEADER, AWS_OTLP_LOGS_STREAM_HEADER));
6591

6692
return false;
6793
}
94+
Map<String, String> parsedHeaders =
95+
AwsApplicationSignalsConfigUtils.parseOtlpHeaders(logsHeaders);
6896

69-
long filteredLogHeaders =
70-
Arrays.stream(logsHeaders.split(","))
71-
.filter(
72-
pair -> {
73-
if (pair.contains("=")) {
74-
String key = pair.split("=", 2)[0];
75-
return key.equals(AWS_OTLP_LOGS_GROUP_HEADER)
76-
|| key.equals(AWS_OTLP_LOGS_STREAM_HEADER);
77-
}
78-
return false;
79-
})
80-
.count();
81-
82-
if (filteredLogHeaders != 2) {
97+
if (!(parsedHeaders.containsKey(AWS_OTLP_LOGS_GROUP_HEADER)
98+
&& parsedHeaders.containsKey(AWS_OTLP_LOGS_STREAM_HEADER))) {
8399
logger.warning(
84-
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to have values for x-aws-log-group and x-aws-log-stream");
100+
String.format(
101+
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to have values for %s and %s",
102+
AWS_OTLP_LOGS_GROUP_HEADER, AWS_OTLP_LOGS_STREAM_HEADER));
85103
return false;
86104
}
87105

@@ -168,4 +186,26 @@ private static boolean isSigv4ValidConfig(
168186

169187
return false;
170188
}
189+
190+
/**
191+
* Parse OTLP headers and return a map of header key to value. See: <a
192+
* href="https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_headers">...</a>
193+
*
194+
* @param headersString the headers string in format "key1=value1,key2=value2"
195+
* @return map of header keys to values
196+
*/
197+
static Map<String, String> parseOtlpHeaders(String headersString) {
198+
Map<String, String> headers = new HashMap<>();
199+
if (headersString == null || headersString.isEmpty()) {
200+
return headers;
201+
}
202+
203+
for (String pair : headersString.split(",")) {
204+
if (pair.contains("=")) {
205+
String[] keyValue = pair.split("=", 2);
206+
headers.put(keyValue[0].trim(), keyValue[1].trim());
207+
}
208+
}
209+
return headers;
210+
}
171211
}

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

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.util.logging.Level;
6666
import java.util.logging.Logger;
6767
import javax.annotation.concurrent.Immutable;
68+
import software.amazon.opentelemetry.javaagent.providers.exporter.aws.metrics.AwsCloudWatchEmfExporter;
6869
import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporterBuilder;
6970
import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporterBuilder;
7071

@@ -86,6 +87,9 @@
8687
@Immutable
8788
public final class AwsApplicationSignalsCustomizerProvider
8889
implements AutoConfigurationCustomizerProvider {
90+
// https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-envvars.html
91+
static final String AWS_REGION = "AWS_REGION";
92+
static final String AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION";
8993
static final String AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME";
9094
static final String LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT =
9195
"LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT";
@@ -103,6 +107,7 @@ public final class AwsApplicationSignalsCustomizerProvider
103107
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html#CloudWatch-LogsEndpoint
104108
static final String AWS_OTLP_LOGS_GROUP_HEADER = "x-aws-log-group";
105109
static final String AWS_OTLP_LOGS_STREAM_HEADER = "x-aws-log-stream";
110+
static final String AWS_EMF_METRICS_NAMESPACE = "x-aws-metric-namespace";
106111

107112
private static final String DEPRECATED_SMP_ENABLED_CONFIG = "otel.smp.enabled";
108113
private static final String DEPRECATED_APP_SIGNALS_ENABLED_CONFIG =
@@ -132,7 +137,7 @@ public final class AwsApplicationSignalsCustomizerProvider
132137
private static final String OTEL_BSP_MAX_EXPORT_BATCH_SIZE_CONFIG =
133138
"otel.bsp.max.export.batch.size";
134139

135-
private static final String OTEL_METRICS_EXPORTER = "otel.metrics.exporter";
140+
static final String OTEL_METRICS_EXPORTER = "otel.metrics.exporter";
136141
static final String OTEL_LOGS_EXPORTER = "otel.logs.exporter";
137142
static final String OTEL_TRACES_EXPORTER = "otel.traces.exporter";
138143
static final String OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "otel.exporter.otlp.traces.protocol";
@@ -161,6 +166,7 @@ public final class AwsApplicationSignalsCustomizerProvider
161166
private static final int LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10;
162167

163168
private Sampler sampler;
169+
private boolean isEmfExporterEnabled = false;
164170

165171
public void customize(AutoConfigurationCustomizer autoConfiguration) {
166172
autoConfiguration.addPropertiesCustomizer(this::customizeProperties);
@@ -171,6 +177,15 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) {
171177
autoConfiguration.addMeterProviderCustomizer(this::customizeMeterProvider);
172178
autoConfiguration.addSpanExporterCustomizer(this::customizeSpanExporter);
173179
autoConfiguration.addLogRecordExporterCustomizer(this::customizeLogsExporter);
180+
autoConfiguration.addMetricExporterCustomizer(this::customizeMetricExporter);
181+
}
182+
183+
private static Optional<String> getAwsRegionFromEnvironment() {
184+
String region = System.getenv(AWS_REGION);
185+
if (region != null) {
186+
return Optional.of(region);
187+
}
188+
return Optional.ofNullable(System.getenv(AWS_DEFAULT_REGION));
174189
}
175190

176191
static boolean isLambdaEnvironment() {
@@ -194,6 +209,14 @@ private Map<String, String> customizeProperties(ConfigProperties configProps) {
194209
Map<String, String> propsOverride = new HashMap<>();
195210
boolean isLambdaEnvironment = isLambdaEnvironment();
196211

212+
// Check if awsemf was specified and remove it from OTEL_METRICS_EXPORTER
213+
String filteredExporters =
214+
AwsApplicationSignalsConfigUtils.removeEmfExporterIfEnabled(configProps);
215+
if (filteredExporters != null) {
216+
this.isEmfExporterEnabled = true;
217+
propsOverride.put(OTEL_METRICS_EXPORTER, filteredExporters);
218+
}
219+
197220
// Enable AWS Resource Providers
198221
propsOverride.put(OTEL_RESOURCE_PROVIDERS_AWS_ENABLED, "true");
199222

@@ -394,7 +417,6 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder(
394417

395418
private SdkMeterProviderBuilder customizeMeterProvider(
396419
SdkMeterProviderBuilder sdkMeterProviderBuilder, ConfigProperties configProps) {
397-
398420
if (isApplicationSignalsRuntimeEnabled(configProps)) {
399421
Set<String> registeredScopeNames = new HashSet<>(1);
400422
String jmxRuntimeScopeName = "io.opentelemetry.jmx";
@@ -434,7 +456,7 @@ SpanExporter customizeSpanExporter(SpanExporter spanExporter, ConfigProperties c
434456
}
435457
}
436458

437-
if (AwsApplicationSignalsConfigValidator.isSigV4EnabledTraces(configProps)) {
459+
if (AwsApplicationSignalsConfigUtils.isSigV4EnabledTraces(configProps)) {
438460
// can cast here since we've checked that the configuration for OTEL_TRACES_EXPORTER is otlp
439461
// and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL is http/protobuf
440462
// so the given spanExporter will be an instance of OtlpHttpSpanExporter
@@ -480,7 +502,7 @@ private boolean isOtlpSpanExporter(SpanExporter spanExporter) {
480502

481503
LogRecordExporter customizeLogsExporter(
482504
LogRecordExporter logsExporter, ConfigProperties configProps) {
483-
if (AwsApplicationSignalsConfigValidator.isSigV4EnabledLogs(configProps)) {
505+
if (AwsApplicationSignalsConfigUtils.isSigV4EnabledLogs(configProps)) {
484506
// can cast here since we've checked that the configuration for OTEL_LOGS_EXPORTER is otlp and
485507
// OTEL_EXPORTER_OTLP_LOGS_PROTOCOL is http/protobuf
486508
// so the given logsExporter will be an instance of OtlpHttpLogRecorderExporter
@@ -509,6 +531,40 @@ LogRecordExporter customizeLogsExporter(
509531
return logsExporter;
510532
}
511533

534+
MetricExporter customizeMetricExporter(
535+
MetricExporter metricExporter, ConfigProperties configProps) {
536+
if (isEmfExporterEnabled) {
537+
Map<String, String> headers =
538+
AwsApplicationSignalsConfigUtils.parseOtlpHeaders(
539+
configProps.getString(OTEL_EXPORTER_OTLP_LOGS_HEADERS));
540+
Optional<String> awsRegion = getAwsRegionFromEnvironment();
541+
if (awsRegion.isPresent()) {
542+
if (headers.containsKey(AWS_OTLP_LOGS_GROUP_HEADER)
543+
&& headers.containsKey(AWS_OTLP_LOGS_STREAM_HEADER)
544+
&& headers.containsKey(AWS_EMF_METRICS_NAMESPACE)) {
545+
String namespace = headers.get(AWS_EMF_METRICS_NAMESPACE);
546+
String logGroup = headers.get(AWS_OTLP_LOGS_GROUP_HEADER);
547+
String logStream = headers.get(AWS_OTLP_LOGS_STREAM_HEADER);
548+
return new AwsCloudWatchEmfExporter(namespace, logGroup, logStream, awsRegion.get());
549+
}
550+
logger.warning(
551+
String.format(
552+
"Improper configuration: Please configure the environment variable OTEL_EXPORTER_OTLP_LOGS_HEADERS to have values for %s, %s, and %s",
553+
AWS_OTLP_LOGS_GROUP_HEADER,
554+
AWS_OTLP_LOGS_STREAM_HEADER,
555+
AWS_EMF_METRICS_NAMESPACE));
556+
557+
} else {
558+
logger.warning(
559+
String.format(
560+
"Improper configuration: AWS region not found in environment variables please set %s or %s",
561+
AWS_REGION, AWS_DEFAULT_REGION));
562+
}
563+
}
564+
565+
return metricExporter;
566+
}
567+
512568
static AwsXrayAdaptiveSamplingConfig parseConfigString(String config)
513569
throws JsonProcessingException {
514570
if (config == null) {

0 commit comments

Comments
 (0)