Skip to content

Commit e7ac70c

Browse files
authored
SigV4 Authentication support for http exporter (#1046)
**Background** Supporting ADOT auto instrumentation to automatically inject SigV4 authentication headers for outgoing export trace requests to the allow exporting to the AWS XRay OTLP endpoint. Users will need to configure the following environment variables in order to enable and properly run this exporter: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://xray.[AWS-REGION].amazonaws.com/v1/traces; **required** OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf **required** OTEL_LOGS_EXPORTER=none; OTEL_METRICS_EXPORTER=none; **Description of changes:** 1. Added new OtlpAwsSpanExporter class which uses composition to extend upstream's OtlpHttpSpanExporter which is responsible for making the http/grpc client calls to export traces to the given endpoint. 2. The OtlpAwsSpanExporter customizes the headers by adding an intermediary step to sign the request with SigV4 authentication and injects the signed headers to the outgoing trace request 3. In order to ensure we don't override any user configurations from environment variables, the OtlpAwsSpanExporter constructor copies any existing configurations create by upstream's auto instrumentation except for the following environment variable: OTEL_EXPORTER_OTLP_TRACES_HEADERS Any headers set here will not be acknowledged within this exporter. 4. The ADOT auto instrumentation is now configured to automatically detect if a user is exporting to XRay OTLP endpoint by checking if the environment variable OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is configured to match this url pattern: https://xray.[AWS-REGION].amazonaws.com/v1/traces AND by checking if OTEL_EXPORTER_OTLP_TRACES_PROTOCOL is set to http/protobuf **Testing:** 1. Unit tests were added to ensure that the behavior of the exporter was properly injecting SigV4 headers before making the outgoing request 2. Manual testing was done by configuring the above environment variables and setting up the sample app locally with ADOT auto instrumentation and verified the spans in CW Logs. 3. The sample app was run and rerun 30 times and confirmed issues exporting the traces to the endpoint Further testing will be done with the Release tests. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 594392f commit e7ac70c

File tree

5 files changed

+500
-0
lines changed

5 files changed

+500
-0
lines changed

awsagentprovider/build.gradle.kts

+5
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,17 @@ dependencies {
4545
// For Udp emitter
4646
compileOnly("io.opentelemetry:opentelemetry-exporter-otlp-common")
4747

48+
// For OtlpAwsSpanExporter SigV4 Authentication
49+
implementation("software.amazon.awssdk:auth")
50+
implementation("software.amazon.awssdk:http-auth-aws")
51+
4852
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
4953
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
5054
testImplementation("io.opentelemetry:opentelemetry-extension-aws")
5155
testImplementation("io.opentelemetry:opentelemetry-extension-trace-propagators")
5256
testImplementation("com.google.guava:guava")
5357
testRuntimeOnly("io.opentelemetry:opentelemetry-exporter-otlp-common")
58+
testImplementation("io.opentelemetry:opentelemetry-exporter-otlp")
5459

5560
compileOnly("com.google.code.findbugs:jsr305:3.0.2")
5661
testImplementation("org.mockito:mockito-core:5.14.2")

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

+63
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import java.util.Set;
5252
import java.util.logging.Level;
5353
import java.util.logging.Logger;
54+
import java.util.regex.Pattern;
5455

5556
/**
5657
* This customizer performs the following customizations:
@@ -70,6 +71,8 @@
7071
public class AwsApplicationSignalsCustomizerProvider
7172
implements AutoConfigurationCustomizerProvider {
7273
static final String AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME";
74+
private static final String XRAY_OTLP_ENDPOINT_PATTERN =
75+
"^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$";
7376

7477
private static final Duration DEFAULT_METRIC_EXPORT_INTERVAL = Duration.ofMinutes(1);
7578
private static final Logger logger =
@@ -95,6 +98,9 @@ public class AwsApplicationSignalsCustomizerProvider
9598
private static final String OTEL_JMX_TARGET_SYSTEM_CONFIG = "otel.jmx.target.system";
9699
private static final String OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG =
97100
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
101+
private static final String OTEL_EXPORTER_HTTP_PROTOBUF_PROTOCOL = "http/protobuf";
102+
private static final String OTEL_EXPORTER_OTLP_TRACES_PROTOCOL_CONFIG =
103+
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
98104
private static final String AWS_XRAY_DAEMON_ADDRESS_CONFIG = "AWS_XRAY_DAEMON_ADDRESS";
99105
private static final String DEFAULT_UDP_ENDPOINT = "127.0.0.1:2000";
100106
private static final String OTEL_DISABLED_RESOURCE_PROVIDERS_CONFIG =
@@ -116,8 +122,10 @@ public class AwsApplicationSignalsCustomizerProvider
116122
// This is a bit of a magic number, as there is no simple way to tell how many spans can make a
117123
// 64KB batch since spans can vary in size.
118124
private static final int LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10;
125+
private static boolean isSigV4Enabled = false;
119126

120127
public void customize(AutoConfigurationCustomizer autoConfiguration) {
128+
isSigV4Enabled = AwsApplicationSignalsCustomizerProvider.isSigV4Enabled();
121129
autoConfiguration.addPropertiesCustomizer(this::customizeProperties);
122130
autoConfiguration.addPropertiesCustomizer(this::customizeLambdaEnvProperties);
123131
autoConfiguration.addResourceCustomizer(this::customizeResource);
@@ -131,6 +139,47 @@ static boolean isLambdaEnvironment() {
131139
return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG) != null;
132140
}
133141

142+
static boolean isSigV4Enabled() {
143+
String otlpEndpoint = System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG);
144+
boolean isXrayOtlpEndpoint;
145+
try {
146+
isXrayOtlpEndpoint =
147+
otlpEndpoint != null
148+
&& Pattern.compile(XRAY_OTLP_ENDPOINT_PATTERN)
149+
.matcher(otlpEndpoint.toLowerCase())
150+
.matches();
151+
152+
if (isXrayOtlpEndpoint) {
153+
logger.log(Level.INFO, "Detected using AWS OTLP XRay Endpoint.");
154+
155+
String otlpTracesProtocol = System.getenv(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL_CONFIG);
156+
157+
if (otlpTracesProtocol == null
158+
|| !otlpTracesProtocol.equals(OTEL_EXPORTER_HTTP_PROTOBUF_PROTOCOL)) {
159+
logger.info(
160+
String.format(
161+
"Improper configuration: Please configure your environment variables and export/set %s=%s",
162+
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL_CONFIG, OTEL_EXPORTER_HTTP_PROTOBUF_PROTOCOL));
163+
return false;
164+
}
165+
166+
logger.info(
167+
String.format(
168+
"Proper configuration detected: Now exporting trace span data to %s",
169+
otlpEndpoint));
170+
return true;
171+
}
172+
} catch (Exception e) {
173+
logger.log(
174+
Level.WARNING,
175+
String.format(
176+
"Caught error while attempting to validate configuration to export traces to XRay OTLP endpoint: %s",
177+
e.getMessage()));
178+
}
179+
180+
return false;
181+
}
182+
134183
private boolean isApplicationSignalsEnabled(ConfigProperties configProps) {
135184
return configProps.getBoolean(
136185
APPLICATION_SIGNALS_ENABLED_CONFIG,
@@ -257,6 +306,10 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder(
257306
return tracerProviderBuilder;
258307
}
259308

309+
if (isSigV4Enabled) {
310+
return tracerProviderBuilder;
311+
}
312+
260313
// Construct meterProvider
261314
MetricExporter metricsExporter =
262315
ApplicationSignalsExporterProvider.INSTANCE.createExporter(configProps);
@@ -323,6 +376,16 @@ private SpanExporter customizeSpanExporter(
323376
}
324377
}
325378

379+
// When running OTLP endpoint for X-Ray backend, use custom exporter for SigV4 authentication.
380+
if (isSigV4Enabled) {
381+
// can cast here since we've checked that the environment variable is
382+
// set to http/protobuf
383+
return OtlpAwsSpanExporterBuilder.create(
384+
(OtlpHttpSpanExporter) spanExporter,
385+
System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG))
386+
.build();
387+
}
388+
326389
if (isApplicationSignalsEnabled(configProps)) {
327390
return AwsMetricAttributesSpanExporterBuilder.create(
328391
spanExporter, ResourceHolder.getResource())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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;
17+
18+
import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler;
19+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
20+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
21+
import io.opentelemetry.sdk.common.CompletableResultCode;
22+
import io.opentelemetry.sdk.common.export.MemoryMode;
23+
import io.opentelemetry.sdk.trace.data.SpanData;
24+
import io.opentelemetry.sdk.trace.export.SpanExporter;
25+
import java.io.ByteArrayInputStream;
26+
import java.io.ByteArrayOutputStream;
27+
import java.net.URI;
28+
import java.util.Collection;
29+
import java.util.Collections;
30+
import java.util.HashMap;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.StringJoiner;
34+
import java.util.concurrent.atomic.AtomicReference;
35+
import java.util.function.Supplier;
36+
import java.util.logging.Level;
37+
import java.util.logging.Logger;
38+
import javax.annotation.Nonnull;
39+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
40+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
41+
import software.amazon.awssdk.http.SdkHttpFullRequest;
42+
import software.amazon.awssdk.http.SdkHttpMethod;
43+
import software.amazon.awssdk.http.SdkHttpRequest;
44+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
45+
import software.amazon.awssdk.http.auth.spi.signer.SignedRequest;
46+
47+
/**
48+
* This exporter extends the functionality of the OtlpHttpSpanExporter to allow spans to be exported
49+
* to the XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the AWSSDK
50+
* library to sign and directly inject SigV4 Authentication to the exported request's headers. <a
51+
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
52+
*/
53+
public class OtlpAwsSpanExporter implements SpanExporter {
54+
private static final String SERVICE_NAME = "xray";
55+
private static final Logger logger = Logger.getLogger(OtlpAwsSpanExporter.class.getName());
56+
57+
private final OtlpHttpSpanExporterBuilder parentExporterBuilder;
58+
private final OtlpHttpSpanExporter parentExporter;
59+
private final AtomicReference<Collection<SpanData>> spanData;
60+
private final String awsRegion;
61+
private final String endpoint;
62+
63+
static OtlpAwsSpanExporter getDefault(String endpoint) {
64+
return new OtlpAwsSpanExporter(endpoint);
65+
}
66+
67+
static OtlpAwsSpanExporter create(OtlpHttpSpanExporter parent, String endpoint) {
68+
return new OtlpAwsSpanExporter(parent, endpoint);
69+
}
70+
71+
private OtlpAwsSpanExporter(String endpoint) {
72+
this(null, endpoint);
73+
}
74+
75+
private OtlpAwsSpanExporter(OtlpHttpSpanExporter parentExporter, String endpoint) {
76+
this.awsRegion = endpoint.split("\\.")[1];
77+
this.endpoint = endpoint;
78+
this.spanData = new AtomicReference<>(Collections.emptyList());
79+
80+
if (parentExporter == null) {
81+
this.parentExporterBuilder =
82+
OtlpHttpSpanExporter.builder()
83+
.setMemoryMode(MemoryMode.IMMUTABLE_DATA)
84+
.setEndpoint(endpoint)
85+
.setHeaders(new SigV4AuthHeaderSupplier());
86+
this.parentExporter = this.parentExporterBuilder.build();
87+
return;
88+
}
89+
this.parentExporterBuilder =
90+
parentExporter.toBuilder()
91+
.setMemoryMode(MemoryMode.IMMUTABLE_DATA)
92+
.setEndpoint(endpoint)
93+
.setHeaders(new SigV4AuthHeaderSupplier());
94+
this.parentExporter = this.parentExporterBuilder.build();
95+
}
96+
97+
/**
98+
* Overrides the upstream implementation of export. All behaviors are the same except if the
99+
* endpoint is an XRay OTLP endpoint, we will sign the request with SigV4 in headers before
100+
* sending it to the endpoint. Otherwise, we will skip signing.
101+
*/
102+
@Override
103+
public CompletableResultCode export(@Nonnull Collection<SpanData> spans) {
104+
this.spanData.set(spans);
105+
return this.parentExporter.export(spans);
106+
}
107+
108+
@Override
109+
public CompletableResultCode flush() {
110+
return this.parentExporter.flush();
111+
}
112+
113+
@Override
114+
public CompletableResultCode shutdown() {
115+
return this.parentExporter.shutdown();
116+
}
117+
118+
@Override
119+
public String toString() {
120+
StringJoiner joiner = new StringJoiner(", ", "OtlpAwsSpanExporter{", "}");
121+
joiner.add(this.parentExporterBuilder.toString());
122+
joiner.add("memoryMode=" + MemoryMode.IMMUTABLE_DATA);
123+
return joiner.toString();
124+
}
125+
126+
private final class SigV4AuthHeaderSupplier implements Supplier<Map<String, String>> {
127+
128+
@Override
129+
public Map<String, String> get() {
130+
try {
131+
Collection<SpanData> spans = OtlpAwsSpanExporter.this.spanData.get();
132+
ByteArrayOutputStream encodedSpans = new ByteArrayOutputStream();
133+
TraceRequestMarshaler.create(spans).writeBinaryTo(encodedSpans);
134+
135+
SdkHttpRequest httpRequest =
136+
SdkHttpFullRequest.builder()
137+
.uri(URI.create(OtlpAwsSpanExporter.this.endpoint))
138+
.method(SdkHttpMethod.POST)
139+
.putHeader("Content-Type", "application/x-protobuf")
140+
.contentStreamProvider(() -> new ByteArrayInputStream(encodedSpans.toByteArray()))
141+
.build();
142+
143+
AwsCredentials credentials = DefaultCredentialsProvider.create().resolveCredentials();
144+
145+
SignedRequest signedRequest =
146+
AwsV4HttpSigner.create()
147+
.sign(
148+
b ->
149+
b.identity(credentials)
150+
.request(httpRequest)
151+
.putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, SERVICE_NAME)
152+
.putProperty(
153+
AwsV4HttpSigner.REGION_NAME, OtlpAwsSpanExporter.this.awsRegion)
154+
.payload(() -> new ByteArrayInputStream(encodedSpans.toByteArray())));
155+
156+
Map<String, String> result = new HashMap<>();
157+
158+
Map<String, List<String>> headers = signedRequest.request().headers();
159+
headers.forEach(
160+
(key, values) -> {
161+
if (!values.isEmpty()) {
162+
result.put(key, values.get(0));
163+
}
164+
});
165+
166+
return result;
167+
168+
} catch (Exception e) {
169+
logger.log(
170+
Level.WARNING,
171+
String.format(
172+
"Failed to sign/authenticate the given exported Span request to OTLP CloudWatch endpoint with error: %s",
173+
e.getMessage()));
174+
175+
return Collections.emptyMap();
176+
}
177+
}
178+
}
179+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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;
17+
18+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
19+
20+
public class OtlpAwsSpanExporterBuilder {
21+
private final OtlpHttpSpanExporter parentExporter;
22+
private final String endpoint;
23+
24+
public static OtlpAwsSpanExporterBuilder create(
25+
OtlpHttpSpanExporter parentExporter, String endpoint) {
26+
return new OtlpAwsSpanExporterBuilder(parentExporter, endpoint);
27+
}
28+
29+
public static OtlpAwsSpanExporter getDefault(String endpoint) {
30+
return OtlpAwsSpanExporter.getDefault(endpoint);
31+
}
32+
33+
private OtlpAwsSpanExporterBuilder(OtlpHttpSpanExporter parentExporter, String endpoint) {
34+
this.parentExporter = parentExporter;
35+
this.endpoint = endpoint;
36+
}
37+
38+
public OtlpAwsSpanExporter build() {
39+
return OtlpAwsSpanExporter.create(this.parentExporter, this.endpoint);
40+
}
41+
}

0 commit comments

Comments
 (0)