Skip to content

Commit 0f1ddd1

Browse files
committed
added SigV4 support for HttpSpan Exporter
1 parent 779da66 commit 0f1ddd1

File tree

6 files changed

+157
-643
lines changed

6 files changed

+157
-643
lines changed

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

Lines changed: 19 additions & 5 deletions
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,7 +234,9 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder(
221234
return tracerProviderBuilder;
222235
}
223236

224-
// TODO: RETURN HERE IF OUR USE CASE IS HIT
237+
if (isXrayOtlpEndpoint(System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG))) {
238+
return tracerProviderBuilder;
239+
}
225240

226241
// Construct meterProvider
227242
MetricExporter metricsExporter =
@@ -289,10 +304,9 @@ private SpanExporter customizeSpanExporter(
289304
}
290305
}
291306
// When running OTLP endpoint for X-Ray backend, use custom exporter for SigV4 authentication
292-
// TODO: Figure out if `isOtlpSpanExporter(spanExporter)` is needed in the condition
293-
else if (OtlpSigV4HttpSpanExporterBuilder.CLOUDWATCH_OTLP_TRACES_ENDPOINT.equals(
294-
System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG))) {
295-
spanExporter = new OtlpSigV4HttpSpanExporterBuilder().build();
307+
else if (isXrayOtlpEndpoint(System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG))) {
308+
spanExporter =
309+
new OtlpAwsSpanExporter(System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG));
296310
}
297311

298312
if (isApplicationSignalsEnabled(configProps)) {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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.*;
24+
import java.net.URI;
25+
import java.util.*;
26+
import java.util.function.Supplier;
27+
import javax.annotation.concurrent.Immutable;
28+
import org.jetbrains.annotations.NotNull;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
31+
import software.amazon.awssdk.auth.credentials.AwsCredentials;
32+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
33+
import software.amazon.awssdk.http.SdkHttpFullRequest;
34+
import software.amazon.awssdk.http.SdkHttpMethod;
35+
import software.amazon.awssdk.http.SdkHttpRequest;
36+
import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner;
37+
import software.amazon.awssdk.http.auth.spi.signer.SignedRequest;
38+
39+
/**
40+
*
41+
*
42+
* <p>This exporter is NOT meant for generic use since the payload is prefixed with AWS X-Ray
43+
* specific information.
44+
*/
45+
@Immutable
46+
public class OtlpAwsSpanExporter implements SpanExporter {
47+
private static final Logger logger = LoggerFactory.getLogger(OtlpAwsSpanExporter.class);
48+
private static final AwsV4HttpSigner signer = AwsV4HttpSigner.create();
49+
50+
private final OtlpHttpSpanExporter parentExporter;
51+
private final String awsRegion;
52+
private final String endpoint;
53+
private Collection<SpanData> spanData;
54+
55+
public OtlpAwsSpanExporter(String endpoint) {
56+
this.parentExporter =
57+
OtlpHttpSpanExporter.builder()
58+
.setEndpoint(endpoint)
59+
.setHeaders(new SigV4AuthHeaderSupplier())
60+
.build();
61+
62+
this.awsRegion = endpoint.split("\\.")[1];
63+
this.endpoint = endpoint;
64+
this.spanData = new ArrayList<>();
65+
}
66+
67+
@Override
68+
public CompletableResultCode export(@NotNull Collection<SpanData> spans) {
69+
this.spanData = spans;
70+
return this.parentExporter.export(spans);
71+
}
72+
73+
@Override
74+
public CompletableResultCode flush() {
75+
return this.parentExporter.flush();
76+
}
77+
78+
@Override
79+
public CompletableResultCode shutdown() {
80+
return this.parentExporter.shutdown();
81+
}
82+
83+
@Override
84+
public String toString() {
85+
return this.parentExporter.toString();
86+
}
87+
88+
private final class SigV4AuthHeaderSupplier implements Supplier<Map<String, String>> {
89+
90+
@Override
91+
public Map<String, String> get() {
92+
try {
93+
ByteArrayOutputStream encodedSpans = new ByteArrayOutputStream();
94+
TraceRequestMarshaler.create(OtlpAwsSpanExporter.this.spanData)
95+
.writeBinaryTo(encodedSpans);
96+
97+
SdkHttpRequest httpRequest =
98+
SdkHttpFullRequest.builder()
99+
.uri(URI.create(OtlpAwsSpanExporter.this.endpoint))
100+
.method(SdkHttpMethod.POST)
101+
.putHeader("Content-Type", "application/x-protobuf")
102+
.contentStreamProvider(() -> new ByteArrayInputStream(encodedSpans.toByteArray()))
103+
.build();
104+
105+
AwsCredentials credentials = DefaultCredentialsProvider.create().resolveCredentials();
106+
107+
SignedRequest signedRequest =
108+
signer.sign(
109+
b ->
110+
b.identity(credentials)
111+
.request(httpRequest)
112+
.putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, "xray")
113+
.putProperty(
114+
AwsV4HttpSigner.REGION_NAME, OtlpAwsSpanExporter.this.awsRegion)
115+
.payload(() -> new ByteArrayInputStream(encodedSpans.toByteArray())));
116+
117+
Map<String, String> result = new HashMap<>();
118+
119+
Map<String, List<String>> headers = signedRequest.request().headers();
120+
headers.forEach(
121+
(key, values) -> {
122+
if (!values.isEmpty()) {
123+
result.put(key, values.get(0));
124+
}
125+
});
126+
127+
return result;
128+
129+
} catch (Exception e) {
130+
logger.error(
131+
"Failed to sign/authenticate the given exported Span request to OTLP CloudWatch endpoint with error: {}",
132+
e.getMessage());
133+
134+
return new HashMap<>();
135+
}
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)