Skip to content

Commit a0e282a

Browse files
Jeel-mehtaJeel Mehta
andauthored
Contribute OTLP UDP Exporter (#1035)
*Issue #, if available:* *Description of changes:* Contributing our OTLP UDP exporter. Testing: Published the package to MavenLocal and tried running the sample app ![Screenshot 2025-03-04 at 1 11 32 PM](https://github.com/user-attachments/assets/585950fd-49ec-4f97-8101-17e00c9a21ce) Ran the unit tests as well ![Screenshot 2025-03-04 at 1 06 20 PM](https://github.com/user-attachments/assets/65ac480f-eec2-449e-a557-e953337e643f) By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Jeel Mehta <[email protected]>
1 parent b87ecbb commit a0e282a

File tree

6 files changed

+582
-0
lines changed

6 files changed

+582
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
plugins {
17+
id("java")
18+
id("java-library")
19+
id("maven-publish")
20+
}
21+
22+
group = "software.amazon.distro.opentelemetry.exporter.xray.lambda"
23+
version = "0.1.0"
24+
25+
dependencies {
26+
implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.10.0"))
27+
compileOnly("io.opentelemetry:opentelemetry-api")
28+
compileOnly("io.opentelemetry:opentelemetry-sdk")
29+
compileOnly("io.opentelemetry:opentelemetry-exporter-otlp-common")
30+
compileOnly("com.google.code.findbugs:jsr305:3.0.2")
31+
testImplementation("io.opentelemetry:opentelemetry-api")
32+
testImplementation("io.opentelemetry:opentelemetry-sdk")
33+
testImplementation("io.opentelemetry:opentelemetry-exporter-otlp-common")
34+
testImplementation(platform("org.junit:junit-bom:5.9.2"))
35+
testImplementation("org.junit.jupiter:junit-jupiter-api")
36+
testImplementation("org.junit.jupiter:junit-jupiter-engine")
37+
testImplementation("org.mockito:mockito-core:5.3.1")
38+
testImplementation("org.assertj:assertj-core:3.24.2")
39+
testImplementation("org.mockito:mockito-junit-jupiter:5.3.1")
40+
}
41+
42+
java {
43+
withSourcesJar()
44+
withJavadocJar()
45+
}
46+
47+
tasks.javadoc {
48+
options {
49+
(this as CoreJavadocOptions).addStringOption("Xdoclint:none", "-quiet")
50+
}
51+
isFailOnError = false
52+
}
53+
54+
sourceSets {
55+
main {
56+
java {
57+
srcDirs("src/main/java")
58+
}
59+
}
60+
test {
61+
java {
62+
srcDirs("src/test/java")
63+
}
64+
}
65+
}
66+
67+
tasks.test {
68+
useJUnitPlatform()
69+
testLogging {
70+
events("passed", "skipped", "failed")
71+
}
72+
}
73+
74+
tasks.jar {
75+
manifest {
76+
attributes(
77+
"Implementation-Title" to project.name,
78+
"Implementation-Version" to project.version,
79+
)
80+
}
81+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
82+
}
83+
84+
tasks.named<Jar>("javadocJar") {
85+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
86+
}
87+
88+
tasks.named<Jar>("sourcesJar") {
89+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
90+
}
91+
92+
publishing {
93+
publications {
94+
create<MavenPublication>("mavenJava") {
95+
from(components["java"])
96+
groupId = project.group.toString()
97+
artifactId = "aws-opentelemetry-xray-lambda-exporter"
98+
version = project.version.toString()
99+
}
100+
}
101+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
rootProject.name = "aws-opentelemetry-xray-lambda-exporter"
2+
3+
dependencyResolutionManagement {
4+
repositories {
5+
mavenCentral()
6+
mavenLocal()
7+
8+
maven {
9+
setUrl("https://oss.sonatype.org/content/repositories/snapshots")
10+
}
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.distro.opentelemetry.exporter.xray.lambda;
17+
18+
import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler;
19+
import io.opentelemetry.sdk.common.CompletableResultCode;
20+
import io.opentelemetry.sdk.trace.data.SpanData;
21+
import io.opentelemetry.sdk.trace.export.SpanExporter;
22+
import java.io.ByteArrayOutputStream;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.Base64;
25+
import java.util.Collection;
26+
import java.util.concurrent.atomic.AtomicBoolean;
27+
import java.util.logging.Level;
28+
import java.util.logging.Logger;
29+
import javax.annotation.concurrent.Immutable;
30+
31+
/**
32+
* Exports spans via UDP, using OpenTelemetry's protobuf model. The protobuf modelled spans are
33+
* Base64 encoded and prefixed with AWS X-Ray specific information before being sent over to {@link
34+
* UdpSender}.
35+
*
36+
* <p>This exporter is NOT meant for generic use since the payload is prefixed with AWS X-Ray
37+
* specific information.
38+
*/
39+
@Immutable
40+
public class AwsXrayLambdaExporter implements SpanExporter {
41+
42+
private static final Logger logger = Logger.getLogger(AwsXrayLambdaExporter.class.getName());
43+
44+
private final AtomicBoolean isShutdown = new AtomicBoolean();
45+
46+
private final UdpSender sender;
47+
private final String payloadPrefix;
48+
49+
AwsXrayLambdaExporter(UdpSender sender, String payloadPrefix) {
50+
this.sender = sender;
51+
this.payloadPrefix = payloadPrefix;
52+
}
53+
54+
@Override
55+
public CompletableResultCode export(Collection<SpanData> spans) {
56+
if (isShutdown.get()) {
57+
return CompletableResultCode.ofFailure();
58+
}
59+
60+
TraceRequestMarshaler exportRequest = TraceRequestMarshaler.create(spans);
61+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
62+
try {
63+
exportRequest.writeBinaryTo(baos);
64+
String payload = payloadPrefix + Base64.getEncoder().encodeToString(baos.toByteArray());
65+
sender.send(payload.getBytes(StandardCharsets.UTF_8));
66+
return CompletableResultCode.ofSuccess();
67+
} catch (Exception e) {
68+
logger.log(Level.SEVERE, "Failed to export spans. Error: " + e.getMessage(), e);
69+
return CompletableResultCode.ofFailure();
70+
}
71+
}
72+
73+
@Override
74+
public CompletableResultCode flush() {
75+
// TODO: implement
76+
return CompletableResultCode.ofSuccess();
77+
}
78+
79+
@Override
80+
public CompletableResultCode shutdown() {
81+
if (!isShutdown.compareAndSet(false, true)) {
82+
logger.log(Level.INFO, "Calling shutdown() multiple times.");
83+
return CompletableResultCode.ofSuccess();
84+
}
85+
return sender.shutdown();
86+
}
87+
88+
// Visible for testing
89+
UdpSender getSender() {
90+
return sender;
91+
}
92+
93+
// Visible for testing
94+
String getPayloadPrefix() {
95+
return payloadPrefix;
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.distro.opentelemetry.exporter.xray.lambda;
17+
18+
import static java.util.Objects.requireNonNull;
19+
20+
import java.util.Map;
21+
22+
public final class AwsXrayLambdaExporterBuilder {
23+
24+
private static final String DEFAULT_HOST = "127.0.0.1";
25+
private static final int DEFAULT_PORT = 2000;
26+
27+
// The protocol header and delimiter is required for sending data to X-Ray Daemon or when running
28+
// in Lambda.
29+
// https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-daemon
30+
private static final String PROTOCOL_HEADER = "{\"format\": \"json\", \"version\": 1}";
31+
private static final char PROTOCOL_DELIMITER = '\n';
32+
33+
// These prefixes help the backend identify if the spans payload is sampled or not.
34+
private static final String FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = "T1S";
35+
private static final String FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = "T1U";
36+
37+
private UdpSender sender;
38+
private String tracePayloadPrefix = FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX;
39+
private Map<String, String> environmentVariables = System.getenv();
40+
41+
private static final String AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME";
42+
private static final String AWS_XRAY_DAEMON_ADDRESS_CONFIG = "AWS_XRAY_DAEMON_ADDRESS";
43+
44+
public AwsXrayLambdaExporterBuilder setEndpoint(String endpoint) {
45+
requireNonNull(endpoint, "endpoint must not be null");
46+
try {
47+
this.sender = createSenderFromEndpoint(endpoint);
48+
} catch (Exception e) {
49+
throw new IllegalArgumentException("Invalid endpoint, must be a valid URL: " + endpoint, e);
50+
}
51+
return this;
52+
}
53+
54+
public AwsXrayLambdaExporterBuilder setPayloadSampleDecision(TracePayloadSampleDecision decision) {
55+
this.tracePayloadPrefix =
56+
decision == TracePayloadSampleDecision.SAMPLED
57+
? FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX
58+
: FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX;
59+
return this;
60+
}
61+
62+
private UdpSender createSenderFromEndpoint(String endpoint) {
63+
String[] parts = endpoint.split(":");
64+
String host = parts[0];
65+
int port = Integer.parseInt(parts[1]);
66+
return new UdpSender(host, port);
67+
}
68+
69+
// For testing purposes
70+
AwsXrayLambdaExporterBuilder withEnvironmentVariables(Map<String, String> env) {
71+
this.environmentVariables = env;
72+
return this;
73+
}
74+
75+
// NEW: Added getter for testing
76+
Map<String, String> getEnvironmentVariables() {
77+
return environmentVariables;
78+
}
79+
80+
public AwsXrayLambdaExporter build() {
81+
if (sender == null) {
82+
String endpoint = null;
83+
84+
// If in Lambda environment, try to get X-Ray daemon address
85+
if (isLambdaEnvironment()) {
86+
endpoint = environmentVariables.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG);
87+
if (endpoint != null && !endpoint.isEmpty()) {
88+
try {
89+
this.sender = createSenderFromEndpoint(endpoint);
90+
return new AwsXrayLambdaExporter(
91+
this.sender, PROTOCOL_HEADER + PROTOCOL_DELIMITER + tracePayloadPrefix);
92+
} catch (Exception e) {
93+
// Fallback to defaults if parsing fails
94+
this.sender = new UdpSender(DEFAULT_HOST, DEFAULT_PORT);
95+
}
96+
}
97+
}
98+
99+
// Use defaults if not in Lambda or if daemon address is invalid/unavailable
100+
this.sender = new UdpSender(DEFAULT_HOST, DEFAULT_PORT);
101+
}
102+
return new AwsXrayLambdaExporter(
103+
this.sender, PROTOCOL_HEADER + PROTOCOL_DELIMITER + tracePayloadPrefix);
104+
}
105+
106+
private boolean isLambdaEnvironment() {
107+
String functionName = environmentVariables.get(AWS_LAMBDA_FUNCTION_NAME_CONFIG);
108+
return functionName != null && !functionName.isEmpty();
109+
}
110+
111+
// Only for testing
112+
AwsXrayLambdaExporterBuilder setSender(UdpSender sender) {
113+
this.sender = sender;
114+
return this;
115+
}
116+
}
117+
118+
enum TracePayloadSampleDecision {
119+
SAMPLED,
120+
UNSAMPLED
121+
}

0 commit comments

Comments
 (0)