Skip to content

Commit 83e5ade

Browse files
SigV4 Authentication support for http exporter (#1019)
Issue #, if available: Adding SigV4 Authentication extension for Exporting traces to OTLP CloudWatch endpoint without needing to explictily install the collector. Description of changes: Added a new class that extends upstream's OTLP http span exporter. Overrides the export method so that if the endpoint is CW, we add an extra step of injecting SigV4 authentication to the headers. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Mahad Janjua <[email protected]>
1 parent 51bec57 commit 83e5ade

File tree

4 files changed

+399
-1
lines changed

4 files changed

+399
-1
lines changed

awsagentprovider/build.gradle.kts

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ dependencies {
4141
// Import AWS SDK v1 core for ARN parsing utilities
4242
implementation("com.amazonaws:aws-java-sdk-core:1.12.773")
4343
// Export configuration
44-
compileOnly("io.opentelemetry:opentelemetry-exporter-otlp")
44+
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
4545
// For Udp emitter
4646
compileOnly("io.opentelemetry:opentelemetry-exporter-otlp-common")
47+
// For HTTP SigV4 emitter
48+
implementation("software.amazon.awssdk:auth:2.30.14")
49+
implementation("software.amazon.awssdk:http-auth-aws:2.30.14")
4750

4851
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
4952
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")

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

+25
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 =
@@ -121,6 +124,16 @@ static boolean isLambdaEnvironment() {
121124
return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG) != null;
122125
}
123126

127+
static boolean isXrayOtlpEndpoint(String otlpEndpoint) {
128+
if (otlpEndpoint == null) {
129+
return false;
130+
}
131+
132+
return Pattern.compile(XRAY_OTLP_ENDPOINT_PATTERN)
133+
.matcher(otlpEndpoint.toLowerCase())
134+
.matches();
135+
}
136+
124137
private boolean isApplicationSignalsEnabled(ConfigProperties configProps) {
125138
return configProps.getBoolean(
126139
APPLICATION_SIGNALS_ENABLED_CONFIG,
@@ -221,6 +234,10 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder(
221234
return tracerProviderBuilder;
222235
}
223236

237+
if (isXrayOtlpEndpoint(System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG))) {
238+
return tracerProviderBuilder;
239+
}
240+
224241
// Construct meterProvider
225242
MetricExporter metricsExporter =
226243
ApplicationSignalsExporterProvider.INSTANCE.createExporter(configProps);
@@ -286,6 +303,14 @@ private SpanExporter customizeSpanExporter(
286303
.build();
287304
}
288305
}
306+
// When running OTLP endpoint for X-Ray backend, use custom exporter for SigV4 authentication
307+
else if (spanExporter instanceof OtlpHttpSpanExporter
308+
&& isXrayOtlpEndpoint(System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG))) {
309+
spanExporter =
310+
new OtlpAwsSpanExporter(
311+
(OtlpHttpSpanExporter) spanExporter,
312+
System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG));
313+
}
289314

