Skip to content

Commit 6055d50

Browse files
Add azure-functions instrumentation (#8432)
* add azure-functions instrumentation * add unit tests for azure-functions instrumentation * add assertions for azure-functions instrumentation * add comment for minimum azure-functions-java-library version * add comment for azure-functions using v1 as default schema version * add comment for null hierarchyMarkerType in azure-functions instrumentation
1 parent 0f1105d commit 6055d50

File tree

11 files changed

+364
-6
lines changed

11 files changed

+364
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
muzzle {
2+
pass {
3+
group = 'com.microsoft.azure.functions'
4+
module = 'azure-functions-java-library'
5+
6+
// 1.2.2 is the first stable release. The earlier versions are either betas or are unstable
7+
versions = '[1.2.2,)'
8+
}
9+
}
10+
11+
apply from: "$rootDir/gradle/java.gradle"
12+
13+
addTestSuiteForDir('latestDepTest', 'test')
14+
15+
dependencies {
16+
compileOnly group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '1.2.2'
17+
18+
testImplementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '1.2.2'
19+
testImplementation libs.bundles.mockito
20+
21+
latestDepTestImplementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '+'
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package datadog.trace.instrumentation.azurefunctions;
2+
3+
import com.microsoft.azure.functions.HttpRequestMessage;
4+
import com.microsoft.azure.functions.HttpResponseMessage;
5+
import datadog.trace.api.naming.SpanNaming;
6+
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
7+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
8+
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
9+
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
10+
import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter;
11+
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
12+
import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator;
13+
14+
public class AzureFunctionsDecorator
15+
extends HttpServerDecorator<
16+
HttpRequestMessage, HttpRequestMessage, HttpResponseMessage, HttpRequestMessage> {
17+
public static final CharSequence AZURE_FUNCTIONS = UTF8BytesString.create("azure-functions");
18+
19+
public static final AzureFunctionsDecorator DECORATE = new AzureFunctionsDecorator();
20+
public static final CharSequence AZURE_FUNCTIONS_REQUEST =
21+
UTF8BytesString.create(
22+
SpanNaming.instance().namingSchema().cloud().operationForFaas("azure"));
23+
24+
@Override
25+
protected String[] instrumentationNames() {
26+
return new String[] {"azure-functions"};
27+
}
28+
29+
@Override
30+
protected CharSequence component() {
31+
return AZURE_FUNCTIONS;
32+
}
33+
34+
@Override
35+
protected AgentPropagation.ContextVisitor<HttpRequestMessage> getter() {
36+
return HttpRequestMessageExtractAdapter.GETTER;
37+
}
38+
39+
@Override
40+
protected AgentPropagation.ContextVisitor<HttpResponseMessage> responseGetter() {
41+
return null;
42+
}
43+
44+
@Override
45+
public CharSequence spanName() {
46+
return AZURE_FUNCTIONS_REQUEST;
47+
}
48+
49+
@Override
50+
protected String method(final HttpRequestMessage request) {
51+
return request.getHttpMethod().name();
52+
}
53+
54+
@Override
55+
protected URIDataAdapter url(final HttpRequestMessage request) {
56+
return new URIDefaultDataAdapter(request.getUri());
57+
}
58+
59+
@Override
60+
protected String peerHostIP(final HttpRequestMessage request) {
61+
return null;
62+
}
63+
64+
@Override
65+
protected int peerPort(final HttpRequestMessage request) {
66+
return 0;
67+
}
68+
69+
@Override
70+
protected CharSequence spanType() {
71+
return InternalSpanTypes.SERVERLESS;
72+
}
73+
74+
@Override
75+
protected int status(final HttpResponseMessage response) {
76+
return response.getStatusCode();
77+
}
78+
79+
public AgentSpan afterStart(final AgentSpan span, final String functionName) {
80+
span.setTag("aas.function.name", functionName);
81+
span.setTag("aas.function.trigger", "Http");
82+
return super.afterStart(span);
83+
}
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package datadog.trace.instrumentation.azurefunctions;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.declaresMethod;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.isAnnotatedWith;
5+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
6+
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
7+
import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR;
8+
import static datadog.trace.instrumentation.azurefunctions.AzureFunctionsDecorator.DECORATE;
9+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
10+
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
11+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
12+
13+
import com.google.auto.service.AutoService;
14+
import com.microsoft.azure.functions.ExecutionContext;
15+
import com.microsoft.azure.functions.HttpRequestMessage;
16+
import com.microsoft.azure.functions.HttpResponseMessage;
17+
import datadog.trace.agent.tooling.Instrumenter;
18+
import datadog.trace.agent.tooling.InstrumenterModule;
19+
import datadog.trace.bootstrap.instrumentation.api.AgentScope;
20+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
21+
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
22+
import net.bytebuddy.asm.Advice;
23+
import net.bytebuddy.description.type.TypeDescription;
24+
import net.bytebuddy.matcher.ElementMatcher;
25+
26+
@AutoService(InstrumenterModule.class)
27+
public class AzureFunctionsInstrumentation extends InstrumenterModule.Tracing
28+
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
29+
public AzureFunctionsInstrumentation() {
30+
super("azure-functions");
31+
}
32+
33+
@Override
34+
public String hierarchyMarkerType() {
35+
/*
36+
Due to the class-loading in the Azure Function environment we cannot assume that
37+
"com.microsoft.azure.functions.annotation.FunctionName" will be visible (as in defined as a
38+
resource) for any types using that annotation
39+
*/
40+
return null;
41+
}
42+
43+
@Override
44+
public ElementMatcher<TypeDescription> hierarchyMatcher() {
45+
return declaresMethod(
46+
isAnnotatedWith(named("com.microsoft.azure.functions.annotation.FunctionName")));
47+
}
48+
49+
@Override
50+
public String[] helperClassNames() {
51+
return new String[] {
52+
packageName + ".AzureFunctionsDecorator", packageName + ".HttpRequestMessageExtractAdapter"
53+
};
54+
}
55+
56+
public void methodAdvice(MethodTransformer transformer) {
57+
transformer.applyAdvice(
58+
isMethod()
59+
.and(isPublic())
60+
.and(takesArgument(0, named("com.microsoft.azure.functions.HttpRequestMessage")))
61+
.and(takesArgument(1, named("com.microsoft.azure.functions.ExecutionContext"))),
62+
AzureFunctionsInstrumentation.class.getName() + "$AzureFunctionsAdvice");
63+
}
64+
65+
public static class AzureFunctionsAdvice {
66+
@Advice.OnMethodEnter(suppress = Throwable.class)
67+
public static AgentScope methodEnter(
68+
@Advice.Argument(0) final HttpRequestMessage request,
69+
@Advice.Argument(1) final ExecutionContext context) {
70+
final AgentSpanContext.Extracted extractedContext = DECORATE.extract(request);
71+
final AgentSpan span = DECORATE.startSpan(request, extractedContext);
72+
DECORATE.afterStart(span, context.getFunctionName());
73+
DECORATE.onRequest(span, request, request, extractedContext);
74+
HTTP_RESOURCE_DECORATOR.withRoute(
75+
span, request.getHttpMethod().name(), request.getUri().getPath());
76+
return activateSpan(span);
77+
}
78+
79+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
80+
public static void methodExit(
81+
@Advice.Enter final AgentScope scope,
82+
@Advice.Return final HttpResponseMessage response,
83+
@Advice.Thrown final Throwable throwable) {
84+
final AgentSpan span = scope.span();
85+
DECORATE.onError(span, throwable);
86+
DECORATE.onResponse(span, response);
87+
DECORATE.beforeFinish(span);
88+
scope.close();
89+
span.finish();
90+
}
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package datadog.trace.instrumentation.azurefunctions;
2+
3+
import com.microsoft.azure.functions.HttpRequestMessage;
4+
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
5+
import datadog.trace.bootstrap.instrumentation.api.ContextVisitors;
6+
7+
public class HttpRequestMessageExtractAdapter
8+
implements AgentPropagation.ContextVisitor<HttpRequestMessage> {
9+
public static final HttpRequestMessageExtractAdapter GETTER =
10+
new HttpRequestMessageExtractAdapter();
11+
12+
@Override
13+
public void forEachKey(HttpRequestMessage carrier, AgentPropagation.KeyClassifier classifier) {
14+
ContextVisitors.stringValuesEntrySet().forEachKey(carrier.getHeaders().entrySet(), classifier);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import static org.mockito.Mockito.mock
2+
import static org.mockito.Mockito.when
3+
4+
import com.microsoft.azure.functions.ExecutionContext
5+
import com.microsoft.azure.functions.HttpMethod
6+
import com.microsoft.azure.functions.HttpRequestMessage
7+
import com.microsoft.azure.functions.HttpResponseMessage
8+
import com.microsoft.azure.functions.HttpStatus
9+
import datadog.trace.agent.test.naming.VersionedNamingTestBase
10+
import datadog.trace.api.DDSpanTypes
11+
import datadog.trace.bootstrap.instrumentation.api.Tags
12+
import okhttp3.internal.Version
13+
14+
abstract class AzureFunctionsTest extends VersionedNamingTestBase {
15+
16+
@Override
17+
String service() {
18+
null
19+
}
20+
21+
def "test azure functions http trigger"() {
22+
given:
23+
HttpRequestMessage<Optional<String>> request = mock(HttpRequestMessage)
24+
HttpResponseMessage.Builder responseBuilder = mock(HttpResponseMessage.Builder)
25+
HttpResponseMessage response = mock(HttpResponseMessage)
26+
ExecutionContext context = mock(ExecutionContext)
27+
28+
String functionName = "HttpTest"
29+
Map<String, String> headers = ["user-agent": Version.userAgent()]
30+
HttpMethod method = HttpMethod.GET
31+
String responseBody = "Hello Datadog test!"
32+
HttpStatus status = HttpStatus.OK
33+
int statusCode = 200
34+
URI uri = new URI("https://localhost:7071/api/HttpTest")
35+
36+
and:
37+
when(request.getHeaders()).thenReturn(headers)
38+
when(request.getHttpMethod()).thenReturn(method)
39+
when(request.getUri()).thenReturn(uri)
40+
when(request.createResponseBuilder(status)).thenReturn(responseBuilder)
41+
42+
when(responseBuilder.body(responseBody)).thenReturn(responseBuilder)
43+
when(responseBuilder.build()).thenReturn(response)
44+
45+
when(response.getStatusCode()).thenReturn(statusCode)
46+
when(response.getBody()).thenReturn(responseBody)
47+
48+
when(context.getFunctionName()).thenReturn(functionName)
49+
50+
when:
51+
new Function().run(request, context)
52+
53+
then:
54+
assertTraces(1) {
55+
trace(1) {
56+
span {
57+
parent()
58+
operationName operation()
59+
spanType DDSpanTypes.SERVERLESS
60+
errored false
61+
tags {
62+
defaultTags()
63+
"$Tags.COMPONENT" "azure-functions"
64+
"$Tags.SPAN_KIND" "$Tags.SPAN_KIND_SERVER"
65+
"$Tags.HTTP_HOSTNAME" "localhost"
66+
"$Tags.HTTP_METHOD" "GET"
67+
"$Tags.HTTP_ROUTE" "/api/HttpTest"
68+
"$Tags.HTTP_STATUS" 200
69+
"$Tags.HTTP_URL" "https://localhost:7071/api/HttpTest"
70+
"$Tags.HTTP_USER_AGENT" "${Version.userAgent()}"
71+
"aas.function.name" "HttpTest"
72+
"aas.function.trigger" "Http"
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
79+
80+
81+
class AzureFunctionsV0ForkedTest extends AzureFunctionsTest {
82+
@Override
83+
int version() {
84+
0
85+
}
86+
87+
@Override
88+
String operation() {
89+
"dd-tracer-serverless-span"
90+
}
91+
}
92+
93+
class AzureFunctionsV1Test extends AzureFunctionsTest {
94+
@Override
95+
int version() {
96+
1
97+
}
98+
99+
@Override
100+
String operation() {
101+
"azure.functions.invoke"
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import com.microsoft.azure.functions.ExecutionContext;
2+
import com.microsoft.azure.functions.HttpMethod;
3+
import com.microsoft.azure.functions.HttpRequestMessage;
4+
import com.microsoft.azure.functions.HttpResponseMessage;
5+
import com.microsoft.azure.functions.HttpStatus;
6+
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
7+
import com.microsoft.azure.functions.annotation.FunctionName;
8+
import com.microsoft.azure.functions.annotation.HttpTrigger;
9+
import java.util.Optional;
10+
11+
public class Function {
12+
@FunctionName("HttpTest")
13+
public HttpResponseMessage run(
14+
@HttpTrigger(
15+
name = "req",
16+
methods = {HttpMethod.GET},
17+
authLevel = AuthorizationLevel.ANONYMOUS)
18+
HttpRequestMessage<Optional<String>> request,
19+
final ExecutionContext context) {
20+
return request.createResponseBuilder(HttpStatus.OK).body("Hello Datadog test!").build();
21+
}
22+
}

dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java

+2
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ public class DDSpanTypes {
3737

3838
public static final String MULE = "mule";
3939
public static final String VALKEY = "valkey";
40+
41+
public static final String SERVERLESS = "serverless";
4042
}

internal-api/src/main/java/datadog/trace/api/Config.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ public static String getHostName() {
514514
private final int telemetryDependencyResolutionQueueSize;
515515

516516
private final boolean azureAppServices;
517+
private final boolean azureFunctions;
517518
private final String traceAgentPath;
518519
private final List<String> traceAgentArgs;
519520
private final String dogStatsDPath;
@@ -798,6 +799,9 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins
798799

799800
baggageMapping = configProvider.getMergedMapWithOptionalMappings(null, true, BAGGAGE_MAPPING);
800801

802+
azureFunctions =
803+
getEnv("FUNCTIONS_WORKER_RUNTIME") != null && getEnv("FUNCTIONS_EXTENSION_VERSION") != null;
804+
801805
spanAttributeSchemaVersion = schemaVersionFromConfig();
802806

803807
// following two only used in v0.
@@ -3777,8 +3781,14 @@ private Map<String, String> getAzureAppServicesTags() {
37773781
}
37783782

37793783
private int schemaVersionFromConfig() {
3780-
String versionStr =
3781-
configProvider.getString(TRACE_SPAN_ATTRIBUTE_SCHEMA, "v" + SpanNaming.SCHEMA_MIN_VERSION);
3784+
String defaultVersion;
3785+
// use v1 so Azure Functions operation name is consistent with that of other tracers
3786+
if (azureFunctions) {
3787+
defaultVersion = "v1";
3788+
} else {
3789+
defaultVersion = "v" + SpanNaming.SCHEMA_MIN_VERSION;
3790+
}
3791+
String versionStr = configProvider.getString(TRACE_SPAN_ATTRIBUTE_SCHEMA, defaultVersion);
37823792
Matcher matcher = Pattern.compile("^v?(0|[1-9]\\d*)$").matcher(versionStr);
37833793
int parsedVersion = -1;
37843794
if (matcher.matches()) {

0 commit comments

Comments
 (0)