diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index fc663bf0b2..33d133e655 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,7 @@ endif::[]
[float]
===== Features
+* Added support for azure blob storage framework -
* Added support for setting service name and version for a transaction via the public api - {pull}2451[#2451]
* Added support for en-/disabling each public annotation on each own - {pull}2472[#2472]
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/pom.xml b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/pom.xml
new file mode 100644
index 0000000000..79a6180385
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/pom.xml
@@ -0,0 +1,20 @@
+
+
+
+ apm-azure-storage
+ co.elastic.apm
+ 1.29.1-SNAPSHOT
+
+ 4.0.0
+
+ apm-azure-storage-core-plugin
+ ${project.groupId}:${project.artifactId}
+
+
+ ${project.basedir}/../../..
+
+
+
+
+
+
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageHelper.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageHelper.java
new file mode 100644
index 0000000000..081c1717e6
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageHelper.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.apm.agent.azurestorage;
+
+import co.elastic.apm.agent.impl.Tracer;
+import co.elastic.apm.agent.impl.transaction.AbstractSpan;
+import co.elastic.apm.agent.impl.transaction.Span;
+
+import java.net.URL;
+import java.util.Map;
+
+public class AzureStorageHelper {
+ private final Tracer tracer;
+ public static String SPAN_NAME = "AzureBlob";
+ public static String SPAN_SUBTYPE = "azureblob";
+ public static String SPAN_TYPE = "storage";
+
+ public AzureStorageHelper(Tracer tracer) {
+ this.tracer = tracer;
+ }
+
+ public Span startAzureStorageSpan(String method, URL url, Map requestHeaderMap) {
+
+ Span span = tracer.createExitChildSpan();
+ if (span == null) {
+ return null;
+ }
+
+ BlobUrl blobUrl = new BlobUrl(url);
+ String action = null;
+
+ String query = url.getQuery() != null ? url.getQuery().toLowerCase() : "";
+
+ switch (method)
+ {
+ case "DELETE":
+ action = "Delete";
+ break;
+ case "GET":
+ if (query.indexOf("restype=container")!=-1)
+ {
+ if (query.indexOf("comp=list")!=-1)
+ action = "ListBlobs";
+ else if (query.indexOf("comp=acl")!=-1)
+ action = "GetAcl";
+ else
+ action = "GetProperties";
+ }
+ else
+ {
+ if (query.indexOf("comp=metadata")!=-1)
+ action = "GetMetadata";
+ else if (query.indexOf("comp=list")!=-1)
+ action = "ListContainers";
+ else if (query.indexOf("comp=tags")!=-1)
+ action = query.indexOf("where=")!=-1 ? "FindTags" : "GetTags";
+ else
+ action = "Download";
+ }
+ break;
+ case "HEAD":
+ if (query.indexOf("comp=metadata")!=-1)
+ action = "GetMetadata";
+ else if (query.indexOf("comp=acl")!=-1)
+ action = "GetAcl";
+ else
+ action = "GetProperties";
+ break;
+ case "POST":
+ if (query.indexOf("comp=batch")!=-1)
+ action = "Batch";
+ else if (query.indexOf("comp=query")!=-1)
+ action = "Query";
+ break;
+ case "PUT":
+ String msCopySource = requestHeaderMap.get("x-ms-copy-source");
+ String msBlobType = requestHeaderMap.get("x-ms-blob-type");
+ if (msCopySource!=null && !"".equals(msCopySource))
+ action = "Copy";
+ else if (query.indexOf("comp=copy")!=-1)
+ action = "Abort";
+ else if ((msBlobType!=null &&!"".equals(msBlobType)) ||
+ query.indexOf("comp=block")!=-1 ||
+ query.indexOf("comp=blocklist")!=-1 ||
+ query.indexOf("comp=page")!=-1 ||
+ query.indexOf("comp=appendblock")!=-1)
+ action = "Upload";
+ else if (query.indexOf("comp=metadata")!=-1)
+ action = "SetMetadata";
+ else if (query.indexOf("comp=acl")!=-1)
+ action = "SetAcl";
+ else if (query.indexOf("comp=properties")!=-1)
+ action = "SetProperties";
+ else if (query.indexOf("comp=lease")!=-1)
+ action = "Lease";
+ else if (query.indexOf("comp=snapshot")!=-1)
+ action = "Snapshot";
+ else if (query.indexOf("comp=undelete")!=-1)
+ action = "Undelete";
+ else if (query.indexOf("comp=tags")!=-1)
+ action = "SetTags";
+ else if (query.indexOf("comp=tier")!=-1)
+ action = "SetTier";
+ else if (query.indexOf("comp=expiry")!=-1)
+ action = "SetExpiry";
+ else if (query.indexOf("comp=seal")!=-1)
+ action = "Seal";
+ else
+ action = "Create";
+
+ break;
+ }
+
+ if (action == null) return null;
+
+ String name = SPAN_NAME + " " + action + " " + blobUrl.getResourceName();
+
+ span.activate()
+ .withAction(action)
+ .withType(SPAN_TYPE)
+ .withSubtype(SPAN_SUBTYPE)
+ .withName(name, AbstractSpan.PRIO_DEFAULT - 1);
+
+ span.appendToName(name);
+
+ span.getContext()
+ .getDestination()
+ .withAddress(blobUrl.getFullyQualifiedNamespace())
+ .withPort(blobUrl.getPort())
+ .getService()
+ .withType(SPAN_TYPE)
+ .withResource(SPAN_SUBTYPE + "/" + blobUrl.getStorageAccountName())
+ .withName(name);
+ return span;
+ }
+}
+
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/BlobUrl.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/BlobUrl.java
new file mode 100644
index 0000000000..d18e75b029
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/BlobUrl.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.apm.agent.azurestorage;
+
+import java.net.URL;
+
+public class BlobUrl {
+ private String resourceName;
+ private String storageAccountName;
+ private String fullyQualifiedNamespace;
+ private int port;
+
+ public BlobUrl(URL url) {
+ resourceName = url.getPath();
+ port = url.getPort();
+ if (port <= 0) {
+ port = 80;
+ }
+ String[] split = url.getHost().split("\\.");
+ if (split.length > 0) {
+ storageAccountName = split[0];
+ }
+ fullyQualifiedNamespace = url.getHost();
+ if ("127".equals(storageAccountName)) {
+ // Dev Mode (azurite) account name is not on url
+ // https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string
+ String[] split1 = resourceName.split("/");
+ if (split1.length > 1) {
+ storageAccountName = split1[1];
+ resourceName = resourceName.substring(storageAccountName.length() + 1);
+ }
+ }
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+
+ public String getStorageAccountName() {
+ return storageAccountName;
+ }
+
+ public String getFullyQualifiedNamespace() {
+ return fullyQualifiedNamespace;
+ }
+
+ public int getPort() {
+ return port;
+ }
+}
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/SpanTrackerHolder.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/SpanTrackerHolder.java
new file mode 100644
index 0000000000..1a521bdf5c
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/SpanTrackerHolder.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.apm.agent.azurestorage;
+
+import co.elastic.apm.agent.impl.transaction.Span;
+import co.elastic.apm.agent.sdk.weakconcurrent.DetachedThreadLocal;
+import co.elastic.apm.agent.sdk.weakconcurrent.WeakConcurrent;
+
+import javax.annotation.Nullable;
+
+public class SpanTrackerHolder {
+ private static DetachedThreadLocal detachedThreadLocal = WeakConcurrent.buildThreadLocal();
+
+ public static SpanTrackerHolder getSpanTrackHolder() {
+ SpanTrackerHolder spanTrackerHolder = detachedThreadLocal.get();
+ if (spanTrackerHolder == null) {
+ spanTrackerHolder = new SpanTrackerHolder();
+ detachedThreadLocal.set(spanTrackerHolder);
+ }
+ return spanTrackerHolder;
+ }
+ public static void removeSpanTrackHolder() {
+ detachedThreadLocal.remove();
+ }
+
+ public static boolean isCreated() {
+ return detachedThreadLocal.get() != null;
+ }
+
+ private SpanTrackerHolder() {
+ }
+
+ @Nullable
+ private Span span;
+ private boolean storageEntrypointCreated;
+
+ @Nullable
+ public Span getSpan() {
+ return span;
+ }
+
+ public void setSpan(Span span) {
+ this.span = span;
+ }
+
+ public boolean isStorageEntrypointCreated() {
+ return storageEntrypointCreated;
+ }
+
+ public void setStorageEntrypointCreated(boolean storageEntrypointCreated) {
+ this.storageEntrypointCreated = storageEntrypointCreated;
+ }
+}
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/package-info.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/package-info.java
new file mode 100644
index 0000000000..3f63f94833
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-core-plugin/src/main/java/co/elastic/apm/agent/azurestorage/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+@NonnullApi
+package co.elastic.apm.agent.azurestorage;
+
+import co.elastic.apm.agent.sdk.NonnullApi;
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/pom.xml b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/pom.xml
new file mode 100644
index 0000000000..0d2da0cd70
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/pom.xml
@@ -0,0 +1,36 @@
+
+
+
+ apm-azure-storage
+ co.elastic.apm
+ 1.29.1-SNAPSHOT
+
+ 4.0.0
+
+ apm-azure-storage-plugin
+ ${project.groupId}:${project.artifactId}
+
+
+ ${project.basedir}/../../..
+ 8
+ ${maven.compiler.target}
+ ${maven.compiler.target}
+ true
+
+
+
+
+ com.azure
+ azure-storage-blob
+ 12.14.3
+ provided
+
+
+
+ ${project.groupId}
+ apm-azure-storage-core-plugin
+ ${project.version}
+
+
+
+
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationRestProxy.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationRestProxy.java
new file mode 100644
index 0000000000..de6ca0fe13
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationRestProxy.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.apm.agent.azurestorage;
+
+import co.elastic.apm.agent.bci.TracerAwareInstrumentation;
+import co.elastic.apm.agent.impl.GlobalTracer;
+import co.elastic.apm.agent.impl.transaction.Span;
+import com.azure.core.http.HttpRequest;
+import com.azure.core.util.Context;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.NamedElement;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import net.bytebuddy.matcher.ElementMatchers;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nullable;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+public class AzureStorageInstrumentationRestProxy extends TracerAwareInstrumentation {
+ static String[] BLOB_HOSTS = new String[] {
+ ".blob.core.windows.net",
+ ".blob.core.usgovcloudapi.net",
+ ".blob.core.chinacloudapi.cn",
+ ".blob.core.cloudapi.de"
+ };
+ @Override
+ public ElementMatcher super NamedElement> getTypeMatcherPreFilter() {
+ return ElementMatchers.nameContains("RestProxy");
+ }
+
+ @Override
+ public ElementMatcher super TypeDescription> getTypeMatcher() {
+ return named("com.azure.core.http.rest.RestProxy");
+ }
+
+ /**
+ */
+ @Override
+ public ElementMatcher super MethodDescription> getMethodMatcher() {
+ return named("send")
+ .and(takesArguments(2))
+ .and(takesArgument(0, named("com.azure.core.http.HttpRequest")))
+ .and(takesArgument(1, named("com.azure.core.util.Context")));
+ }
+
+ @Override
+ public Collection getInstrumentationGroupNames() {
+ return Collections.singletonList("azurestorage");
+ }
+
+ @Override
+ public String getAdviceClassName() {
+ return "co.elastic.apm.agent.azurestorage.AzureStorageInstrumentationRestProxy$AzureStorageAdvice";
+ }
+
+ public static class AzureStorageAdvice {
+ private static final AzureStorageHelper azureHelper = new AzureStorageHelper(GlobalTracer.get());
+
+ @Nullable
+ @Advice.OnMethodEnter(suppress = Throwable.class, inline = false)
+ public static Object onEnter(@Advice.Argument(0) HttpRequest request,@Advice.Argument(1) Context context) {
+ URL url = request.getUrl();
+ boolean matches = false;
+ if (url != null && url.getHost() != null) {
+ String host = url.getHost().toLowerCase();
+ for (String checkHost : BLOB_HOSTS) {
+ if (host.indexOf(checkHost) != 0) {
+ matches = true;
+ break;
+ }
+ }
+ }
+ if (!matches) return null;
+ SpanTrackerHolder spanTrackerHolder = SpanTrackerHolder.getSpanTrackHolder();
+ Map requestHeaderMap = request.getHeaders().toMap();
+ String httpMethod = request.getHttpMethod().toString();
+ Span span = azureHelper.startAzureStorageSpan(httpMethod, url, requestHeaderMap);
+ spanTrackerHolder.setSpan(span);
+ return spanTrackerHolder;
+ }
+
+ @Nullable
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false)
+ public static Object onExit(@Advice.Thrown @Nullable Throwable thrown,
+ @Advice.Return @Nullable Mono> returnValue,
+ @Nullable @Advice.Enter Object spanTrackerHolderObj) {
+ if (thrown != null || returnValue == null) {
+ // in case of thrown exception, we don't need to wrap to end transaction
+ return returnValue;
+ }
+ SpanTrackerHolder spanTrackerHolder = (SpanTrackerHolder) spanTrackerHolderObj;
+ if (spanTrackerHolder != null && !spanTrackerHolder.isStorageEntrypointCreated() && spanTrackerHolder.getSpan() != null) {
+ // If the spanTrackHolder not created on StorageEntrypoint, finish span
+ spanTrackerHolder.getSpan().captureException(thrown).deactivate();
+ spanTrackerHolder.getSpan().end();
+ SpanTrackerHolder.removeSpanTrackHolder();
+ }
+ return returnValue;
+ }
+ }
+}
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationStorageEntrypoint.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationStorageEntrypoint.java
new file mode 100644
index 0000000000..72b2a66e3e
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationStorageEntrypoint.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.apm.agent.azurestorage;
+
+import co.elastic.apm.agent.bci.TracerAwareInstrumentation;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.NamedElement;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import net.bytebuddy.matcher.ElementMatchers;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.Collections;
+
+import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+public class AzureStorageInstrumentationStorageEntrypoint extends TracerAwareInstrumentation {
+ @Override
+ public ElementMatcher super NamedElement> getTypeMatcherPreFilter() {
+ return ElementMatchers.nameStartsWith("com.azure.storage.blob");
+ }
+
+ @Override
+ public ElementMatcher super TypeDescription> getTypeMatcher() {
+ return named("com.azure.storage.blob.BlobAsyncClient")
+ .or(named("com.azure.storage.blob.BlobClient"))
+ .or(named("com.azure.storage.blob.BlobContainerAsyncClient"))
+ .or(named("com.azure.storage.blob.BlobContainerClient"))
+ .or(named("com.azure.storage.blob.BlobServiceAsyncClient"))
+ .or(named("com.azure.storage.blob.BlobServiceClient"))
+ .or(named("com.azure.storage.blob.implementation.AppendBlobsImpl"))
+ .or(named("com.azure.storage.blob.implementation.BlobsImpl"))
+ .or(named("com.azure.storage.blob.implementation.BlockBlobsImpl"))
+ .or(named("com.azure.storage.blob.implementation.ContainersImpl"))
+ .or(named("com.azure.storage.blob.implementation.PageBlobsImpl"))
+ .or(named("com.azure.storage.blob.implementation.ServicesImpl"))
+ .or(named("com.azure.storage.blob.specialized.AppendBlobAsyncClient"))
+ .or(named("com.azure.storage.blob.specialized.AppendBlobClient"))
+ .or(named("com.azure.storage.blob.specialized.BlobAsyncClientBase"))
+ .or(named("com.azure.storage.blob.specialized.BlobClientBase"))
+ .or(named("com.azure.storage.blob.specialized.BlobLeaseAsyncClient"))
+ .or(named("com.azure.storage.blob.specialized.BlobLeaseClient"))
+ .or(named("com.azure.storage.blob.specialized.BlockBlobAsyncClient"))
+ .or(named("com.azure.storage.blob.specialized.BlockBlobClient"))
+ .or(named("com.azure.storage.blob.specialized.PageBlobAsyncClient"))
+ .or(named("com.azure.storage.blob.specialized.PageBlobClient"));
+ }
+
+ @Override
+ public ElementMatcher super MethodDescription> getMethodMatcher() {
+ return isAnnotatedWith(named("com.azure.core.annotation.ServiceMethod"));
+ }
+
+ @Override
+ public Collection getInstrumentationGroupNames() {
+ return Collections.singletonList("azurestorage");
+ }
+
+ @Override
+ public String getAdviceClassName() {
+ return "co.elastic.apm.agent.azurestorage.AzureStorageInstrumentationStorageEntrypoint$StorageEntrypointAdvice";
+ }
+
+ public static class StorageEntrypointAdvice {
+
+ @Nullable
+ @Advice.OnMethodEnter(suppress = Throwable.class, inline = false)
+ public static Object onEnter() {
+ if (!SpanTrackerHolder.isCreated()) {
+ SpanTrackerHolder spanTrackerHolder = SpanTrackerHolder.getSpanTrackHolder();
+ spanTrackerHolder.setStorageEntrypointCreated(true);
+ return spanTrackerHolder;
+ }
+ return null;
+ }
+
+ @Nullable
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false)
+ public static void onExit(@Advice.Thrown @Nullable Throwable thrown,
+ @Nullable @Advice.Enter Object spanTrackerHolderObj) {
+ if (thrown != null ) {
+ // in case of thrown exception, we don't need to wrap to end transaction
+ return ;
+ }
+ SpanTrackerHolder spanTrackerHolder = (SpanTrackerHolder) spanTrackerHolderObj;
+ if (spanTrackerHolder != null && spanTrackerHolder.isStorageEntrypointCreated()) {
+ if (spanTrackerHolder.getSpan() != null) {
+ spanTrackerHolder.getSpan().captureException(thrown).deactivate();
+ spanTrackerHolder.getSpan().end();
+ }
+ SpanTrackerHolder.removeSpanTrackHolder();
+ }
+ }
+
+ }
+}
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/package-info.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/package-info.java
new file mode 100644
index 0000000000..3f63f94833
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/java/co/elastic/apm/agent/azurestorage/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+@NonnullApi
+package co.elastic.apm.agent.azurestorage;
+
+import co.elastic.apm.agent.sdk.NonnullApi;
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation
new file mode 100644
index 0000000000..d28b51f138
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation
@@ -0,0 +1,2 @@
+co.elastic.apm.agent.azurestorage.AzureStorageInstrumentationStorageEntrypoint
+co.elastic.apm.agent.azurestorage.AzureStorageInstrumentationRestProxy
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/test/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationIT.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/test/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationIT.java
new file mode 100644
index 0000000000..b2f4977755
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/test/java/co/elastic/apm/agent/azurestorage/AzureStorageInstrumentationIT.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.apm.agent.azurestorage;
+
+import co.elastic.apm.agent.AbstractInstrumentationTest;
+import co.elastic.apm.agent.impl.context.Destination;
+import co.elastic.apm.agent.impl.transaction.Span;
+import co.elastic.apm.agent.impl.transaction.Transaction;
+import co.elastic.apm.agent.testutils.TestContainersUtils;
+import com.azure.core.http.rest.PagedIterable;
+import com.azure.core.util.BinaryData;
+import com.azure.storage.blob.BlobClient;
+import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.blob.models.BlobItem;
+import com.azure.storage.blob.models.PageRange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.shaded.org.bouncycastle.util.Arrays;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Testcontainers
+class AzureStorageInstrumentationIT extends AbstractInstrumentationTest {
+
+ @Container
+ public static GenericContainer> azurite = new GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.15.0")
+ .withExposedPorts(10000,10001)
+ .withLogConsumer(TestContainersUtils.createSlf4jLogConsumer(AzureStorageInstrumentationIT.class))
+ .withStartupTimeout(Duration.ofSeconds(120))
+ .withCreateContainerCmdModifier(TestContainersUtils.withMemoryLimit(2048));
+ private static int blobPort;
+ private Transaction transaction;
+ private BlobServiceClient session;
+
+ @BeforeAll
+ public static void beforeClass() throws Exception {
+ AzureStorageInstrumentationRestProxy.BLOB_HOSTS = Arrays.append(AzureStorageInstrumentationRestProxy.BLOB_HOSTS, "localhost");
+ blobPort = azurite.getMappedPort(10000);
+ }
+
+ private static BlobServiceClient getSession() {
+ return new BlobServiceClientBuilder()
+ .connectionString("DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey="+
+ "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:"
+ + blobPort + "/devstoreaccount1")
+ .buildClient();
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ transaction = tracer.startRootTransaction(null).withName("transaction").activate();
+ session = getSession();
+
+ }
+
+ @AfterEach
+ void tearDown() {
+ Optional.ofNullable(transaction).ifPresent(t -> t.deactivate().end());
+ }
+
+ @Test
+ void Should_Capture_Span_When_Create_Container() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ session.createBlobContainer(containerName);
+ reporter.awaitSpanCount(1);
+ AssertSpan("Create", containerName, 1);
+ }
+
+ @Test
+ void Should_Capture_Span_When_Delete_Container() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ session.createBlobContainer(containerName);
+ reporter.reset();
+ session.deleteBlobContainer(containerName);
+ reporter.awaitSpanCount(1);
+ AssertSpan("Delete", containerName, 1);
+ }
+
+ @Test
+ void Should_Capture_Span_When_Create_Page_Blob() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ String blobName = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ reporter.reset();
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ blobClient.getPageBlobClient().create(1024);
+ reporter.awaitSpanCount(1);
+ AssertSpan("Upload", containerName + "/" + blobName, 1);
+ }
+
+ @Test
+ void Should_Capture_Span_When_Upload_Page_Blob() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ String blobName = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ blobClient.getPageBlobClient().create(1024);
+ reporter.reset();
+ ByteArrayInputStream ba = new ByteArrayInputStream(new byte[512]);
+ blobClient.getPageBlobClient().uploadPages(new PageRange().setStart(0).setEnd(511), ba);
+ reporter.awaitSpanCount(1);
+ AssertSpan("Upload", containerName + "/" + blobName, 1);
+ }
+
+ @Test
+ void Should_Capture_Span_When_Upload_Block_Blob() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ String blobName = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ reporter.reset();
+ blobClient.upload(BinaryData.fromString("block blob"));
+ reporter.awaitSpanCount(1);
+ AssertSpan("Upload", containerName + "/" + blobName, 1);
+ }
+
+ @Test
+ void Should_Capture_Span_When_Download_Blob() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ String blobName = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ blobClient.upload(BinaryData.fromString("block blob"));
+ reporter.reset();
+ BinaryData bd = blobClient.downloadContent();
+ reporter.awaitSpanCount(1);
+ AssertSpan("Download", containerName + "/" + blobName, 1);
+ }
+
+
+ @Test
+ void Should_Capture_Span_When_Download_Streaming_Blob() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ String blobName = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ blobClient.upload(BinaryData.fromString("block blob"));
+ reporter.reset();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ blobClient.download(bos);
+ reporter.awaitSpanCount(1);
+ AssertSpan("Download", containerName + "/" + blobName, 1);
+ }
+
+ @Test
+ void Should_Capture_Span_When_Delete_Blob() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ String blobName = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ blobClient.upload(BinaryData.fromString("block blob"));
+ reporter.reset();
+ blobClient.delete();
+ reporter.awaitSpanCount(1);
+ AssertSpan("Delete", containerName + "/" + blobName, 1);
+ }
+
+ @Test
+ void Should_Capture_Span_When_Copy_From_Uri() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ String blobName = java.util.UUID.randomUUID().toString();
+ String blobName2 = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ blobClient.upload(BinaryData.fromString("block blob"));
+ reporter.reset();
+ BlobClient blobClient2 = blobContainerClient.getBlobClient(blobName2);
+ blobClient2.copyFromUrl(blobClient.getBlobUrl());
+ reporter.awaitSpanCount(1);
+ AssertSpan("Copy", containerName + "/" + blobName2, 1);
+ }
+ @Test
+ void Should_Capture_Span_When_Get_Blobs() throws Exception {
+ String containerName = java.util.UUID.randomUUID().toString();
+ BlobContainerClient blobContainerClient = session.createBlobContainer(containerName);
+ for (int i = 0; i < 2; i++) {
+
+ String blobName = java.util.UUID.randomUUID().toString();
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ blobClient.upload(BinaryData.fromString("block blob"));
+ }
+ reporter.reset();
+ PagedIterable listBlobs = blobContainerClient.listBlobs();
+ for (BlobItem bi : listBlobs) {
+ System.out.println(bi.getName());
+ }
+ reporter.awaitSpanCount(1);
+ AssertSpan("ListBlobs", containerName , 1);
+ }
+
+ private void AssertSpan(String action, String containerName, int count) {
+ List spans = reporter.getSpans();
+ assertThat(spans).hasSize(count);
+ Span span = reporter.getFirstSpan();
+
+ assertThat(span.getNameAsString()).isEqualTo(AzureStorageHelper.SPAN_NAME + " " + action + " /"+containerName);
+ assertThat(span.getType()).isEqualTo(AzureStorageHelper.SPAN_TYPE);
+ assertThat(span.getSubtype()).isEqualTo(AzureStorageHelper.SPAN_SUBTYPE);
+ assertThat(span.getAction()).isEqualTo(action);
+ assertThat(span.getContext().getDestination()).isNotNull();
+ Destination destination = span.getContext().getDestination();
+
+ assertThat(destination.getAddress().toString()).isEqualTo("127.0.0.1");
+ assertThat(destination.getService().getResource().toString()).isEqualTo("azureblob/devstoreaccount1");
+ }
+
+}
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/test/java/co/elastic/apm/agent/azurestorage/BlobUrlTest.java b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/test/java/co/elastic/apm/agent/azurestorage/BlobUrlTest.java
new file mode 100644
index 0000000000..e0e28fc019
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/apm-azure-storage-plugin/src/test/java/co/elastic/apm/agent/azurestorage/BlobUrlTest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.apm.agent.azurestorage;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.URL;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BlobUrlTest {
+
+ @Test
+ void Should_Create_Uri_Azure() throws Exception {
+ URL url = new URL("https" , "account.blob.core.windows.net", "/container");
+ BlobUrl blobUrl = new BlobUrl(url);
+ assertThat(blobUrl.getFullyQualifiedNamespace()).isEqualTo("account.blob.core.windows.net");
+ assertThat(blobUrl.getStorageAccountName()).isEqualTo("account");
+ assertThat(blobUrl.getResourceName()).isEqualTo("/container");
+ }
+
+ @Test
+ void Should_Create_Uri_Azurite() throws Exception {
+ URL url = new URL("http" , "127.0.0.1", "/account/container");
+ BlobUrl blobUrl = new BlobUrl(url);
+ assertThat(blobUrl.getFullyQualifiedNamespace()).isEqualTo("127.0.0.1");
+ assertThat(blobUrl.getStorageAccountName()).isEqualTo("account");
+ assertThat(blobUrl.getResourceName()).isEqualTo("/container");
+ }
+}
diff --git a/apm-agent-plugins/apm-azure-storage-plugin/pom.xml b/apm-agent-plugins/apm-azure-storage-plugin/pom.xml
new file mode 100644
index 0000000000..e35eaaf361
--- /dev/null
+++ b/apm-agent-plugins/apm-azure-storage-plugin/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ apm-agent-plugins
+ co.elastic.apm
+ 1.29.1-SNAPSHOT
+
+ 4.0.0
+
+ apm-azure-storage
+ ${project.groupId}:${project.artifactId}
+
+ apm-azure-storage-core-plugin
+ apm-azure-storage-plugin
+
+ pom
+
+
+ ${project.basedir}/../..
+
+
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+
diff --git a/apm-agent-plugins/pom.xml b/apm-agent-plugins/pom.xml
index 522023c69e..021d14ae79 100644
--- a/apm-agent-plugins/pom.xml
+++ b/apm-agent-plugins/pom.xml
@@ -71,6 +71,7 @@
apm-scheduled-annotation-plugin-jakartaee-test
apm-jakarta-websocket-plugin
apm-ecs-logging-plugin
+ apm-azure-storage-plugin
diff --git a/apm-agent/pom.xml b/apm-agent/pom.xml
index 169c4e56f6..cdeff84a78 100644
--- a/apm-agent/pom.xml
+++ b/apm-agent/pom.xml
@@ -51,6 +51,11 @@
apm-asynchttpclient-plugin
${project.version}
+
+ ${project.groupId}
+ apm-azure-storage-plugin
+ ${project.version}
+
${project.groupId}
apm-cassandra3-plugin