diff --git a/dd-java-agent/instrumentation/azure-functions/build.gradle b/dd-java-agent/instrumentation/azure-functions/build.gradle new file mode 100644 index 00000000000..a7ad15700cc --- /dev/null +++ b/dd-java-agent/instrumentation/azure-functions/build.gradle @@ -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: '+' +} diff --git a/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/AzureFunctionsDecorator.java b/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/AzureFunctionsDecorator.java new file mode 100644 index 00000000000..a7654462b91 --- /dev/null +++ b/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/AzureFunctionsDecorator.java @@ -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 getter() { + return HttpRequestMessageExtractAdapter.GETTER; + } + + @Override + protected AgentPropagation.ContextVisitor 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); + } +} diff --git a/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/AzureFunctionsInstrumentation.java b/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/AzureFunctionsInstrumentation.java new file mode 100644 index 00000000000..604b5d77c42 --- /dev/null +++ b/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/AzureFunctionsInstrumentation.java @@ -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 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(); + } + } +} diff --git a/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/HttpRequestMessageExtractAdapter.java b/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/HttpRequestMessageExtractAdapter.java new file mode 100644 index 00000000000..60b3ad86b8f --- /dev/null +++ b/dd-java-agent/instrumentation/azure-functions/src/main/java/datadog/trace/instrumentation/azure/functions/HttpRequestMessageExtractAdapter.java @@ -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 { + public static final HttpRequestMessageExtractAdapter GETTER = + new HttpRequestMessageExtractAdapter(); + + @Override + public void forEachKey(HttpRequestMessage carrier, AgentPropagation.KeyClassifier classifier) { + ContextVisitors.stringValuesEntrySet().forEachKey(carrier.getHeaders().entrySet(), classifier); + } +} diff --git a/dd-java-agent/instrumentation/azure-functions/src/test/groovy/AzureFunctionsTest.groovy b/dd-java-agent/instrumentation/azure-functions/src/test/groovy/AzureFunctionsTest.groovy new file mode 100644 index 00000000000..80d9af8dabb --- /dev/null +++ b/dd-java-agent/instrumentation/azure-functions/src/test/groovy/AzureFunctionsTest.groovy @@ -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> request = mock(HttpRequestMessage) + HttpResponseMessage.Builder responseBuilder = mock(HttpResponseMessage.Builder) + HttpResponseMessage response = mock(HttpResponseMessage) + ExecutionContext context = mock(ExecutionContext) + + String functionName = "HttpTest" + Map 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" + } +} diff --git a/dd-java-agent/instrumentation/azure-functions/src/test/groovy/Function.java b/dd-java-agent/instrumentation/azure-functions/src/test/groovy/Function.java new file mode 100644 index 00000000000..94df257d079 --- /dev/null +++ b/dd-java-agent/instrumentation/azure-functions/src/test/groovy/Function.java @@ -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> request, + final ExecutionContext context) { + return request.createResponseBuilder(HttpStatus.OK).body("Hello Datadog test!").build(); + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java b/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java index a0118b62899..c380b6078c9 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/DDSpanTypes.java @@ -37,4 +37,6 @@ public class DDSpanTypes { public static final String MULE = "mule"; public static final String VALKEY = "valkey"; + + public static final String SERVERLESS = "serverless"; } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index d758f675c32..eb8d2be3985 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -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 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 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"; + } 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()) { diff --git a/internal-api/src/main/java/datadog/trace/api/naming/v1/CloudNamingV1.java b/internal-api/src/main/java/datadog/trace/api/naming/v1/CloudNamingV1.java index 2798506b2bb..5f463c851e9 100644 --- a/internal-api/src/main/java/datadog/trace/api/naming/v1/CloudNamingV1.java +++ b/internal-api/src/main/java/datadog/trace/api/naming/v1/CloudNamingV1.java @@ -46,9 +46,13 @@ public String serviceForRequest( @Nonnull @Override public String operationForFaas(@Nonnull final String provider) { - // for now only aws is implemented. For the future provider might be used to return specific - // function as a service name - // (e.g. azure automation) - return "aws.lambda.invoke"; + switch (provider) { + case "aws": + return "aws.lambda.invoke"; + case "azure": + return "azure.functions.invoke"; + default: + return "aws.lambda.invoke"; + } } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java index 6fc420b9e9a..72e47c2a131 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InternalSpanTypes.java @@ -49,4 +49,6 @@ public class InternalSpanTypes { public static final UTF8BytesString TIBCO_BW = UTF8BytesString.create("tibco_bw"); public static final UTF8BytesString MULE = UTF8BytesString.create(DDSpanTypes.MULE); public static final CharSequence VALKEY = UTF8BytesString.create(DDSpanTypes.VALKEY); + + public static final CharSequence SERVERLESS = UTF8BytesString.create(DDSpanTypes.SERVERLESS); } diff --git a/settings.gradle b/settings.gradle index c79530dd5ba..08043a76680 100644 --- a/settings.gradle +++ b/settings.gradle @@ -205,6 +205,7 @@ include ':dd-java-agent:instrumentation:aws-java-s3-2.0' include ':dd-java-agent:instrumentation:aws-lambda-handler' include ':dd-java-agent:instrumentation:axis-2' include ':dd-java-agent:instrumentation:axway-api' +include ':dd-java-agent:instrumentation:azure-functions' include ':dd-java-agent:instrumentation:caffeine' include ':dd-java-agent:instrumentation:cdi-1.2' include ':dd-java-agent:instrumentation:classloading'