290315
if (isApplicationSignalsEnabled(configProps)) {
291316
return AwsMetricAttributesSpanExporterBuilder.create(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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.sdk.common.CompletableResultCode;
21+
import io.opentelemetry.sdk.trace.data.SpanData;
22+
import io.opentelemetry.sdk.trace.export.SpanExporter;
23+
import java.io.ByteArrayInputStream;
24+
import java.io.ByteArrayOutputStream;
25+
import java.net.URI;
26+
import java.util.ArrayList;
27+
import java.util.Collection;
28+
import java.util.HashMap;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.function.Supplier;
32+
import javax.annotation.concurrent.Immutable;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
36+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
37+
import software.amazon.awssdk.http.SdkHttpFullRequest;
38+
import software.amazon.awssdk.http.SdkHttpMethod;
39+
import software.amazon.awssdk.http.SdkHttpRequest;
40+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
41+
import software.amazon.awssdk.http.auth.spi.signer.SignedRequest;
42+
43+
/**
44+
* This exporter extends the functionality of the OtlpHttpSpanExporter to allow spans to be exported
45+
* to the XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the AWSSDK
46+
* library to sign and directly inject SigV4 Authentication to the exported request's headers. <a
47+
* href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html">...</a>
48+
*/
49+
@Immutable
50+
public class OtlpAwsSpanExporter implements SpanExporter {
51+
private static final String SERVICE_NAME = "xray";
52+
private static final Logger logger = LoggerFactory.getLogger(OtlpAwsSpanExporter.class);
53+
54+
private final OtlpHttpSpanExporter parentExporter;
55+
private final String awsRegion;
56+
private final String endpoint;
57+
private Collection<SpanData> spanData;
58+
59+
public OtlpAwsSpanExporter(String endpoint) {
60+
this.parentExporter =
61+
OtlpHttpSpanExporter.builder()
62+
.setEndpoint(endpoint)
63+
.setHeaders(new SigV4AuthHeaderSupplier())
64+
.build();
65+
66+
this.awsRegion = endpoint.split("\\.")[1];
67+
this.endpoint = endpoint;
68+
this.spanData = new ArrayList<>();
69+
}
70+
71+
public OtlpAwsSpanExporter(OtlpHttpSpanExporter parentExporter, String endpoint) {
72+
this.parentExporter =
73+
parentExporter.toBuilder()
74+
.setEndpoint(endpoint)
75+
.setHeaders(new SigV4AuthHeaderSupplier())
76+
.build();
77+
78+
this.awsRegion = endpoint.split("\\.")[1];
79+
this.endpoint = endpoint;
80+
this.spanData = new ArrayList<>();
81+
}
82+
83+
/**
84+
* Overrides the upstream implementation of export. All behaviors are the same except if the
85+
* endpoint is an XRay OTLP endpoint, we will sign the request with SigV4 in headers before
86+
* sending it to the endpoint. Otherwise, we will skip signing.
87+
*/
88+
@Override
89+
public CompletableResultCode export(Collection<SpanData> spans) {
90+
this.spanData = spans;
91+
return this.parentExporter.export(spans);
92+
}
93+
94+
@Override
95+
public CompletableResultCode flush() {
96+
return this.parentExporter.flush();
97+
}
98+
99+
@Override
100+
public CompletableResultCode shutdown() {
101+
return this.parentExporter.shutdown();
102+
}
103+
104+
@Override
105+
public String toString() {
106+
return this.parentExporter.toString();
107+
}
108+
109+
private final class SigV4AuthHeaderSupplier implements Supplier<Map<String, String>> {
110+
111+
@Override
112+
public Map<String, String> get() {
113+
try {
114+
ByteArrayOutputStream encodedSpans = new ByteArrayOutputStream();
115+
TraceRequestMarshaler.create(OtlpAwsSpanExporter.this.spanData).writeBinaryTo(encodedSpans);
116+
117+
SdkHttpRequest httpRequest =
118+
SdkHttpFullRequest.builder()
119+
.uri(URI.create(OtlpAwsSpanExporter.this.endpoint))
120+
.method(SdkHttpMethod.POST)
121+
.putHeader("Content-Type", "application/x-protobuf")
122+
.contentStreamProvider(() -> new ByteArrayInputStream(encodedSpans.toByteArray()))
123+
.build();
124+
125+
AwsCredentials credentials = DefaultCredentialsProvider.create().resolveCredentials();
126+
127+
SignedRequest signedRequest =
128+
AwsV4HttpSigner.create()
129+
.sign(
130+
b ->
131+
b.identity(credentials)
132+
.request(httpRequest)
133+
.putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, SERVICE_NAME)
134+
.putProperty(
135+
AwsV4HttpSigner.REGION_NAME, OtlpAwsSpanExporter.this.awsRegion)
136+
.payload(() -> new ByteArrayInputStream(encodedSpans.toByteArray())));
137+
138+
Map<String, String> result = new HashMap<>();
139+
140+
Map<String, List<String>> headers = signedRequest.request().headers();
141+
headers.forEach(
142+
(key, values) -> {
143+
if (!values.isEmpty()) {
144+
result.put(key, values.get(0));
145+
}
146+
});
147+
148+
return result;
149+
150+
} catch (Exception e) {
151+
logger.error(
152+
"Failed to sign/authenticate the given exported Span request to OTLP CloudWatch endpoint with error: {}",
153+
e.getMessage());
154+
155+
return new HashMap<>();
156+
}
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)