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 getTypeMatcherPreFilter() { + return ElementMatchers.nameContains("RestProxy"); + } + + @Override + public ElementMatcher getTypeMatcher() { + return named("com.azure.core.http.rest.RestProxy"); + } + + /** + */ + @Override + public ElementMatcher 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 getTypeMatcherPreFilter() { + return ElementMatchers.nameStartsWith("com.azure.storage.blob"); + } + + @Override + public ElementMatcher 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 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