-
Notifications
You must be signed in to change notification settings - Fork 303
Add azure-functions instrumentation #8432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
75b9949
9b4e61d
d628cb6
95d4c18
5e596f9
85648d1
a40817f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
muzzle { | ||
pass { | ||
group = 'com.microsoft.azure.functions' | ||
module = 'azure-functions-java-library' | ||
|
||
// 1.2.2 is the first stable release. The earlier versions are either betas or are unstable | ||
versions = '[1.2.2,)' | ||
} | ||
} | ||
|
||
apply from: "$rootDir/gradle/java.gradle" | ||
|
||
addTestSuiteForDir('latestDepTest', 'test') | ||
|
||
dependencies { | ||
compileOnly group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '1.2.2' | ||
|
||
testImplementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '1.2.2' | ||
testImplementation libs.bundles.mockito | ||
|
||
latestDepTestImplementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '+' | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package datadog.trace.instrumentation.azurefunctions; | ||
|
||
import com.microsoft.azure.functions.HttpRequestMessage; | ||
import com.microsoft.azure.functions.HttpResponseMessage; | ||
import datadog.trace.api.naming.SpanNaming; | ||
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; | ||
import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; | ||
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; | ||
import datadog.trace.bootstrap.instrumentation.api.URIDefaultDataAdapter; | ||
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; | ||
import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator; | ||
|
||
public class AzureFunctionsDecorator | ||
extends HttpServerDecorator< | ||
HttpRequestMessage, HttpRequestMessage, HttpResponseMessage, HttpRequestMessage> { | ||
public static final CharSequence AZURE_FUNCTIONS = UTF8BytesString.create("azure-functions"); | ||
|
||
public static final AzureFunctionsDecorator DECORATE = new AzureFunctionsDecorator(); | ||
public static final CharSequence AZURE_FUNCTIONS_REQUEST = | ||
UTF8BytesString.create( | ||
SpanNaming.instance().namingSchema().cloud().operationForFaas("azure")); | ||
|
||
@Override | ||
protected String[] instrumentationNames() { | ||
return new String[] {"azure-functions"}; | ||
} | ||
|
||
@Override | ||
protected CharSequence component() { | ||
return AZURE_FUNCTIONS; | ||
} | ||
|
||
@Override | ||
protected AgentPropagation.ContextVisitor<HttpRequestMessage> getter() { | ||
return HttpRequestMessageExtractAdapter.GETTER; | ||
} | ||
|
||
@Override | ||
protected AgentPropagation.ContextVisitor<HttpResponseMessage> responseGetter() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public CharSequence spanName() { | ||
return AZURE_FUNCTIONS_REQUEST; | ||
} | ||
|
||
@Override | ||
protected String method(final HttpRequestMessage request) { | ||
return request.getHttpMethod().name(); | ||
} | ||
|
||
@Override | ||
protected URIDataAdapter url(final HttpRequestMessage request) { | ||
return new URIDefaultDataAdapter(request.getUri()); | ||
} | ||
|
||
@Override | ||
protected String peerHostIP(final HttpRequestMessage request) { | ||
return null; | ||
} | ||
|
||
@Override | ||
protected int peerPort(final HttpRequestMessage request) { | ||
return 0; | ||
} | ||
|
||
@Override | ||
protected CharSequence spanType() { | ||
return InternalSpanTypes.SERVERLESS; | ||
} | ||
|
||
@Override | ||
protected int status(final HttpResponseMessage response) { | ||
return response.getStatusCode(); | ||
} | ||
|
||
public AgentSpan afterStart(final AgentSpan span, final String functionName) { | ||
span.setTag("aas.function.name", functionName); | ||
span.setTag("aas.function.trigger", "Http"); | ||
return super.afterStart(span); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package datadog.trace.instrumentation.azurefunctions; | ||
|
||
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.declaresMethod; | ||
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.isAnnotatedWith; | ||
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; | ||
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; | ||
import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; | ||
import static datadog.trace.instrumentation.azurefunctions.AzureFunctionsDecorator.DECORATE; | ||
import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
import static net.bytebuddy.matcher.ElementMatchers.isPublic; | ||
import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
|
||
import com.google.auto.service.AutoService; | ||
import com.microsoft.azure.functions.ExecutionContext; | ||
import com.microsoft.azure.functions.HttpRequestMessage; | ||
import com.microsoft.azure.functions.HttpResponseMessage; | ||
import datadog.trace.agent.tooling.Instrumenter; | ||
import datadog.trace.agent.tooling.InstrumenterModule; | ||
import datadog.trace.bootstrap.instrumentation.api.AgentScope; | ||
import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; | ||
import net.bytebuddy.asm.Advice; | ||
import net.bytebuddy.description.type.TypeDescription; | ||
import net.bytebuddy.matcher.ElementMatcher; | ||
|
||
@AutoService(InstrumenterModule.class) | ||
public class AzureFunctionsInstrumentation extends InstrumenterModule.Tracing | ||
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { | ||
public AzureFunctionsInstrumentation() { | ||
super("azure-functions"); | ||
} | ||
|
||
@Override | ||
public String hierarchyMarkerType() { | ||
/* | ||
Due to the class-loading in the Azure Function environment we cannot assume that | ||
"com.microsoft.azure.functions.annotation.FunctionName" will be visible (as in defined as a | ||
resource) for any types using that annotation | ||
*/ | ||
return null; | ||
} | ||
|
||
@Override | ||
public ElementMatcher<TypeDescription> hierarchyMatcher() { | ||
return declaresMethod( | ||
isAnnotatedWith(named("com.microsoft.azure.functions.annotation.FunctionName"))); | ||
} | ||
|
||
@Override | ||
public String[] helperClassNames() { | ||
return new String[] { | ||
packageName + ".AzureFunctionsDecorator", packageName + ".HttpRequestMessageExtractAdapter" | ||
}; | ||
} | ||
|
||
public void methodAdvice(MethodTransformer transformer) { | ||
transformer.applyAdvice( | ||
isMethod() | ||
.and(isPublic()) | ||
.and(takesArgument(0, named("com.microsoft.azure.functions.HttpRequestMessage"))) | ||
.and(takesArgument(1, named("com.microsoft.azure.functions.ExecutionContext"))), | ||
AzureFunctionsInstrumentation.class.getName() + "$AzureFunctionsAdvice"); | ||
} | ||
|
||
public static class AzureFunctionsAdvice { | ||
@Advice.OnMethodEnter(suppress = Throwable.class) | ||
public static AgentScope methodEnter( | ||
@Advice.Argument(0) final HttpRequestMessage request, | ||
@Advice.Argument(1) final ExecutionContext context) { | ||
final AgentSpanContext.Extracted extractedContext = DECORATE.extract(request); | ||
final AgentSpan span = DECORATE.startSpan(request, extractedContext); | ||
DECORATE.afterStart(span, context.getFunctionName()); | ||
DECORATE.onRequest(span, request, request, extractedContext); | ||
HTTP_RESOURCE_DECORATOR.withRoute( | ||
span, request.getHttpMethod().name(), request.getUri().getPath()); | ||
return activateSpan(span); | ||
} | ||
|
||
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) | ||
public static void methodExit( | ||
@Advice.Enter final AgentScope scope, | ||
@Advice.Return final HttpResponseMessage response, | ||
@Advice.Thrown final Throwable throwable) { | ||
final AgentSpan span = scope.span(); | ||
DECORATE.onError(span, throwable); | ||
DECORATE.onResponse(span, response); | ||
DECORATE.beforeFinish(span); | ||
scope.close(); | ||
span.finish(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package datadog.trace.instrumentation.azurefunctions; | ||
|
||
import com.microsoft.azure.functions.HttpRequestMessage; | ||
import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; | ||
import datadog.trace.bootstrap.instrumentation.api.ContextVisitors; | ||
|
||
public class HttpRequestMessageExtractAdapter | ||
implements AgentPropagation.ContextVisitor<HttpRequestMessage> { | ||
public static final HttpRequestMessageExtractAdapter GETTER = | ||
new HttpRequestMessageExtractAdapter(); | ||
|
||
@Override | ||
public void forEachKey(HttpRequestMessage carrier, AgentPropagation.KeyClassifier classifier) { | ||
ContextVisitors.stringValuesEntrySet().forEachKey(carrier.getHeaders().entrySet(), classifier); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import static org.mockito.Mockito.mock | ||
import static org.mockito.Mockito.when | ||
|
||
import com.microsoft.azure.functions.ExecutionContext | ||
import com.microsoft.azure.functions.HttpMethod | ||
import com.microsoft.azure.functions.HttpRequestMessage | ||
import com.microsoft.azure.functions.HttpResponseMessage | ||
import com.microsoft.azure.functions.HttpStatus | ||
import datadog.trace.agent.test.naming.VersionedNamingTestBase | ||
import datadog.trace.api.DDSpanTypes | ||
import datadog.trace.bootstrap.instrumentation.api.Tags | ||
import okhttp3.internal.Version | ||
|
||
abstract class AzureFunctionsTest extends VersionedNamingTestBase { | ||
|
||
@Override | ||
String service() { | ||
null | ||
} | ||
|
||
def "test azure functions http trigger"() { | ||
given: | ||
HttpRequestMessage<Optional<String>> request = mock(HttpRequestMessage) | ||
HttpResponseMessage.Builder responseBuilder = mock(HttpResponseMessage.Builder) | ||
HttpResponseMessage response = mock(HttpResponseMessage) | ||
ExecutionContext context = mock(ExecutionContext) | ||
|
||
String functionName = "HttpTest" | ||
Map<String, String> headers = ["user-agent": Version.userAgent()] | ||
HttpMethod method = HttpMethod.GET | ||
String responseBody = "Hello Datadog test!" | ||
HttpStatus status = HttpStatus.OK | ||
int statusCode = 200 | ||
URI uri = new URI("https://localhost:7071/api/HttpTest") | ||
|
||
and: | ||
when(request.getHeaders()).thenReturn(headers) | ||
when(request.getHttpMethod()).thenReturn(method) | ||
when(request.getUri()).thenReturn(uri) | ||
when(request.createResponseBuilder(status)).thenReturn(responseBuilder) | ||
|
||
when(responseBuilder.body(responseBody)).thenReturn(responseBuilder) | ||
when(responseBuilder.build()).thenReturn(response) | ||
|
||
when(response.getStatusCode()).thenReturn(statusCode) | ||
when(response.getBody()).thenReturn(responseBody) | ||
|
||
when(context.getFunctionName()).thenReturn(functionName) | ||
|
||
when: | ||
new Function().run(request, context) | ||
|
||
then: | ||
assertTraces(1) { | ||
trace(1) { | ||
span { | ||
parent() | ||
operationName operation() | ||
spanType DDSpanTypes.SERVERLESS | ||
errored false | ||
tags { | ||
defaultTags() | ||
"$Tags.COMPONENT" "azure-functions" | ||
"$Tags.SPAN_KIND" "$Tags.SPAN_KIND_SERVER" | ||
"$Tags.HTTP_HOSTNAME" "localhost" | ||
"$Tags.HTTP_METHOD" "GET" | ||
"$Tags.HTTP_ROUTE" "/api/HttpTest" | ||
"$Tags.HTTP_STATUS" 200 | ||
"$Tags.HTTP_URL" "https://localhost:7071/api/HttpTest" | ||
"$Tags.HTTP_USER_AGENT" "${Version.userAgent()}" | ||
"aas.function.name" "HttpTest" | ||
"aas.function.trigger" "Http" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
class AzureFunctionsV0ForkedTest extends AzureFunctionsTest { | ||
@Override | ||
int version() { | ||
0 | ||
} | ||
|
||
@Override | ||
String operation() { | ||
"dd-tracer-serverless-span" | ||
} | ||
} | ||
|
||
class AzureFunctionsV1Test extends AzureFunctionsTest { | ||
@Override | ||
int version() { | ||
1 | ||
} | ||
|
||
@Override | ||
String operation() { | ||
"azure.functions.invoke" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import com.microsoft.azure.functions.ExecutionContext; | ||
import com.microsoft.azure.functions.HttpMethod; | ||
import com.microsoft.azure.functions.HttpRequestMessage; | ||
import com.microsoft.azure.functions.HttpResponseMessage; | ||
import com.microsoft.azure.functions.HttpStatus; | ||
import com.microsoft.azure.functions.annotation.AuthorizationLevel; | ||
import com.microsoft.azure.functions.annotation.FunctionName; | ||
import com.microsoft.azure.functions.annotation.HttpTrigger; | ||
import java.util.Optional; | ||
|
||
public class Function { | ||
@FunctionName("HttpTest") | ||
public HttpResponseMessage run( | ||
@HttpTrigger( | ||
name = "req", | ||
methods = {HttpMethod.GET}, | ||
authLevel = AuthorizationLevel.ANONYMOUS) | ||
HttpRequestMessage<Optional<String>> request, | ||
final ExecutionContext context) { | ||
return request.createResponseBuilder(HttpStatus.OK).body("Hello Datadog test!").build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -511,6 +511,7 @@ public static String getHostName() { | |
private final int telemetryDependencyResolutionQueueSize; | ||
|
||
private final boolean azureAppServices; | ||
private final boolean azureFunctions; | ||
private final String traceAgentPath; | ||
private final List<String> traceAgentArgs; | ||
private final String dogStatsDPath; | ||
|
@@ -795,6 +796,9 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins | |
|
||
baggageMapping = configProvider.getMergedMapWithOptionalMappings(null, true, BAGGAGE_MAPPING); | ||
|
||
azureFunctions = | ||
getEnv("FUNCTIONS_WORKER_RUNTIME") != null && getEnv("FUNCTIONS_EXTENSION_VERSION") != null; | ||
|
||
spanAttributeSchemaVersion = schemaVersionFromConfig(); | ||
|
||
// following two only used in v0. | ||
|
@@ -3741,8 +3745,14 @@ private Map<String, String> getAzureAppServicesTags() { | |
} | ||
|
||
private int schemaVersionFromConfig() { | ||
String versionStr = | ||
configProvider.getString(TRACE_SPAN_ATTRIBUTE_SCHEMA, "v" + SpanNaming.SCHEMA_MIN_VERSION); | ||
String defaultVersion; | ||
// use v1 so Azure Functions operation name is consistent with that of other tracers | ||
if (azureFunctions) { | ||
defaultVersion = "v1"; | ||
Comment on lines
+3750
to
+3751
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah ? Why ? Can you add a comment to justify this ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be consistent with the instrumentation in other runtimes (node, python) the operation name should be |
||
} else { | ||
defaultVersion = "v" + SpanNaming.SCHEMA_MIN_VERSION; | ||
} | ||
String versionStr = configProvider.getString(TRACE_SPAN_ATTRIBUTE_SCHEMA, defaultVersion); | ||
Matcher matcher = Pattern.compile("^v?(0|[1-9]\\d*)$").matcher(versionStr); | ||
int parsedVersion = -1; | ||
if (matcher.matches()) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're targeting this one and not earlier versions because it's the oldest one targeting Java 8 ?
A round major version as lower bound is pretty standard, but a specific version like this deserves a comment I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1.2.2
is the earliest stable version I was able to use. Using1.2.0
results in an error (see below). Prior to1.2.0
all versions are beta versions. I added a comment to reflect this in code.