Skip to content

Commit 43bc6bc

Browse files
authored
feat: Support microservice span in Lambda Java environment. (#1053)
When a microservice is hosted in a Lambda Java environment (e.g., a Spring Boot app), this PR ensures the following to provide a better observation experience: The microservice will emit a server span that follows Lambda's server span. Lambda's server span will be tagged with aws.trace.lambda.multiple-server=true. If Lambda detects the HTTP call context, the operation attribute will be set as the HTTP method and path, instead of FunctionHandler. Test: All unit tests pass. Built the Java layer and Spring Boot app on Lambda, and completed end-to-end testing. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent aa6fa08 commit 43bc6bc

File tree

6 files changed

+341
-0
lines changed

6 files changed

+341
-0
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ private Sampler customizeSampler(Sampler sampler, ConfigProperties configProps)
195195

196196
private SdkTracerProviderBuilder customizeTracerProviderBuilder(
197197
SdkTracerProviderBuilder tracerProviderBuilder, ConfigProperties configProps) {
198+
if (isLambdaEnvironment()) {
199+
tracerProviderBuilder.addSpanProcessor(new AwsLambdaSpanProcessor());
200+
}
201+
198202
if (isApplicationSignalsEnabled(configProps)) {
199203
logger.info("AWS Application Signals enabled");
200204
Duration exportInterval =

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,7 @@ private AwsAttributeKeys() {}
9797
AttributeKey.stringKey("aws.bedrock.guardrail.id");
9898
static final AttributeKey<String> AWS_GUARDRAIL_ARN =
9999
AttributeKey.stringKey("aws.bedrock.guardrail.arn");
100+
101+
static final AttributeKey<Boolean> AWS_TRACE_LAMBDA_MULTIPLE_SERVER =
102+
AttributeKey.booleanKey("aws.trace.lambda.multiple-server");
100103
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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.api.trace.Span;
19+
import io.opentelemetry.context.Context;
20+
import io.opentelemetry.sdk.trace.ReadWriteSpan;
21+
import io.opentelemetry.sdk.trace.ReadableSpan;
22+
import io.opentelemetry.sdk.trace.SpanProcessor;
23+
import javax.annotation.concurrent.Immutable;
24+
25+
@Immutable
26+
public final class AwsLambdaSpanProcessor implements SpanProcessor {
27+
@Override
28+
public void onStart(Context parentContext, ReadWriteSpan span) {
29+
if (AwsSpanProcessingUtil.isServletServerSpan(span)) {
30+
Span parentSpan = Span.fromContextOrNull(parentContext);
31+
if (parentSpan == null || !(parentSpan instanceof ReadWriteSpan)) {
32+
return;
33+
}
34+
35+
ReadWriteSpan parentReadWriteSpan = (ReadWriteSpan) parentSpan;
36+
if (!AwsSpanProcessingUtil.isLambdaServerSpan(parentReadWriteSpan)) {
37+
return;
38+
}
39+
parentReadWriteSpan.setAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER, true);
40+
}
41+
}
42+
43+
@Override
44+
public boolean isStartRequired() {
45+
return true;
46+
}
47+
48+
@Override
49+
public void onEnd(ReadableSpan span) {}
50+
51+
@Override
52+
public boolean isEndRequired() {
53+
return false;
54+
}
55+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.opentelemetry.api.trace.SpanContext;
3838
import io.opentelemetry.api.trace.SpanKind;
3939
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
40+
import io.opentelemetry.sdk.trace.ReadableSpan;
4041
import io.opentelemetry.sdk.trace.data.SpanData;
4142
import java.io.IOException;
4243
import java.io.InputStream;
@@ -68,6 +69,10 @@ final class AwsSpanProcessingUtil {
6869

6970
private static final String SQL_DIALECT_KEYWORDS_JSON = "configuration/sql_dialect_keywords.json";
7071

72+
static final AttributeKey<String> OTEL_SCOPE_NAME = AttributeKey.stringKey("otel.scope.name");
73+
static final String LAMBDA_SCOPE_PREFIX = "io.opentelemetry.aws-lambda-";
74+
static final String SERVLET_SCOPE_PREFIX = "io.opentelemetry.servlet-";
75+
7176
static List<String> getDialectKeywords() {
7277
try (InputStream jsonFile =
7378
AwsSpanProcessingUtil.class
@@ -91,6 +96,10 @@ static List<String> getDialectKeywords() {
9196
*/
9297
static String getIngressOperation(SpanData span) {
9398
if (isLambdaEnvironment()) {
99+
String op = generateIngressOperation(span);
100+
if (!op.equals(UNKNOWN_OPERATION)) {
101+
return op;
102+
}
94103
return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG) + "/FunctionHandler";
95104
}
96105
String operation = span.getName();
@@ -248,4 +257,30 @@ static boolean isDBSpan(SpanData span) {
248257
|| isKeyPresent(span, DB_OPERATION)
249258
|| isKeyPresent(span, DB_STATEMENT);
250259
}
260+
261+
static boolean isLambdaServerSpan(ReadableSpan span) {
262+
String scopeName = null;
263+
if (span != null
264+
&& span.toSpanData() != null
265+
&& span.toSpanData().getInstrumentationScopeInfo() != null) {
266+
scopeName = span.toSpanData().getInstrumentationScopeInfo().getName();
267+
}
268+
269+
return scopeName != null
270+
&& scopeName.startsWith(LAMBDA_SCOPE_PREFIX)
271+
&& SpanKind.SERVER == span.getKind();
272+
}
273+
274+
static boolean isServletServerSpan(ReadableSpan span) {
275+
String scopeName = null;
276+
if (span != null
277+
&& span.toSpanData() != null
278+
&& span.toSpanData().getInstrumentationScopeInfo() != null) {
279+
scopeName = span.toSpanData().getInstrumentationScopeInfo().getName();
280+
}
281+
282+
return scopeName != null
283+
&& scopeName.startsWith(SERVLET_SCOPE_PREFIX)
284+
&& SpanKind.SERVER == span.getKind();
285+
}
251286
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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 static org.assertj.core.api.Assertions.assertThat;
19+
import static org.junit.jupiter.api.Assertions.assertNull;
20+
import static org.mockito.Mockito.*;
21+
22+
import io.opentelemetry.api.common.AttributeKey;
23+
import io.opentelemetry.api.trace.Span;
24+
import io.opentelemetry.api.trace.SpanContext;
25+
import io.opentelemetry.api.trace.SpanKind;
26+
import io.opentelemetry.api.trace.Tracer;
27+
import io.opentelemetry.context.Context;
28+
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
29+
import io.opentelemetry.sdk.trace.ReadWriteSpan;
30+
import io.opentelemetry.sdk.trace.ReadableSpan;
31+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
32+
import io.opentelemetry.sdk.trace.data.SpanData;
33+
import java.util.Map;
34+
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.Test;
36+
37+
class AwsLambdaSpanProcessorTest {
38+
39+
private AwsLambdaSpanProcessor processor;
40+
private ReadWriteSpan mockLambdaServerSpan;
41+
private SpanData mockLambdaSpanData;
42+
private InstrumentationScopeInfo mockLambdaScopeInfo;
43+
private Map<AttributeKey<?>, Object> attributeMapForLambdaSpan;
44+
private SpanContext mockSpanContext;
45+
46+
private ReadWriteSpan mockServletServerSpan;
47+
private SpanData mockServletSpanData;
48+
private InstrumentationScopeInfo mockServletScopeInfo;
49+
50+
private Tracer lambdaTracer;
51+
private Tracer servletTracer;
52+
private Tracer otherTracer;
53+
54+
@BeforeEach
55+
public void setup() {
56+
processor = new AwsLambdaSpanProcessor();
57+
lambdaTracer =
58+
SdkTracerProvider.builder()
59+
.addSpanProcessor(processor)
60+
.build()
61+
.get(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "core-1.0");
62+
63+
servletTracer =
64+
SdkTracerProvider.builder()
65+
.addSpanProcessor(processor)
66+
.build()
67+
.get(AwsSpanProcessingUtil.SERVLET_SCOPE_PREFIX + "lib-3.0");
68+
69+
otherTracer =
70+
SdkTracerProvider.builder().addSpanProcessor(processor).build().get("other-lib-2.0");
71+
}
72+
73+
@Test
74+
void testOnStart_servletServerSpan_withLambdaServerSpan() {
75+
Span parentSpan =
76+
lambdaTracer.spanBuilder("parent-lambda").setSpanKind(SpanKind.SERVER).startSpan();
77+
servletTracer
78+
.spanBuilder("child-servlet")
79+
.setSpanKind(SpanKind.SERVER)
80+
.setParent(Context.current().with(parentSpan))
81+
.startSpan();
82+
83+
ReadableSpan parentReadableSpan = (ReadableSpan) parentSpan;
84+
assertThat(parentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER))
85+
.isEqualTo(true);
86+
}
87+
88+
@Test
89+
void testOnStart_servletInternalSpan_withLambdaServerSpan() {
90+
Span parentSpan =
91+
lambdaTracer.spanBuilder("parent-lambda").setSpanKind(SpanKind.SERVER).startSpan();
92+
93+
servletTracer
94+
.spanBuilder("child-servlet")
95+
.setSpanKind(SpanKind.INTERNAL)
96+
.setParent(Context.current().with(parentSpan))
97+
.startSpan();
98+
99+
ReadableSpan parentReadableSpan = (ReadableSpan) parentSpan;
100+
assertNull(parentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER));
101+
}
102+
103+
@Test
104+
void testOnStart_servletServerSpan_withLambdaInternalSpan() {
105+
Span parentSpan =
106+
lambdaTracer.spanBuilder("parent-lambda").setSpanKind(SpanKind.INTERNAL).startSpan();
107+
108+
servletTracer
109+
.spanBuilder("child-servlet")
110+
.setSpanKind(SpanKind.SERVER)
111+
.setParent(Context.current().with(parentSpan))
112+
.startSpan();
113+
114+
ReadableSpan parentReadableSpan = (ReadableSpan) parentSpan;
115+
assertNull(parentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER));
116+
}
117+
118+
@Test
119+
void testOnStart_servletServerSpan_withLambdaServerSpanAsGrandParent() {
120+
Span grandParentSpan =
121+
lambdaTracer.spanBuilder("grandparent-lambda").setSpanKind(SpanKind.SERVER).startSpan();
122+
123+
Span parentSpan =
124+
otherTracer
125+
.spanBuilder("parent-other")
126+
.setSpanKind(SpanKind.SERVER)
127+
.setParent(Context.current().with(grandParentSpan))
128+
.startSpan();
129+
130+
servletTracer
131+
.spanBuilder("child-servlet")
132+
.setSpanKind(SpanKind.SERVER)
133+
.setParent(Context.current().with(parentSpan))
134+
.startSpan();
135+
136+
ReadableSpan grandParentReadableSpan = (ReadableSpan) grandParentSpan;
137+
assertNull(
138+
grandParentReadableSpan.getAttribute(AwsAttributeKeys.AWS_TRACE_LAMBDA_MULTIPLE_SERVER));
139+
}
140+
}

awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import static io.opentelemetry.semconv.SemanticAttributes.MessagingOperationValues.PROCESS;
2020
import static io.opentelemetry.semconv.SemanticAttributes.MessagingOperationValues.RECEIVE;
2121
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.junit.jupiter.api.Assertions.assertFalse;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
2224
import static org.mockito.Mockito.mock;
2325
import static org.mockito.Mockito.when;
2426
import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION;
@@ -29,6 +31,7 @@
2931
import io.opentelemetry.api.trace.SpanContext;
3032
import io.opentelemetry.api.trace.SpanKind;
3133
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
34+
import io.opentelemetry.sdk.trace.ReadableSpan;
3235
import io.opentelemetry.sdk.trace.data.SpanData;
3336
import java.util.List;
3437
import org.junit.jupiter.api.BeforeEach;
@@ -403,4 +406,105 @@ public void testSqlDialectKeywordsMaxLength() {
403406
assertThat(MAX_KEYWORD_LENGTH >= keyword.length());
404407
}
405408
}
409+
410+
@Test
411+
public void testIsLambdaServerSpan_withLambdaScope() {
412+
ReadableSpan span = mock(ReadableSpan.class);
413+
SpanData spanData = mock(SpanData.class);
414+
InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class);
415+
when(span.toSpanData()).thenReturn(spanData);
416+
when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo);
417+
when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-lib-1.0");
418+
when(span.getKind()).thenReturn(SpanKind.SERVER);
419+
420+
assertTrue(AwsSpanProcessingUtil.isLambdaServerSpan(span));
421+
}
422+
423+
@Test
424+
public void testIsLambdaServerSpan_withNonLambdaScope() {
425+
ReadableSpan span = mock(ReadableSpan.class);
426+
SpanData spanData = mock(SpanData.class);
427+
InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class);
428+
when(span.toSpanData()).thenReturn(spanData);
429+
when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo);
430+
when(scopeInfo.getName())
431+
.thenReturn("org.abc." + AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-lib-3.0");
432+
when(span.getKind()).thenReturn(SpanKind.SERVER);
433+
434+
assertFalse(AwsSpanProcessingUtil.isLambdaServerSpan(span));
435+
}
436+
437+
@Test
438+
public void testIsLambdaServerSpan_withNullScope() {
439+
ReadableSpan span = mock(ReadableSpan.class);
440+
SpanData spanData = mock(SpanData.class);
441+
when(span.toSpanData()).thenReturn(spanData);
442+
when(spanData.getInstrumentationScopeInfo()).thenReturn(null);
443+
when(span.getKind()).thenReturn(SpanKind.SERVER);
444+
445+
assertFalse(AwsSpanProcessingUtil.isLambdaServerSpan(span));
446+
}
447+
448+
@Test
449+
public void testIsLambdaServerSpan_withNonServerSpanKind() {
450+
ReadableSpan span = mock(ReadableSpan.class);
451+
SpanData spanData = mock(SpanData.class);
452+
InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class);
453+
when(span.toSpanData()).thenReturn(spanData);
454+
when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo);
455+
when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-core-1.0");
456+
when(span.getKind()).thenReturn(SpanKind.CLIENT);
457+
458+
assertFalse(AwsSpanProcessingUtil.isLambdaServerSpan(span));
459+
}
460+
461+
@Test
462+
public void testIsServletServerSpan_withServletScope() {
463+
ReadableSpan span = mock(ReadableSpan.class);
464+
SpanData spanData = mock(SpanData.class);
465+
InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class);
466+
when(span.toSpanData()).thenReturn(spanData);
467+
when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo);
468+
when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.SERVLET_SCOPE_PREFIX + "-3.0");
469+
when(span.getKind()).thenReturn(SpanKind.SERVER);
470+
471+
assertTrue(AwsSpanProcessingUtil.isServletServerSpan(span));
472+
}
473+
474+
@Test
475+
public void testIsServletServerSpan_withNonServletScope() {
476+
ReadableSpan span = mock(ReadableSpan.class);
477+
SpanData spanData = mock(SpanData.class);
478+
InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class);
479+
when(span.toSpanData()).thenReturn(spanData);
480+
when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo);
481+
when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.LAMBDA_SCOPE_PREFIX + "-2.0");
482+
when(span.getKind()).thenReturn(SpanKind.SERVER);
483+
484+
assertFalse(AwsSpanProcessingUtil.isServletServerSpan(span));
485+
}
486+
487+
@Test
488+
public void testIsServletServerSpan_withNullScope() {
489+
ReadableSpan span = mock(ReadableSpan.class);
490+
SpanData spanData = mock(SpanData.class);
491+
when(span.toSpanData()).thenReturn(spanData);
492+
when(spanData.getInstrumentationScopeInfo()).thenReturn(null);
493+
when(span.getKind()).thenReturn(SpanKind.SERVER);
494+
495+
assertFalse(AwsSpanProcessingUtil.isServletServerSpan(span));
496+
}
497+
498+
@Test
499+
public void testIsServletServerSpan_withNonServerSpanKind() {
500+
ReadableSpan span = mock(ReadableSpan.class);
501+
SpanData spanData = mock(SpanData.class);
502+
InstrumentationScopeInfo scopeInfo = mock(InstrumentationScopeInfo.class);
503+
when(span.toSpanData()).thenReturn(spanData);
504+
when(spanData.getInstrumentationScopeInfo()).thenReturn(scopeInfo);
505+
when(scopeInfo.getName()).thenReturn(AwsSpanProcessingUtil.SERVLET_SCOPE_PREFIX + "-5.0");
506+
when(span.getKind()).thenReturn(SpanKind.CLIENT);
507+
508+
assertFalse(AwsSpanProcessingUtil.isServletServerSpan(span));
509+
}
406510
}

0 commit comments

Comments
 (0)