From 6d6aba36c63a4b992c19a8296409625a9b97a2cf Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 10 Mar 2021 17:20:18 +1000 Subject: [PATCH 01/32] Instrument send --- ElasticApmAgent.sln | 14 ++ .../terraform/azure/service_bus_resources.tf | 102 ++++++++++++ .../AzureServiceBusDiagnosticListener.cs | 150 ++++++++++++++++++ .../AzureServiceBusDiagnosticsSubscriber.cs | 32 ++++ .../Elastic.Apm.Azure.ServiceBus.csproj | 11 ++ .../ElasticsearchDiagnosticsListenerBase.cs | 2 +- .../ElasticApmProfiler.cs | 2 +- src/Elastic.Apm/ApmAgentExtensions.cs | 5 + src/Elastic.Apm/Elastic.Apm.csproj | 2 + .../Model/ExecutionSegmentCommon.cs | 5 +- .../AzureServiceBusDiagnosticListenerTests.cs | 99 ++++++++++++ .../AzureServiceBusTestEnvironment.cs | 26 +++ .../Elastic.Apm.Azure.ServiceBus.Tests.csproj | 25 +++ .../QueueScope.cs | 35 ++++ .../TopicScope.cs | 35 ++++ .../Elastic.Apm.Tests.Utilities.csproj | 1 + 16 files changed, 540 insertions(+), 6 deletions(-) create mode 100644 build/terraform/azure/service_bus_resources.tf create mode 100644 src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs create mode 100644 src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs create mode 100644 src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/QueueScope.cs create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/TopicScope.cs diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index e0e02cba1..4f1391099 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -131,6 +131,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.Extensions.Logg EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Extensions.Logging.Tests", "test\Elastic.Apm.Extensions.Logging.Tests\Elastic.Apm.Extensions.Logging.Tests.csproj", "{B235B13F-42AE-42DA-A3C8-20D047F38685}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus", "src\Elastic.Apm.Azure.ServiceBus\Elastic.Apm.Azure.ServiceBus.csproj", "{1D43C8C5-4116-45C5-9F4B-56C1D926ED29}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus.Tests", "test\Elastic.Apm.Azure.ServiceBus.Tests\Elastic.Apm.Azure.ServiceBus.Tests.csproj", "{D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution test\Elastic.Apm.DatabaseTests.Common\Elastic.Apm.DatabaseTests.Common.projitems*{968e1e85-e996-42de-9845-d20dae16165a}*SharedItemsImports = 5 @@ -324,6 +328,14 @@ Global {B235B13F-42AE-42DA-A3C8-20D047F38685}.Debug|Any CPU.Build.0 = Debug|Any CPU {B235B13F-42AE-42DA-A3C8-20D047F38685}.Release|Any CPU.ActiveCfg = Release|Any CPU {B235B13F-42AE-42DA-A3C8-20D047F38685}.Release|Any CPU.Build.0 = Release|Any CPU + {1D43C8C5-4116-45C5-9F4B-56C1D926ED29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D43C8C5-4116-45C5-9F4B-56C1D926ED29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D43C8C5-4116-45C5-9F4B-56C1D926ED29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D43C8C5-4116-45C5-9F4B-56C1D926ED29}.Release|Any CPU.Build.0 = Release|Any CPU + {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -375,6 +387,8 @@ Global {9AE4805D-2586-4FA5-A0D0-885264EBC565} = {267A241E-571F-458F-B04C-B6C4DE79E735} {9BAEEF56-4061-488A-8FB8-28BDBBB26C3D} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} {B235B13F-42AE-42DA-A3C8-20D047F38685} = {267A241E-571F-458F-B04C-B6C4DE79E735} + {1D43C8C5-4116-45C5-9F4B-56C1D926ED29} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} + {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D} = {267A241E-571F-458F-B04C-B6C4DE79E735} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69E02FD9-C9DE-412C-AB6B-5B8BECC6BFA5} diff --git a/build/terraform/azure/service_bus_resources.tf b/build/terraform/azure/service_bus_resources.tf new file mode 100644 index 000000000..daae06518 --- /dev/null +++ b/build/terraform/azure/service_bus_resources.tf @@ -0,0 +1,102 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=2.46.0" + } + } +} + +provider "azurerm" { + features {} +} + +# configuration is sourced from the following environment variables: +# ARM_CLIENT_ID +# ARM_CLIENT_SECRET +# ARM_SUBSCRIPTION_ID +# ARM_TENANT_ID +# +# See https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret +# for creating a Service Principal and Client Secret +data "azurerm_client_config" "current" { +} + +variable "resource_group" { + type = string + description = "The name of the resource group to create" + + # TODO validation +} + +variable "location" { + type = string + description = "The Azure location in which to deploy resources" + + # TODO validation +} + +variable "servicebus_namespace" { + type = string + description = "The name of the servicebus namespace to create" + +// validation { +// condition = can(regex("^[a-zA-Z][a-zA-Z0-9-]{5,49}$", var.servicebus_namespace)) && can(regex("[^\\-|\\-sb|\\-mgmt]$", var.servicebus_namespace)) +// error_message = "The value must be a valid service bus namespace. See https://docs.microsoft.com/en-us/rest/api/servicebus/create-namespace." +// } +} + + +resource "azurerm_resource_group" "servicebus_resource_group" { + name = var.resource_group + location = var.location +} + +resource "azurerm_servicebus_namespace" "servicebus_namespace" { + location = azurerm_resource_group.servicebus_resource_group.location + name = var.servicebus_namespace + resource_group_name = azurerm_resource_group.servicebus_resource_group.name + sku = "Basic" + depends_on = [azurerm_resource_group.servicebus_resource_group] +} + +# random name to generate for the contributor role assignment +resource "random_uuid" "contributor_role" { + keepers = { + client_id = data.azurerm_client_config.current.client_id + } +} + +resource "azurerm_role_assignment" "contributor_role" { + name = random_uuid.contributor_role.result + principal_id = data.azurerm_client_config.current.object_id + role_definition_name = "Contributor" + scope = azurerm_resource_group.servicebus_resource_group.id + depends_on = [azurerm_servicebus_namespace.servicebus_namespace] +} + +# random name to generate for the contributor role assignment +resource "random_uuid" "data_owner_role" { + keepers = { + client_id = data.azurerm_client_config.current.client_id + } +} + +resource "azurerm_role_assignment" "servicebus_data_owner_role" { + name = random_uuid.data_owner_role.result + principal_id = data.azurerm_client_config.current.object_id + role_definition_name = "Azure Service Bus Data Owner" + scope = azurerm_resource_group.servicebus_resource_group.id + depends_on = [azurerm_servicebus_namespace.servicebus_namespace] +} + +# following role assignment, there can be a delay of up to ~1 minute +# for the assignments to propagate in Azure. You may need to introduce +# a wait before using the Azure resources created. + +output "connection_string" { + value = azurerm_servicebus_namespace.servicebus_namespace.default_primary_connection_string + description = "The service bus primary connection string" + sensitive = true +} + diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs new file mode 100644 index 000000000..b206bab06 --- /dev/null +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.Logging; +using Elastic.Apm.Model; + +namespace Elastic.Apm.Azure.ServiceBus +{ + public class AzureServiceBusDiagnosticListener: IDiagnosticListener + { + private readonly IApmAgent _agent; + + private readonly ConcurrentDictionary _sendSpans = new ConcurrentDictionary(); + + internal IApmLogger Logger { get; } + + public AzureServiceBusDiagnosticListener(IApmAgent agent) + { + _agent = agent; + Logger = _agent.Logger.Scoped(nameof(AzureServiceBusDiagnosticListener)); + } + + public void OnCompleted() { } + + public void OnError(Exception error) => Logger.Error()?.LogExceptionWithCaller(error, nameof(OnError)); + + public void OnNext(KeyValuePair kv) + { + Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); + + if (string.IsNullOrEmpty(kv.Key)) + { + Logger.Trace()?.Log($"Key is {(kv.Key == null ? "null" : "an empty string")} - exiting"); + return; + } + + switch (kv.Key) + { + case "ServiceBusSender.Send.Start": + OnSendStart(kv); + break; + case "ServiceBusSender.Send.Stop": + OnSendStop(); + break; + case "ServiceBusSender.Send.Exception": + break; + case "ServiceBusSender.Schedule.Start": + break; + case "ServiceBusSender.Schedule.Stop": + break; + case "ServiceBusSender.Schedule.Exception": + break; + case "ServiceBusReceiver.Receive.Start": + break; + case "ServiceBusReceiver.Receive.Stop": + break; + case "ServiceBusReceiver.Receive.Exception": + break; + case "ServiceBusReceiver.ReceiveDeferred.Start": + break; + case "ServiceBusReceiver.ReceiveDeferred.Stop": + break; + case "ServiceBusReceiver.ReceiveDeferred.Exception": + break; + default: + Logger.Trace()?.Log("Unrecognized key `{DiagnosticEventKey}'", kv.Key); + break; + } + } + + private void OnSendStart(KeyValuePair kv) + { + var currentSegment = _agent.GetCurrentExecutionSegment(); + if (currentSegment is null) + { + Logger.Trace()?.Log("No current transaction or span - exiting"); + return; + } + + if (!(kv.Value is Activity activity)) + { + Logger.Trace()?.Log("Value is not an activity - exiting"); + return; + } + + string queueName = null; + string destinationAddress = null; + foreach (var tag in activity.Tags) + { + switch (tag.Key) + { + case "message_bus.destination": + queueName = tag.Value; + break; + case "peer.address": + destinationAddress = tag.Value; + break; + default: + continue; + } + } + + var spanName = queueName is null + ? "AzureServiceBus SEND" + : $"AzureServiceBus SEND to {queueName}"; + var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", "send"); + span.Context.Destination = new Destination + { + Address = destinationAddress, + Service = new Destination.DestinationService + { + Name = "azureservicebus", + Resource = queueName is null ? "azureservicebus" : $"azureservicebus/{queueName}", + Type = "messaging" + } + }; + + if (!_sendSpans.TryAdd(activity.Id, span)) + { + Logger.Error()? + .Log("Could not add send span {SpanId} for activity {ActivityId} to tracked spans", span.Id, activity.Id); + } + } + + private void OnSendStop() + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_sendSpans.TryRemove(activity.Id, out var span)) + { + Logger.Error()? + .Log("Could not get span for activity {ActivityId} from tracked spans", activity.Id); + return; + } + + span.End(); + } + + + public string Name { get; } = "Azure.Messaging.ServiceBus"; + } +} diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs new file mode 100644 index 000000000..921072217 --- /dev/null +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs @@ -0,0 +1,32 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Diagnostics; +using Elastic.Apm.DiagnosticListeners; +using Elastic.Apm.DiagnosticSource; + +namespace Elastic.Apm.Azure.ServiceBus +{ + public class AzureServiceBusDiagnosticsSubscriber : IDiagnosticsSubscriber + { + /// + /// Start listening for HttpClient diagnostic source events. + /// + public IDisposable Subscribe(IApmAgent agent) + { + var retVal = new CompositeDisposable(); + + var initializer = new DiagnosticInitializer(agent.Logger, new[] { new AzureServiceBusDiagnosticListener(agent) }); + retVal.Add(initializer); + + retVal.Add(DiagnosticListener + .AllListeners + .Subscribe(initializer)); + + return retVal; + } + } +} diff --git a/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj new file mode 100644 index 000000000..3dcd8ec87 --- /dev/null +++ b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs b/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs index 952384eea..96a572fdd 100644 --- a/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs +++ b/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs @@ -50,7 +50,7 @@ internal bool TryStartElasticsearchSpan(string name, out Span span, Uri instance if (transaction == null) return false; - span = (Span)ExecutionSegmentCommon.GetCurrentExecutionSegment(ApmAgent) + span = (Span)_agent.GetCurrentExecutionSegment() .StartSpan( name, ApiConstants.TypeDb, diff --git a/src/Elastic.Apm.StackExchange.Redis/ElasticApmProfiler.cs b/src/Elastic.Apm.StackExchange.Redis/ElasticApmProfiler.cs index 5627584a1..973f00593 100644 --- a/src/Elastic.Apm.StackExchange.Redis/ElasticApmProfiler.cs +++ b/src/Elastic.Apm.StackExchange.Redis/ElasticApmProfiler.cs @@ -45,7 +45,7 @@ public ProfilingSession GetProfilingSession() if (!Agent.Config.Enabled || !Agent.Config.Recording) return null; - var executionSegment = ExecutionSegmentCommon.GetCurrentExecutionSegment(_agent.Value); + var executionSegment = _agent.Value.GetCurrentExecutionSegment(); var realSpan = executionSegment as Span; Transaction realTransaction = null; diff --git a/src/Elastic.Apm/ApmAgentExtensions.cs b/src/Elastic.Apm/ApmAgentExtensions.cs index 7bc13a171..c2aff2eb7 100644 --- a/src/Elastic.Apm/ApmAgentExtensions.cs +++ b/src/Elastic.Apm/ApmAgentExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Elastic.Apm.Api; using Elastic.Apm.DiagnosticSource; namespace Elastic.Apm @@ -40,6 +41,10 @@ public static IDisposable Subscribe(this IApmAgent agent, params IDiagnosticsSub return disposable; } + + internal static IExecutionSegment GetCurrentExecutionSegment(this IApmAgent agent) => + agent.Tracer.CurrentSpan ?? (IExecutionSegment)agent.Tracer.CurrentTransaction; + } /// diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index fe0f038b7..cf704d881 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -44,6 +44,8 @@ + + diff --git a/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs b/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs index ca7e40a9f..6cf1f2634 100644 --- a/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs +++ b/src/Elastic.Apm/Model/ExecutionSegmentCommon.cs @@ -258,14 +258,11 @@ public static void CaptureError( }); } - internal static IExecutionSegment GetCurrentExecutionSegment(IApmAgent agent) => - agent.Tracer.CurrentSpan ?? (IExecutionSegment)agent.Tracer.CurrentTransaction; - internal static ISpan StartSpanOnCurrentExecutionSegment(IApmAgent agent, string spanName, string spanType, string subType = null, InstrumentationFlag instrumentationFlag = InstrumentationFlag.None, bool captureStackTraceOnStart = false ) { - var currentExecutionSegment = GetCurrentExecutionSegment(agent); + var currentExecutionSegment = agent.GetCurrentExecutionSegment(); if (currentExecutionSegment == null) return null; diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs new file mode 100644 index 000000000..b14a72b56 --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; +using Elastic.Apm.Tests.Utilities; +using FluentAssertions; +using Xunit; + +namespace Elastic.Apm.Azure.ServiceBus.Tests +{ + // Resource name rules + // https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules + public class AzureServiceBusDiagnosticListenerTests : IClassFixture, IDisposable, IAsyncDisposable + { + private readonly AzureServiceBusTestEnvironment _environment; + private readonly ApmAgent _agent; + private readonly MockPayloadSender _sender; + private readonly ServiceBusClient _client; + private readonly ServiceBusAdministrationClient _adminClient; + + public AzureServiceBusDiagnosticListenerTests(AzureServiceBusTestEnvironment environment) + { + _environment = environment; + + var logger = new NoopLogger(); + _sender = new MockPayloadSender(logger); + _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); + _agent.Subscribe(new AzureServiceBusDiagnosticsSubscriber()); + + _adminClient = new ServiceBusAdministrationClient(environment.ServiceBusConnectionString); + _client = new ServiceBusClient(environment.ServiceBusConnectionString); + } + + [Fact] + public async Task Capture_Span_When_Send_To_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = _client.CreateSender(scope.QueueName); + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendMessageAsync(new ServiceBusMessage("test message")).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans(TimeSpan.FromMinutes(2))) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SEND to {scope.QueueName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("send"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + var properties = ServiceBusConnectionStringProperties.Parse(_environment.ServiceBusConnectionString); + + destination.Address.Should().Be(properties.FullyQualifiedNamespace); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Span_When_Send_To_Topic() + { + await using var scope = await TopicScope.CreateWithTopic(_adminClient); + var sender = _client.CreateSender(scope.TopicName); + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendMessageAsync(new ServiceBusMessage("test message")).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans(TimeSpan.FromMinutes(2))) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SEND to {scope.TopicName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("send"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + var properties = ServiceBusConnectionStringProperties.Parse(_environment.ServiceBusConnectionString); + destination.Address.Should().Be(properties.FullyQualifiedNamespace); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); + destination.Service.Type.Should().Be("messaging"); + } + + public void Dispose() => _agent.Dispose(); + + public ValueTask DisposeAsync() => _client.DisposeAsync(); + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs new file mode 100644 index 000000000..5c9d7e952 --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs @@ -0,0 +1,26 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; + +namespace Elastic.Apm.Azure.ServiceBus.Tests +{ + public class AzureServiceBusTestEnvironment + { + public AzureServiceBusTestEnvironment() + { + var serviceBusConnectionString = Environment.GetEnvironmentVariable("AZURE_SERVICE_BUS_CONNECTION_STRING"); + if (string.IsNullOrEmpty(serviceBusConnectionString)) + { + throw new ArgumentException( + "connection string for Azure Service Bus required. A connection string can be passed with AZURE_SERVICE_BUS_CONNECTION_STRING environment variable"); + } + + ServiceBusConnectionString = serviceBusConnectionString; + } + + public string ServiceBusConnectionString { get; } + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj new file mode 100644 index 000000000..fa1fb1408 --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj @@ -0,0 +1,25 @@ + + + + net5.0 + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/QueueScope.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/QueueScope.cs new file mode 100644 index 000000000..e897aac14 --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/QueueScope.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus.Administration; + +namespace Elastic.Apm.Azure.ServiceBus.Tests +{ + public class QueueScope : IAsyncDisposable + { + public string QueueName { get; } + private readonly QueueProperties _properties; + private readonly ServiceBusAdministrationClient _adminClient; + + private QueueScope(ServiceBusAdministrationClient adminClient, string queueName, QueueProperties properties) + { + _adminClient = adminClient; + QueueName = queueName; + _properties = properties; + } + + public static async Task CreateWithQueue(ServiceBusAdministrationClient adminClient) + { + var queueName = Guid.NewGuid().ToString("D"); + var response = await adminClient.CreateQueueAsync(queueName).ConfigureAwait(false); + return new QueueScope(adminClient, queueName, response.Value); + } + + public async ValueTask DisposeAsync() => + await _adminClient.DeleteQueueAsync(QueueName).ConfigureAwait(false); + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/TopicScope.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/TopicScope.cs new file mode 100644 index 000000000..822cf1ee6 --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/TopicScope.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus.Administration; + +namespace Elastic.Apm.Azure.ServiceBus.Tests +{ + public class TopicScope : IAsyncDisposable + { + public string TopicName { get; } + private readonly TopicProperties _properties; + private readonly ServiceBusAdministrationClient _adminClient; + + private TopicScope(ServiceBusAdministrationClient adminClient, string queueName, TopicProperties properties) + { + _adminClient = adminClient; + TopicName = queueName; + _properties = properties; + } + + public static async Task CreateWithTopic(ServiceBusAdministrationClient adminClient) + { + var topicName = Guid.NewGuid().ToString("D"); + var response = await adminClient.CreateTopicAsync(topicName).ConfigureAwait(false); + return new TopicScope(adminClient, topicName, response.Value); + } + + public async ValueTask DisposeAsync() => + await _adminClient.DeleteQueueAsync(TopicName).ConfigureAwait(false); + } +} diff --git a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj index aa820e3aa..b0fbca0f1 100644 --- a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj +++ b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj @@ -23,6 +23,7 @@ + From 3667cc5f0ba61f67d332030f5d92bf4c9365bc54 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 10 Mar 2021 18:10:07 +1000 Subject: [PATCH 02/32] Move ConnectionString parsing into environment class --- .../AzureServiceBusDiagnosticListener.cs | 40 +++++++++++++++++-- .../AzureServiceBusDiagnosticsSubscriber.cs | 1 - .../AzureServiceBusDiagnosticListenerTests.cs | 7 +--- .../AzureServiceBusTestEnvironment.cs | 6 ++- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs index b206bab06..22fd41956 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs @@ -1,18 +1,24 @@ -using System; +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using Elastic.Apm.Api; using Elastic.Apm.DiagnosticSource; using Elastic.Apm.Logging; -using Elastic.Apm.Model; namespace Elastic.Apm.Azure.ServiceBus { + /// + /// Creates spans for diagnostic events from Azure.Messaging.ServiceBus + /// public class AzureServiceBusDiagnosticListener: IDiagnosticListener { private readonly IApmAgent _agent; - private readonly ConcurrentDictionary _sendSpans = new ConcurrentDictionary(); internal IApmLogger Logger { get; } @@ -23,7 +29,7 @@ public AzureServiceBusDiagnosticListener(IApmAgent agent) Logger = _agent.Logger.Scoped(nameof(AzureServiceBusDiagnosticListener)); } - public void OnCompleted() { } + public void OnCompleted() => Logger.Trace()?.Log("Completed"); public void OnError(Exception error) => Logger.Error()?.LogExceptionWithCaller(error, nameof(OnError)); @@ -46,6 +52,7 @@ public void OnNext(KeyValuePair kv) OnSendStop(); break; case "ServiceBusSender.Send.Exception": + OnSendException(kv); break; case "ServiceBusSender.Schedule.Start": break; @@ -106,7 +113,9 @@ private void OnSendStart(KeyValuePair kv) var spanName = queueName is null ? "AzureServiceBus SEND" : $"AzureServiceBus SEND to {queueName}"; + var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", "send"); + span.Context.Destination = new Destination { Address = destinationAddress, @@ -141,9 +150,32 @@ private void OnSendStop() return; } + span.Outcome = Outcome.Success; span.End(); } + private void OnSendException(KeyValuePair kv) + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_sendSpans.TryRemove(activity.Id, out var span)) + { + Logger.Error()? + .Log("Could not get span for activity {ActivityId} from tracked spans", activity.Id); + return; + } + + if (kv.Value is Exception e) + span.CaptureException(e); + + span.Outcome = Outcome.Failure; + span.End(); + } public string Name { get; } = "Azure.Messaging.ServiceBus"; } diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs index 921072217..d0850d08d 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs @@ -5,7 +5,6 @@ using System; using System.Diagnostics; -using Elastic.Apm.DiagnosticListeners; using Elastic.Apm.DiagnosticSource; namespace Elastic.Apm.Azure.ServiceBus diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs index b14a72b56..9150b1b66 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs @@ -54,9 +54,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; - var properties = ServiceBusConnectionStringProperties.Parse(_environment.ServiceBusConnectionString); - - destination.Address.Should().Be(properties.FullyQualifiedNamespace); + destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); destination.Service.Name.Should().Be("azureservicebus"); destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); destination.Service.Type.Should().Be("messaging"); @@ -85,8 +83,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; - var properties = ServiceBusConnectionStringProperties.Parse(_environment.ServiceBusConnectionString); - destination.Address.Should().Be(properties.FullyQualifiedNamespace); + destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); destination.Service.Name.Should().Be("azureservicebus"); destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); destination.Service.Type.Should().Be("messaging"); diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs index 5c9d7e952..d709e3e2b 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs @@ -4,6 +4,7 @@ // See the LICENSE file in the project root for more information using System; +using Azure.Messaging.ServiceBus; namespace Elastic.Apm.Azure.ServiceBus.Tests { @@ -19,8 +20,11 @@ public AzureServiceBusTestEnvironment() } ServiceBusConnectionString = serviceBusConnectionString; + ServiceBusConnectionStringProperties = ServiceBusConnectionStringProperties.Parse(serviceBusConnectionString); } - + public string ServiceBusConnectionString { get; } + + public ServiceBusConnectionStringProperties ServiceBusConnectionStringProperties { get; } } } From 60b340d8936966023596879f189289fff6116e2b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 11 Mar 2021 22:02:21 +1000 Subject: [PATCH 03/32] Source azure credentials from file or environment variables --- .gitignore | 9 +- .../test_resources.tf} | 0 .../AzureCredentials.cs | 105 +++++++++++ .../AzureServiceBusTestEnvironment.cs | 41 ++++- .../Elastic.Apm.Azure.ServiceBus.Tests.csproj | 2 + .../TerraformResourceException.cs | 27 +++ .../TerraformResources.cs | 165 ++++++++++++++++++ .../SolutionPaths.cs | 3 + 8 files changed, 342 insertions(+), 10 deletions(-) rename build/terraform/azure/{service_bus_resources.tf => service_bus/test_resources.tf} (100%) create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/AzureCredentials.cs create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResourceException.cs create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResources.cs diff --git a/.gitignore b/.gitignore index 00692b4d8..768915e11 100644 --- a/.gitignore +++ b/.gitignore @@ -340,4 +340,11 @@ html_docs build/output/ # Generated .NET core sln file -ElasticApmAgent.NetCore.sln \ No newline at end of file +ElasticApmAgent.NetCore.sln + +.credentials.json + +.terraform +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup \ No newline at end of file diff --git a/build/terraform/azure/service_bus_resources.tf b/build/terraform/azure/service_bus/test_resources.tf similarity index 100% rename from build/terraform/azure/service_bus_resources.tf rename to build/terraform/azure/service_bus/test_resources.tf diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureCredentials.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureCredentials.cs new file mode 100644 index 000000000..7cb157992 --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureCredentials.cs @@ -0,0 +1,105 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Elastic.Apm.Tests.Utilities; +using Newtonsoft.Json; + +namespace Elastic.Apm.Azure.ServiceBus.Tests +{ + public class AzureCredentials + { + // ReSharper disable InconsistentNaming + private const string ARM_CLIENT_ID = nameof(ARM_CLIENT_ID); + private const string ARM_CLIENT_SECRET = nameof(ARM_CLIENT_SECRET); + private const string ARM_TENANT_ID = nameof(ARM_TENANT_ID); + private const string ARM_SUBSCRIPTION_ID = nameof(ARM_SUBSCRIPTION_ID); + + private const string CredentialsJsonFile = ".credentials.json"; + // ReSharper restore InconsistentNaming + + private static readonly Lazy _lazyCredentials = + new Lazy(LoadCredentials, LazyThreadSafetyMode.ExecutionAndPublication); + + [JsonConstructor] + private AzureCredentials() { } + + private AzureCredentials(string clientId, string clientSecret, string tenantId, string subscriptionId) + { + ClientId = clientId; + ClientSecret = clientSecret; + TenantId = tenantId; + SubscriptionId = subscriptionId; + } + + private static AzureCredentials LoadCredentials() + { + var runningInCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_ID")); + if (runningInCi) + { + var clientId = Environment.GetEnvironmentVariable(ARM_CLIENT_ID); + var clientSecret = Environment.GetEnvironmentVariable(ARM_CLIENT_SECRET); + var tenantId = Environment.GetEnvironmentVariable(ARM_TENANT_ID); + var subscriptionId = Environment.GetEnvironmentVariable(ARM_SUBSCRIPTION_ID); + return new AzureCredentials(clientId, clientSecret, tenantId, subscriptionId); + } + + return LoadCredentialsFromFile(); + } + + private static AzureCredentials LoadCredentialsFromFile() + { + var path = Path.Combine(SolutionPaths.Root, CredentialsJsonFile); + + if (!File.Exists(path)) + throw new FileNotFoundException($"{CredentialsJsonFile} file does not exist at ${path}", CredentialsJsonFile); + + using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + using var streamReader = new StreamReader(fileStream); + using var jsonTextReader = new JsonTextReader(streamReader); + + var serializer = new JsonSerializer(); + return serializer.Deserialize(jsonTextReader); + } + + private string GetEnvironmentVariable(string name) + { + var value = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(value)) + throw new ArgumentException($"{name} environment variable is null or empty"); + + return value; + } + + /// + /// A set of Azure credentials obtained from environment variables or a .credentials.json configuration file + /// + public static AzureCredentials Instance => _lazyCredentials.Value; + + [JsonProperty] + public string ClientId { get; private set; } + + [JsonProperty] + public string ClientSecret { get; private set; } + + [JsonProperty] + public string TenantId { get; private set; } + + [JsonProperty] + public string SubscriptionId { get; private set; } + + public IDictionary ToTerraformEnvironmentVariables() => + new Dictionary + { + [ARM_CLIENT_ID] = ClientId, + [ARM_CLIENT_SECRET] = ClientSecret, + [ARM_SUBSCRIPTION_ID] = SubscriptionId, + [ARM_TENANT_ID] = TenantId, + }; + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs index d709e3e2b..ea93fcef4 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs @@ -4,27 +4,50 @@ // See the LICENSE file in the project root for more information using System; +using System.Collections.Generic; +using System.IO; using Azure.Messaging.ServiceBus; +using Elastic.Apm.Tests.Utilities; +using Xunit.Abstractions; namespace Elastic.Apm.Azure.ServiceBus.Tests { - public class AzureServiceBusTestEnvironment + /// + /// A test environment for Azure Service Bus that deploys and configures an Azure Service Bus namespace + /// in a given region and location + /// + public class AzureServiceBusTestEnvironment : IDisposable { - public AzureServiceBusTestEnvironment() + private readonly TerraformResources _terraform; + private readonly Dictionary _variables; + + public AzureServiceBusTestEnvironment(IMessageSink messageSink) { - var serviceBusConnectionString = Environment.GetEnvironmentVariable("AZURE_SERVICE_BUS_CONNECTION_STRING"); - if (string.IsNullOrEmpty(serviceBusConnectionString)) + var solutionRoot = SolutionPaths.Root; + var terraformResourceDirectory = Path.Combine(solutionRoot, "build", "terraform", "azure", "service_bus"); + var credentials = AzureCredentials.Instance; + + _terraform = new TerraformResources(terraformResourceDirectory, credentials, messageSink); + + // TODO: source resource group and location from somewhere + _variables = new Dictionary { - throw new ArgumentException( - "connection string for Azure Service Bus required. A connection string can be passed with AZURE_SERVICE_BUS_CONNECTION_STRING environment variable"); - } + ["location"] = "australiasoutheast", + ["resource_group"] = "russ-service-bus-test", + ["servicebus_namespace"] = "dotnet-" + Guid.NewGuid() + }; - ServiceBusConnectionString = serviceBusConnectionString; - ServiceBusConnectionStringProperties = ServiceBusConnectionStringProperties.Parse(serviceBusConnectionString); + _terraform.Init(); + _terraform.Apply(_variables); + + ServiceBusConnectionString = _terraform.Output("connection_string"); + ServiceBusConnectionStringProperties = ServiceBusConnectionStringProperties.Parse(ServiceBusConnectionString); } public string ServiceBusConnectionString { get; } public ServiceBusConnectionStringProperties ServiceBusConnectionStringProperties { get; } + + public void Dispose() => _terraform.Destroy(_variables); } } diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj index fa1fb1408..2619649e0 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj @@ -7,6 +7,8 @@ + + diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResourceException.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResourceException.cs new file mode 100644 index 000000000..da4ee653f --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResourceException.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Elastic.Apm.Azure.ServiceBus.Tests +{ + /// + /// An exception from interacting with terraform resources. + /// + public class TerraformResourceException : Exception + { + public TerraformResourceException(string message, int exitCode, List output) + : base(string.Join(Environment.NewLine, new [] { message, $"exit code: {exitCode}", "output:" }.Concat(output))) + { + } + + public TerraformResourceException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResources.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResources.cs new file mode 100644 index 000000000..7a0c44cb9 --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResources.cs @@ -0,0 +1,165 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.ExceptionServices; +using System.Text; +using Newtonsoft.Json.Linq; +using ProcNet; +using ProcNet.Std; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Elastic.Apm.Azure.ServiceBus.Tests +{ + /// + /// Interact with Terraform templates to apply and destroy resources + /// + public class TerraformResources + { + private readonly string _resourceDirectory; + private readonly IDictionary _environment; + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromMinutes(10); + private IMessageSink _messageSink; + + public TerraformResources(string resourceDirectory, AzureCredentials credentials, IMessageSink messageSink = null) + { + if (resourceDirectory is null) + throw new ArgumentNullException(nameof(resourceDirectory)); + + if (!Directory.Exists(resourceDirectory)) + throw new DirectoryNotFoundException($"Directory does not exist {resourceDirectory}"); + + _resourceDirectory = resourceDirectory; + _environment = credentials.ToTerraformEnvironmentVariables(); + _messageSink = messageSink; + } + + private ObservableProcess CreateProcess(params string[] arguments) + { + var startArguments = new StartArguments("terraform", arguments) + { + WorkingDirectory = _resourceDirectory, + Environment = _environment + }; + + return new ObservableProcess(startArguments); + } + + private void RunProcess(ObservableProcess process, Action onLine = null) + { + var capturedLines = new List(); + ExceptionDispatchInfo e = null; + + process.SubscribeLines(line => + { + capturedLines.Add(line.Line); + onLine?.Invoke(line); + }, + exception => e = ExceptionDispatchInfo.Capture(exception)); + + var completed = process.WaitForCompletion(_defaultTimeout); + + if (!completed) + { + process.Dispose(); + throw new TerraformResourceException( + $"terraform {_resourceDirectory} timed out after {_defaultTimeout}", -1, capturedLines); + } + + if (e != null) + { + throw new TerraformResourceException( + $"terraform {_resourceDirectory} did not succeed", e.SourceException); + } + + if (process.ExitCode != 0) + { + throw new TerraformResourceException( + $"terraform {_resourceDirectory} did not succeed", process.ExitCode.Value, capturedLines); + } + } + + public void Init() + { + using var process = CreateProcess("init", "-no-color"); + RunProcess(process, _messageSink is null ? null: line => _messageSink.OnMessage(new DiagnosticMessage(line.Line))); + } + + /// + /// Applies the terraform infrastructure with the supplied variables + /// + /// + public void Apply(IDictionary variables = null) + { + var args = new List + { + "apply", + "-auto-approve", + "-no-color", + "-input=false" + }; + + if (variables != null) + { + foreach (var variable in variables) + { + args.Add("-var"); + args.Add($"{variable.Key}={variable.Value}"); + } + } + + using var process = CreateProcess(args.ToArray()); + RunProcess(process, _messageSink is null ? null: line => _messageSink.OnMessage(new DiagnosticMessage(line.Line))); + } + + /// + /// Reads an output value from applied terraform managed infrastructure. + /// + /// The name of the output value to read + /// + public string Output(string name) + { + var output = new StringBuilder(); + using var process = CreateProcess($"output", "-raw", "-no-color", name); + RunProcess(process, line => + { + if (!line.Error) + output.Append(line.Line); + }); + + return output.ToString(); + } + + /// + /// Destroys the terraform managed infrastructure + /// + /// + public void Destroy(IDictionary variables = null) + { + var args = new List + { + "destroy", + "-auto-approve", + "-no-color", + "-input=false" + }; + + if (variables != null) + { + foreach (var variable in variables) + { + args.Add("-var"); + args.Add($"{variable.Key}={variable.Value}"); + } + } + + using var process = CreateProcess(args.ToArray()); + RunProcess(process); + } + } +} diff --git a/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs b/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs index 72fd84627..9bf7a7a94 100644 --- a/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs +++ b/test/Elastic.Apm.Tests.Utilities/SolutionPaths.cs @@ -27,6 +27,9 @@ private static string FindSolutionRoot() throw new InvalidOperationException($"Could not find solution root directory from the current directory `{currentDirectory}'"); } + /// + /// The full path to the solution root + /// public static string Root => _root.Value; } } From 3e74b481c679e1a2f8f32f56f8426874ae057253 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 15 Mar 2021 15:49:32 +1000 Subject: [PATCH 04/32] Integration tests and IgnoreMessageQueues configuration value --- ElasticApmAgent.sln | 4 +- .../azure/service_bus/test_resources.tf | 16 +- ...reMessagingServiceBusDiagnosticListener.cs | 269 +++++++++++++++++ ...ssagingServiceBusDiagnosticsSubscriber.cs} | 6 +- ...tic.Apm.Azure.Messaging.ServiceBus.csproj} | 1 + .../AzureServiceBusDiagnosticListener.cs | 182 ----------- .../CentralConfig/CentralConfigFetcher.cs | 2 + .../Config/AbstractConfigurationReader.cs | 28 +- ...tractConfigurationWithEnvFallbackReader.cs | 3 + src/Elastic.Apm/Config/ConfigConsts.cs | 4 + .../Config/ConfigSnapshotFromReader.cs | 1 + .../Config/EnvironmentConfigurationReader.cs | 2 + .../Config/IConfigurationReader.cs | 7 + src/Elastic.Apm/Elastic.Apm.csproj | 4 +- .../Azure}/AzureCredentials.cs | 12 +- .../Azure}/AzureServiceBusTestEnvironment.cs | 15 +- .../Azure}/QueueScope.cs | 2 +- .../Azure}/TopicScope.cs | 29 +- ...sagingServiceBusDiagnosticListenerTests.cs | 283 ++++++++++++++++++ ...m.Azure.Messaging.ServiceBus.Tests.csproj} | 3 +- .../Terraform}/TerraformResourceException.cs | 2 +- .../Terraform}/TerraformResources.cs | 4 +- .../AzureServiceBusDiagnosticListenerTests.cs | 96 ------ .../Elastic.Apm.Tests.Utilities.csproj | 2 +- .../MockConfigSnapshot.cs | 8 +- test/Elastic.Apm.Tests/ConstructorTests.cs | 1 + 26 files changed, 663 insertions(+), 323 deletions(-) create mode 100644 src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs rename src/{Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs => Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs} (75%) rename src/{Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj => Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj} (73%) delete mode 100644 src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure}/AzureCredentials.cs (86%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure}/AzureServiceBusTestEnvironment.cs (75%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure}/QueueScope.cs (92%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure}/TopicScope.cs (50%) create mode 100644 test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs rename test/{Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj => Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj} (82%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform}/TerraformResourceException.cs (89%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform}/TerraformResources.cs (93%) delete mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index 4f1391099..3f3341fa5 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -131,9 +131,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.Extensions.Logg EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Extensions.Logging.Tests", "test\Elastic.Apm.Extensions.Logging.Tests\Elastic.Apm.Extensions.Logging.Tests.csproj", "{B235B13F-42AE-42DA-A3C8-20D047F38685}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus", "src\Elastic.Apm.Azure.ServiceBus\Elastic.Apm.Azure.ServiceBus.csproj", "{1D43C8C5-4116-45C5-9F4B-56C1D926ED29}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.Messaging.ServiceBus", "src\Elastic.Apm.Azure.Messaging.ServiceBus\Elastic.Apm.Azure.Messaging.ServiceBus.csproj", "{1D43C8C5-4116-45C5-9F4B-56C1D926ED29}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus.Tests", "test\Elastic.Apm.Azure.ServiceBus.Tests\Elastic.Apm.Azure.ServiceBus.Tests.csproj", "{D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.Messaging.ServiceBus.Tests", "test\Elastic.Apm.Azure.Messaging.ServiceBus.Tests\Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj", "{D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/build/terraform/azure/service_bus/test_resources.tf b/build/terraform/azure/service_bus/test_resources.tf index daae06518..5ca5038d4 100644 --- a/build/terraform/azure/service_bus/test_resources.tf +++ b/build/terraform/azure/service_bus/test_resources.tf @@ -22,31 +22,25 @@ provider "azurerm" { data "azurerm_client_config" "current" { } +resource "random_uuid" "variables" { +} + variable "resource_group" { type = string description = "The name of the resource group to create" - - # TODO validation } variable "location" { type = string description = "The Azure location in which to deploy resources" - - # TODO validation + default = "westus" } variable "servicebus_namespace" { type = string description = "The name of the servicebus namespace to create" - -// validation { -// condition = can(regex("^[a-zA-Z][a-zA-Z0-9-]{5,49}$", var.servicebus_namespace)) && can(regex("[^\\-|\\-sb|\\-mgmt]$", var.servicebus_namespace)) -// error_message = "The value must be a valid service bus namespace. See https://docs.microsoft.com/en-us/rest/api/servicebus/create-namespace." -// } } - resource "azurerm_resource_group" "servicebus_resource_group" { name = var.resource_group location = var.location @@ -56,7 +50,7 @@ resource "azurerm_servicebus_namespace" "servicebus_namespace" { location = azurerm_resource_group.servicebus_resource_group.location name = var.servicebus_namespace resource_group_name = azurerm_resource_group.servicebus_resource_group.name - sku = "Basic" + sku = "Standard" depends_on = [azurerm_resource_group.servicebus_resource_group] } diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs new file mode 100644 index 000000000..4abc07527 --- /dev/null +++ b/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -0,0 +1,269 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.Helpers; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Azure.Messaging.ServiceBus +{ + /// + /// Creates spans for diagnostic events from Azure.Messaging.ServiceBus + /// + public class AzureMessagingServiceBusDiagnosticListener: IDiagnosticListener + { + private readonly IApmAgent _agent; + private readonly ApmAgent _realAgent; + private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); + + internal IApmLogger Logger { get; } + + public string Name { get; } = "Azure.Messaging.ServiceBus"; + + public AzureMessagingServiceBusDiagnosticListener(IApmAgent agent) + { + _agent = agent; + _realAgent = agent as ApmAgent; + Logger = _agent.Logger.Scoped(nameof(AzureMessagingServiceBusDiagnosticListener)); + } + + public void OnCompleted() => Logger.Trace()?.Log("Completed"); + + public void OnError(Exception error) => Logger.Error()?.LogExceptionWithCaller(error, nameof(OnError)); + + public void OnNext(KeyValuePair kv) + { + Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); + + if (string.IsNullOrEmpty(kv.Key)) + { + Logger.Trace()?.Log($"Key is {(kv.Key == null ? "null" : "an empty string")} - exiting"); + return; + } + + switch (kv.Key) + { + case "ServiceBusSender.Send.Start": + OnSendStart(kv, "SEND"); + break; + case "ServiceBusSender.Send.Stop": + OnStop(); + break; + case "ServiceBusSender.Send.Exception": + OnException(kv); + break; + case "ServiceBusSender.Schedule.Start": + OnSendStart(kv, "SCHEDULE"); + break; + case "ServiceBusSender.Schedule.Stop": + OnStop(); + break; + case "ServiceBusSender.Schedule.Exception": + OnException(kv); + break; + case "ServiceBusReceiver.Receive.Start": + OnReceiveStart(kv, "RECEIVE"); + break; + case "ServiceBusReceiver.Receive.Stop": + OnStop(); + break; + case "ServiceBusReceiver.Receive.Exception": + OnException(kv); + break; + case "ServiceBusReceiver.ReceiveDeferred.Start": + OnReceiveStart(kv, "RECEIVEDEFERRED"); + break; + case "ServiceBusReceiver.ReceiveDeferred.Stop": + OnStop(); + break; + case "ServiceBusReceiver.ReceiveDeferred.Exception": + OnException(kv); + break; + default: + Logger.Trace()?.Log("`{DiagnosticEventKey}' key is not a traced diagnostic event", kv.Key); + break; + } + } + + private void OnReceiveStart(KeyValuePair kv, string action) + { + if (!(kv.Value is Activity activity)) + { + Logger.Trace()?.Log("Value is not an activity - exiting"); + return; + } + + string queueName = null; + foreach (var tag in activity.Tags) + { + switch (tag.Key) + { + case "message_bus.destination": + queueName = tag.Value; + break; + default: + continue; + } + } + + if (MatchesIgnoreMessageQueues(queueName)) + return; + + var transactionName = queueName is null + ? $"AzureServiceBus {action}" + : $"AzureServiceBus {action} from {queueName}"; + + DistributedTracingData tracingData = null; + + var transaction = _agent.Tracer.StartTransaction(transactionName, "messaging", tracingData); + + // transaction creation will create an activity, so use this as the key. + // TODO: change when existing activity is used. + var activityId = Activity.Current.Id; + + transaction.Context.Service = Service.GetDefaultService(_agent.ConfigurationReader, _agent.Logger); + transaction.Context.Service.Framework = new Framework { Name = "AzureServiceBus" }; + + if (!_processingSegments.TryAdd(activityId, transaction)) + { + Logger.Error()?.Log( + "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", + action, + transaction.Id, + activity.Id); + } + } + + private bool MatchesIgnoreMessageQueues(string name) + { + if (name != null && _realAgent != null) + { + var matcher = WildcardMatcher.AnyMatch(_realAgent.ConfigStore.CurrentSnapshot.IgnoreMessageQueues, name); + if (matcher != null) + { + Logger.Debug()?.Log( + "Not tracing message from {QueueName} because it matched IgnoreMessageQueues pattern {Matcher}", + name, + matcher.GetMatcher()); + return true; + } + } + + return false; + } + + private void OnSendStart(KeyValuePair kv, string action) + { + var currentSegment = _agent.GetCurrentExecutionSegment(); + if (currentSegment is null) + { + Logger.Trace()?.Log("No current transaction or span - exiting"); + return; + } + + if (!(kv.Value is Activity activity)) + { + Logger.Trace()?.Log("Value is not an activity - exiting"); + return; + } + + string queueName = null; + string destinationAddress = null; + foreach (var tag in activity.Tags) + { + switch (tag.Key) + { + case "message_bus.destination": + queueName = tag.Value; + break; + case "peer.address": + destinationAddress = tag.Value; + break; + default: + continue; + } + } + + if (MatchesIgnoreMessageQueues(queueName)) + return; + + var spanName = queueName is null + ? $"AzureServiceBus {action}" + : $"AzureServiceBus {action} to {queueName}"; + + var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", action.ToLowerInvariant()); + + span.Context.Destination = new Destination + { + Address = destinationAddress, + Service = new Destination.DestinationService + { + Name = "azureservicebus", + Resource = queueName is null ? "azureservicebus" : $"azureservicebus/{queueName}", + Type = "messaging" + } + }; + + if (!_processingSegments.TryAdd(activity.Id, span)) + { + Logger.Error()?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + action, + span.Id, + activity.Id); + } + } + + private void OnStop() + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + return; + + // TODO: Get from current activity when current activity is reused when starting transaction. + var parent = activity.Parent; + if (parent?.Links != null) + { + foreach (var link in parent.Links) + { + // Do something with links + } + } + + segment.Outcome = Outcome.Success; + segment.End(); + } + + private void OnException(KeyValuePair kv) + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + return; + + if (kv.Value is Exception e) + segment.CaptureException(e); + + segment.Outcome = Outcome.Failure; + segment.End(); + } + } +} diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs similarity index 75% rename from src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs rename to src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs index d0850d08d..9b9c80600 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticsSubscriber.cs +++ b/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs @@ -7,9 +7,9 @@ using System.Diagnostics; using Elastic.Apm.DiagnosticSource; -namespace Elastic.Apm.Azure.ServiceBus +namespace Elastic.Apm.Azure.Messaging.ServiceBus { - public class AzureServiceBusDiagnosticsSubscriber : IDiagnosticsSubscriber + public class AzureMessagingServiceBusDiagnosticsSubscriber : IDiagnosticsSubscriber { /// /// Start listening for HttpClient diagnostic source events. @@ -18,7 +18,7 @@ public IDisposable Subscribe(IApmAgent agent) { var retVal = new CompositeDisposable(); - var initializer = new DiagnosticInitializer(agent.Logger, new[] { new AzureServiceBusDiagnosticListener(agent) }); + var initializer = new DiagnosticInitializer(agent.Logger, new[] { new AzureMessagingServiceBusDiagnosticListener(agent) }); retVal.Add(initializer); retVal.Add(DiagnosticListener diff --git a/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj b/src/Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj similarity index 73% rename from src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj rename to src/Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj index 3dcd8ec87..fc90103e1 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj +++ b/src/Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj @@ -2,6 +2,7 @@ netstandard2.0 + Elastic.Apm.Azure.Messaging.ServiceBus diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs deleted file mode 100644 index 22fd41956..000000000 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureServiceBusDiagnosticListener.cs +++ /dev/null @@ -1,182 +0,0 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using Elastic.Apm.Api; -using Elastic.Apm.DiagnosticSource; -using Elastic.Apm.Logging; - -namespace Elastic.Apm.Azure.ServiceBus -{ - /// - /// Creates spans for diagnostic events from Azure.Messaging.ServiceBus - /// - public class AzureServiceBusDiagnosticListener: IDiagnosticListener - { - private readonly IApmAgent _agent; - private readonly ConcurrentDictionary _sendSpans = new ConcurrentDictionary(); - - internal IApmLogger Logger { get; } - - public AzureServiceBusDiagnosticListener(IApmAgent agent) - { - _agent = agent; - Logger = _agent.Logger.Scoped(nameof(AzureServiceBusDiagnosticListener)); - } - - public void OnCompleted() => Logger.Trace()?.Log("Completed"); - - public void OnError(Exception error) => Logger.Error()?.LogExceptionWithCaller(error, nameof(OnError)); - - public void OnNext(KeyValuePair kv) - { - Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); - - if (string.IsNullOrEmpty(kv.Key)) - { - Logger.Trace()?.Log($"Key is {(kv.Key == null ? "null" : "an empty string")} - exiting"); - return; - } - - switch (kv.Key) - { - case "ServiceBusSender.Send.Start": - OnSendStart(kv); - break; - case "ServiceBusSender.Send.Stop": - OnSendStop(); - break; - case "ServiceBusSender.Send.Exception": - OnSendException(kv); - break; - case "ServiceBusSender.Schedule.Start": - break; - case "ServiceBusSender.Schedule.Stop": - break; - case "ServiceBusSender.Schedule.Exception": - break; - case "ServiceBusReceiver.Receive.Start": - break; - case "ServiceBusReceiver.Receive.Stop": - break; - case "ServiceBusReceiver.Receive.Exception": - break; - case "ServiceBusReceiver.ReceiveDeferred.Start": - break; - case "ServiceBusReceiver.ReceiveDeferred.Stop": - break; - case "ServiceBusReceiver.ReceiveDeferred.Exception": - break; - default: - Logger.Trace()?.Log("Unrecognized key `{DiagnosticEventKey}'", kv.Key); - break; - } - } - - private void OnSendStart(KeyValuePair kv) - { - var currentSegment = _agent.GetCurrentExecutionSegment(); - if (currentSegment is null) - { - Logger.Trace()?.Log("No current transaction or span - exiting"); - return; - } - - if (!(kv.Value is Activity activity)) - { - Logger.Trace()?.Log("Value is not an activity - exiting"); - return; - } - - string queueName = null; - string destinationAddress = null; - foreach (var tag in activity.Tags) - { - switch (tag.Key) - { - case "message_bus.destination": - queueName = tag.Value; - break; - case "peer.address": - destinationAddress = tag.Value; - break; - default: - continue; - } - } - - var spanName = queueName is null - ? "AzureServiceBus SEND" - : $"AzureServiceBus SEND to {queueName}"; - - var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", "send"); - - span.Context.Destination = new Destination - { - Address = destinationAddress, - Service = new Destination.DestinationService - { - Name = "azureservicebus", - Resource = queueName is null ? "azureservicebus" : $"azureservicebus/{queueName}", - Type = "messaging" - } - }; - - if (!_sendSpans.TryAdd(activity.Id, span)) - { - Logger.Error()? - .Log("Could not add send span {SpanId} for activity {ActivityId} to tracked spans", span.Id, activity.Id); - } - } - - private void OnSendStop() - { - var activity = Activity.Current; - if (activity is null) - { - Logger.Trace()?.Log("Current activity is null - exiting"); - return; - } - - if (!_sendSpans.TryRemove(activity.Id, out var span)) - { - Logger.Error()? - .Log("Could not get span for activity {ActivityId} from tracked spans", activity.Id); - return; - } - - span.Outcome = Outcome.Success; - span.End(); - } - - private void OnSendException(KeyValuePair kv) - { - var activity = Activity.Current; - if (activity is null) - { - Logger.Trace()?.Log("Current activity is null - exiting"); - return; - } - - if (!_sendSpans.TryRemove(activity.Id, out var span)) - { - Logger.Error()? - .Log("Could not get span for activity {ActivityId} from tracked spans", activity.Id); - return; - } - - if (kv.Value is Exception e) - span.CaptureException(e); - - span.Outcome = Outcome.Failure; - span.End(); - } - - public string Name { get; } = "Azure.Messaging.ServiceBus"; - } -} diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs index 438088cc7..a1414805f 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs @@ -266,6 +266,8 @@ internal WrappingConfigSnapshot(IConfigSnapshot wrapped, CentralConfigReader cen public string HostName => _wrapped.HostName; + public IReadOnlyList IgnoreMessageQueues => _wrapped.IgnoreMessageQueues; + public LogLevel LogLevel => _centralConfig.LogLevel ?? _wrapped.LogLevel; public int MaxBatchEventCount => _wrapped.MaxBatchEventCount; diff --git a/src/Elastic.Apm/Config/AbstractConfigurationReader.cs b/src/Elastic.Apm/Config/AbstractConfigurationReader.cs index be8388a4d..eec63c988 100644 --- a/src/Elastic.Apm/Config/AbstractConfigurationReader.cs +++ b/src/Elastic.Apm/Config/AbstractConfigurationReader.cs @@ -27,6 +27,9 @@ public abstract class AbstractConfigurationReader private readonly LazyContextualInit> _cachedWildcardMatchersDisableMetrics = new LazyContextualInit>(); + private readonly LazyContextualInit> _cachedWildcardMatchersIgnoreMessageQueues = + new LazyContextualInit>(); + private readonly LazyContextualInit> _cachedWildcardMatchersSanitizeFieldNames = new LazyContextualInit>(); @@ -108,7 +111,6 @@ protected IReadOnlyList ParseDisableMetrics(ConfigurationKeyVal _cachedWildcardMatchersDisableMetrics.IfNotInited?.InitOrGet(() => ParseDisableMetricsImpl(kv)) ?? _cachedWildcardMatchersDisableMetrics.Value; - private IReadOnlyList ParseDisableMetricsImpl(ConfigurationKeyValue kv) { if (kv?.Value == null) return DefaultValues.DisableMetrics; @@ -129,6 +131,30 @@ private IReadOnlyList ParseDisableMetricsImpl(ConfigurationKeyV } } + protected IReadOnlyList ParseIgnoreMessageQueues(ConfigurationKeyValue kv) => + _cachedWildcardMatchersIgnoreMessageQueues.IfNotInited?.InitOrGet(() => ParseIgnoreMessageQueuesImpl(kv)) + ?? _cachedWildcardMatchersIgnoreMessageQueues.Value; + + private IReadOnlyList ParseIgnoreMessageQueuesImpl(ConfigurationKeyValue kv) + { + if (kv?.Value == null) return DefaultValues.IgnoreMessageQueues; + + try + { + _logger?.Trace()?.Log("Try parsing IgnoreMessageQueues, values: {IgnoreMessageQueues}", kv.Value); + var ignoreMessageQueues = kv.Value.Split(',').Where(n => !string.IsNullOrEmpty(n)).ToList(); + + var retVal = new List(ignoreMessageQueues.Count); + foreach (var item in ignoreMessageQueues) retVal.Add(WildcardMatcher.ValueOf(item.Trim())); + return retVal; + } + catch (Exception e) + { + _logger?.Error()?.LogException(e, "Failed parsing IgnoreMessageQueues, values in the config: {IgnoreMessageQueues}", kv.Value); + return DefaultValues.IgnoreMessageQueues; + } + } + protected string ParseSecretToken(ConfigurationKeyValue kv) { if (kv == null || string.IsNullOrEmpty(kv.Value)) return null; diff --git a/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs b/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs index e16ac94df..f61df82b8 100644 --- a/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs +++ b/src/Elastic.Apm/Config/AbstractConfigurationWithEnvFallbackReader.cs @@ -69,6 +69,9 @@ internal AbstractConfigurationWithEnvFallbackReader(IApmLogger logger, string de public virtual string HostName => ParseHostName(Read(KeyNames.HostName, EnvVarNames.HostName)); + public IReadOnlyList IgnoreMessageQueues => + ParseIgnoreMessageQueues(Read(KeyNames.IgnoreMessageQueues, EnvVarNames.IgnoreMessageQueues)); + public virtual LogLevel LogLevel => ParseLogLevel(Read(KeyNames.LogLevel, EnvVarNames.LogLevel)); public virtual int MaxBatchEventCount => diff --git a/src/Elastic.Apm/Config/ConfigConsts.cs b/src/Elastic.Apm/Config/ConfigConsts.cs index 3d1c78f98..438a18a9a 100644 --- a/src/Elastic.Apm/Config/ConfigConsts.cs +++ b/src/Elastic.Apm/Config/ConfigConsts.cs @@ -55,6 +55,8 @@ public static class DefaultValues public static List DisableMetrics = new List(); + public static List IgnoreMessageQueues = new List(); + public static List SanitizeFieldNames; public static List TransactionIgnoreUrls; @@ -122,6 +124,7 @@ public static class EnvVarNames public const string FullFrameworkConfigurationReaderType = Prefix + "FULL_FRAMEWORK_CONFIGURATION_READER_TYPE"; public const string GlobalLabels = Prefix + "GLOBAL_LABELS"; public const string HostName = Prefix + "HOSTNAME"; + public const string IgnoreMessageQueues = Prefix + "IGNORE_MESSAGE_QUEUES"; public const string LogLevel = Prefix + "LOG_LEVEL"; public const string MaxBatchEventCount = Prefix + "MAX_BATCH_EVENT_COUNT"; public const string MaxQueueEventCount = Prefix + "MAX_QUEUE_EVENT_COUNT"; @@ -163,6 +166,7 @@ public static class KeyNames public const string FullFrameworkConfigurationReaderType = Prefix + nameof(FullFrameworkConfigurationReaderType); public const string GlobalLabels = Prefix + nameof(GlobalLabels); public const string HostName = Prefix + nameof(HostName); + public const string IgnoreMessageQueues = Prefix + nameof(IgnoreMessageQueues); public const string LogLevel = Prefix + nameof(LogLevel); public const string MaxBatchEventCount = Prefix + nameof(MaxBatchEventCount); public const string MaxQueueEventCount = Prefix + nameof(MaxQueueEventCount); diff --git a/src/Elastic.Apm/Config/ConfigSnapshotFromReader.cs b/src/Elastic.Apm/Config/ConfigSnapshotFromReader.cs index e154acc5d..acc759155 100644 --- a/src/Elastic.Apm/Config/ConfigSnapshotFromReader.cs +++ b/src/Elastic.Apm/Config/ConfigSnapshotFromReader.cs @@ -35,6 +35,7 @@ internal ConfigSnapshotFromReader(IConfigurationReader content, string dbgDescri public TimeSpan FlushInterval => _content.FlushInterval; public IReadOnlyDictionary GlobalLabels => _content.GlobalLabels; public string HostName => _content.HostName; + public IReadOnlyList IgnoreMessageQueues => _content.IgnoreMessageQueues; public LogLevel LogLevel => _content.LogLevel; public int MaxBatchEventCount => _content.MaxBatchEventCount; public int MaxQueueEventCount => _content.MaxQueueEventCount; diff --git a/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs b/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs index 2bd6cc5a8..7cbf3a6e0 100644 --- a/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs +++ b/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs @@ -55,6 +55,8 @@ public EnvironmentConfigurationReader(IApmLogger logger = null) : base(logger, T public string HostName => ParseHostName(Read(ConfigConsts.EnvVarNames.HostName)); + public IReadOnlyList IgnoreMessageQueues => ParseIgnoreMessageQueues(Read(ConfigConsts.EnvVarNames.IgnoreMessageQueues)); + public LogLevel LogLevel => ParseLogLevel(Read(ConfigConsts.EnvVarNames.LogLevel)); public int MaxBatchEventCount => ParseMaxBatchEventCount(Read(ConfigConsts.EnvVarNames.MaxBatchEventCount)); diff --git a/src/Elastic.Apm/Config/IConfigurationReader.cs b/src/Elastic.Apm/Config/IConfigurationReader.cs index cc05f654d..3bc4751f4 100644 --- a/src/Elastic.Apm/Config/IConfigurationReader.cs +++ b/src/Elastic.Apm/Config/IConfigurationReader.cs @@ -120,6 +120,13 @@ public interface IConfigurationReader /// string HostName { get; } + /// + /// Disables the tracing of messages from certain queues, topics exchanges. + /// If the name of a queue, topic or exchange matches any of the wildcard expressions, it will + /// not be traced + /// + IReadOnlyList IgnoreMessageQueues { get; } + /// /// The logging level for the agent. /// diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index cf704d881..ca3eb22ca 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -44,8 +44,8 @@ - - + + diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureCredentials.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureCredentials.cs similarity index 86% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/AzureCredentials.cs rename to test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureCredentials.cs index 7cb157992..d32bb8a7e 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureCredentials.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureCredentials.cs @@ -10,7 +10,7 @@ using Elastic.Apm.Tests.Utilities; using Newtonsoft.Json; -namespace Elastic.Apm.Azure.ServiceBus.Tests +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure { public class AzureCredentials { @@ -42,10 +42,10 @@ private static AzureCredentials LoadCredentials() var runningInCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_ID")); if (runningInCi) { - var clientId = Environment.GetEnvironmentVariable(ARM_CLIENT_ID); - var clientSecret = Environment.GetEnvironmentVariable(ARM_CLIENT_SECRET); - var tenantId = Environment.GetEnvironmentVariable(ARM_TENANT_ID); - var subscriptionId = Environment.GetEnvironmentVariable(ARM_SUBSCRIPTION_ID); + var clientId = GetEnvironmentVariable(ARM_CLIENT_ID); + var clientSecret = GetEnvironmentVariable(ARM_CLIENT_SECRET); + var tenantId = GetEnvironmentVariable(ARM_TENANT_ID); + var subscriptionId = GetEnvironmentVariable(ARM_SUBSCRIPTION_ID); return new AzureCredentials(clientId, clientSecret, tenantId, subscriptionId); } @@ -67,7 +67,7 @@ private static AzureCredentials LoadCredentialsFromFile() return serializer.Deserialize(jsonTextReader); } - private string GetEnvironmentVariable(string name) + private static string GetEnvironmentVariable(string name) { var value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value)) diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs similarity index 75% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs rename to test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs index ea93fcef4..f1049eb53 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs @@ -7,15 +7,20 @@ using System.Collections.Generic; using System.IO; using Azure.Messaging.ServiceBus; +using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Terraform; using Elastic.Apm.Tests.Utilities; using Xunit.Abstractions; -namespace Elastic.Apm.Azure.ServiceBus.Tests +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure { /// /// A test environment for Azure Service Bus that deploys and configures an Azure Service Bus namespace /// in a given region and location /// + /// + /// Resource name rules + /// https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules + /// public class AzureServiceBusTestEnvironment : IDisposable { private readonly TerraformResources _terraform; @@ -29,11 +34,13 @@ public AzureServiceBusTestEnvironment(IMessageSink messageSink) _terraform = new TerraformResources(terraformResourceDirectory, credentials, messageSink); - // TODO: source resource group and location from somewhere + var machineName = Environment.MachineName.ToLowerInvariant(); + if (machineName.Length > 66) + machineName = machineName.Substring(0, 66); + _variables = new Dictionary { - ["location"] = "australiasoutheast", - ["resource_group"] = "russ-service-bus-test", + ["resource_group"] = $"dotnet-{machineName}-service-bus-test", ["servicebus_namespace"] = "dotnet-" + Guid.NewGuid() }; diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/QueueScope.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/QueueScope.cs similarity index 92% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/QueueScope.cs rename to test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/QueueScope.cs index e897aac14..44879e113 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/QueueScope.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/QueueScope.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Azure.Messaging.ServiceBus.Administration; -namespace Elastic.Apm.Azure.ServiceBus.Tests +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure { public class QueueScope : IAsyncDisposable { diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/TopicScope.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/TopicScope.cs similarity index 50% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/TopicScope.cs rename to test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/TopicScope.cs index 822cf1ee6..a54b151ef 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/TopicScope.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/TopicScope.cs @@ -7,29 +7,40 @@ using System.Threading.Tasks; using Azure.Messaging.ServiceBus.Administration; -namespace Elastic.Apm.Azure.ServiceBus.Tests +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure { public class TopicScope : IAsyncDisposable { - public string TopicName { get; } - private readonly TopicProperties _properties; private readonly ServiceBusAdministrationClient _adminClient; + public string TopicName { get; } + public string SubscriptionName { get; } - private TopicScope(ServiceBusAdministrationClient adminClient, string queueName, TopicProperties properties) + private TopicScope(ServiceBusAdministrationClient adminClient, string topicName, string subscriptionName) { _adminClient = adminClient; - TopicName = queueName; - _properties = properties; + TopicName = topicName; + SubscriptionName = subscriptionName; } - + public static async Task CreateWithTopic(ServiceBusAdministrationClient adminClient) { var topicName = Guid.NewGuid().ToString("D"); var response = await adminClient.CreateTopicAsync(topicName).ConfigureAwait(false); - return new TopicScope(adminClient, topicName, response.Value); + return new TopicScope(adminClient, topicName, null); + } + + public static async Task CreateWithTopicAndSubscription(ServiceBusAdministrationClient adminClient) + { + var topicName = Guid.NewGuid().ToString("D"); + var subscriptionName = Guid.NewGuid().ToString("D"); + var topicResponse = await adminClient.CreateTopicAsync(topicName).ConfigureAwait(false); + var subscriptionResponse = + await adminClient.CreateSubscriptionAsync(topicName, subscriptionName).ConfigureAwait(false); + return new TopicScope(adminClient, topicName, subscriptionName); } - public async ValueTask DisposeAsync() => + + public async ValueTask DisposeAsync() => await _adminClient.DeleteQueueAsync(TopicName).ConfigureAwait(false); } } diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs new file mode 100644 index 000000000..5b1cb3ed4 --- /dev/null +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -0,0 +1,283 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; +using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.XUnit; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests +{ + public class AzureMessagingServiceBusDiagnosticListenerTests : IClassFixture, IDisposable, IAsyncDisposable + { + private readonly AzureServiceBusTestEnvironment _environment; + private readonly ApmAgent _agent; + private readonly MockPayloadSender _sender; + private readonly ServiceBusClient _client; + private readonly ServiceBusAdministrationClient _adminClient; + + public AzureMessagingServiceBusDiagnosticListenerTests(AzureServiceBusTestEnvironment environment, ITestOutputHelper output) + { + _environment = environment; + + var logger = new XUnitLogger(LogLevel.Trace, output); + _sender = new MockPayloadSender(logger); + _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); + _agent.Subscribe(new AzureMessagingServiceBusDiagnosticsSubscriber()); + + _adminClient = new ServiceBusAdministrationClient(environment.ServiceBusConnectionString); + _client = new ServiceBusClient(environment.ServiceBusConnectionString); + } + + [Fact] + public async Task Capture_Span_When_Send_To_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = _client.CreateSender(scope.QueueName); + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendMessageAsync(new ServiceBusMessage("test message")).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SEND to {scope.QueueName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("send"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Span_When_Send_To_Topic() + { + await using var scope = await TopicScope.CreateWithTopic(_adminClient); + var sender = _client.CreateSender(scope.TopicName); + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendMessageAsync(new ServiceBusMessage("test message")).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SEND to {scope.TopicName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("send"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Span_When_Schedule_To_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = _client.CreateSender(scope.QueueName); + await _agent.Tracer.CaptureTransaction("Schedule AzureServiceBus Message", "message", async () => + { + await sender.ScheduleMessageAsync( + new ServiceBusMessage("test message"), + DateTimeOffset.Now.AddSeconds(10)).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.QueueName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("schedule"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Span_When_Schedule_To_Topic() + { + await using var scope = await TopicScope.CreateWithTopic(_adminClient); + var sender = _client.CreateSender(scope.TopicName); + await _agent.Tracer.CaptureTransaction("Schedule AzureServiceBus Message", "message", async () => + { + await sender.ScheduleMessageAsync( + new ServiceBusMessage("test message"), + DateTimeOffset.Now.AddSeconds(10)).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.TopicName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("schedule"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_Receive_From_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = _client.CreateSender(scope.QueueName); + var receiver = _client.CreateReceiver(scope.QueueName); + + await sender.SendMessageAsync( + new ServiceBusMessage("test message")).ConfigureAwait(false); + + await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2))) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(1); + var transaction = _sender.FirstTransaction; + + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_Receive_From_Topic_Subscription() + { + await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); + + var sender = _client.CreateSender(scope.TopicName); + var receiver = _client.CreateReceiver(scope.TopicName, scope.SubscriptionName); + + await sender.SendMessageAsync( + new ServiceBusMessage("test message")).ConfigureAwait(false); + + await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2))) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(1); + var transaction = _sender.FirstTransaction; + + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_ReceiveDeferred_From_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = _client.CreateSender(scope.QueueName); + var receiver = _client.CreateReceiver(scope.QueueName); + + await sender.SendMessageAsync( + new ServiceBusMessage("test message")).ConfigureAwait(false); + + + var message = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + await receiver.DeferMessageAsync(message).ConfigureAwait(false); + + + await receiver.ReceiveDeferredMessageAsync(message.SequenceNumber).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2), count: 2)) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(2); + + var transaction = _sender.FirstTransaction; + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be("messaging"); + + var secondTransaction = _sender.Transactions[1]; + secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.QueueName}"); + secondTransaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_ReceiveDeferred_From_Topic_Subscription() + { + await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); + + var sender = _client.CreateSender(scope.TopicName); + var receiver = _client.CreateReceiver(scope.TopicName, scope.SubscriptionName); + + await sender.SendMessageAsync( + new ServiceBusMessage("test message")).ConfigureAwait(false); + + var message = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + await receiver.DeferMessageAsync(message).ConfigureAwait(false); + + await receiver.ReceiveDeferredMessageAsync(message.SequenceNumber).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2), count: 2)) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(2); + + var transaction = _sender.FirstTransaction; + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be("messaging"); + + var secondTransaction = _sender.Transactions[1]; + secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + secondTransaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Does_Not_Capture_Span_When_QueueName_Matches_IgnoreMessageQueues() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = _client.CreateSender(scope.QueueName); + _agent.ConfigStore.CurrentSnapshot = new MockConfigSnapshot(ignoreMessageQueues: scope.QueueName); + + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendMessageAsync(new ServiceBusMessage("test message")).ConfigureAwait(false); + }); + + _sender.SignalEndSpans(); + _sender.WaitForSpans(); + _sender.Spans.Should().HaveCount(0); + } + + public void Dispose() => _agent.Dispose(); + + public ValueTask DisposeAsync() => _client.DisposeAsync(); + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj similarity index 82% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj rename to test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj index 2619649e0..b59942ea2 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj @@ -3,6 +3,7 @@ net5.0 false + Elastic.Apm.Azure.Messaging.ServiceBus.Tests @@ -20,7 +21,7 @@ - + diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResourceException.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResourceException.cs similarity index 89% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResourceException.cs rename to test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResourceException.cs index da4ee653f..4917102c1 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResourceException.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResourceException.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Linq; -namespace Elastic.Apm.Azure.ServiceBus.Tests +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Terraform { /// /// An exception from interacting with terraform resources. diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResources.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResources.cs similarity index 93% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResources.cs rename to test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResources.cs index 7a0c44cb9..0a24cd6ee 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/TerraformResources.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResources.cs @@ -8,13 +8,13 @@ using System.IO; using System.Runtime.ExceptionServices; using System.Text; -using Newtonsoft.Json.Linq; +using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure; using ProcNet; using ProcNet.Std; using Xunit.Abstractions; using Xunit.Sdk; -namespace Elastic.Apm.Azure.ServiceBus.Tests +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Terraform { /// /// Interact with Terraform templates to apply and destroy resources diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs deleted file mode 100644 index 9150b1b66..000000000 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureServiceBusDiagnosticListenerTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Threading.Tasks; -using Azure.Messaging.ServiceBus; -using Azure.Messaging.ServiceBus.Administration; -using Elastic.Apm.Tests.Utilities; -using FluentAssertions; -using Xunit; - -namespace Elastic.Apm.Azure.ServiceBus.Tests -{ - // Resource name rules - // https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules - public class AzureServiceBusDiagnosticListenerTests : IClassFixture, IDisposable, IAsyncDisposable - { - private readonly AzureServiceBusTestEnvironment _environment; - private readonly ApmAgent _agent; - private readonly MockPayloadSender _sender; - private readonly ServiceBusClient _client; - private readonly ServiceBusAdministrationClient _adminClient; - - public AzureServiceBusDiagnosticListenerTests(AzureServiceBusTestEnvironment environment) - { - _environment = environment; - - var logger = new NoopLogger(); - _sender = new MockPayloadSender(logger); - _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); - _agent.Subscribe(new AzureServiceBusDiagnosticsSubscriber()); - - _adminClient = new ServiceBusAdministrationClient(environment.ServiceBusConnectionString); - _client = new ServiceBusClient(environment.ServiceBusConnectionString); - } - - [Fact] - public async Task Capture_Span_When_Send_To_Queue() - { - await using var scope = await QueueScope.CreateWithQueue(_adminClient); - var sender = _client.CreateSender(scope.QueueName); - await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => - { - await sender.SendMessageAsync(new ServiceBusMessage("test message")).ConfigureAwait(false); - }); - - if (!_sender.WaitForSpans(TimeSpan.FromMinutes(2))) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"AzureServiceBus SEND to {scope.QueueName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); - span.Action.Should().Be("send"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); - destination.Service.Type.Should().Be("messaging"); - } - - [Fact] - public async Task Capture_Span_When_Send_To_Topic() - { - await using var scope = await TopicScope.CreateWithTopic(_adminClient); - var sender = _client.CreateSender(scope.TopicName); - await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => - { - await sender.SendMessageAsync(new ServiceBusMessage("test message")).ConfigureAwait(false); - }); - - if (!_sender.WaitForSpans(TimeSpan.FromMinutes(2))) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"AzureServiceBus SEND to {scope.TopicName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); - span.Action.Should().Be("send"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); - destination.Service.Type.Should().Be("messaging"); - } - - public void Dispose() => _agent.Dispose(); - - public ValueTask DisposeAsync() => _client.DisposeAsync(); - } -} diff --git a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj index b0fbca0f1..859acea5e 100644 --- a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj +++ b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj @@ -23,7 +23,7 @@ - + diff --git a/test/Elastic.Apm.Tests.Utilities/MockConfigSnapshot.cs b/test/Elastic.Apm.Tests.Utilities/MockConfigSnapshot.cs index c5d067e90..1539d2978 100644 --- a/test/Elastic.Apm.Tests.Utilities/MockConfigSnapshot.cs +++ b/test/Elastic.Apm.Tests.Utilities/MockConfigSnapshot.cs @@ -30,6 +30,7 @@ public class MockConfigSnapshot : AbstractConfigurationReader, IConfigSnapshot private readonly string _flushInterval; private readonly string _globalLabels; private readonly string _hostName; + private readonly string _ignoreMessageQueues; private readonly string _logLevel; private readonly string _maxBatchEventCount; private readonly string _maxQueueEventCount; @@ -87,7 +88,8 @@ public MockConfigSnapshot(IApmLogger logger = null, string enabled = null, string recording = null, string serverUrl = null, - string serverCert = null + string serverCert = null, + string ignoreMessageQueues = null ) : base(logger, ThisClassName) { _serverUrls = serverUrls; @@ -125,6 +127,7 @@ public MockConfigSnapshot(IApmLogger logger = null, _recording = recording; _serverUrl = serverUrl; _serverCert = serverCert; + _ignoreMessageQueues = ignoreMessageQueues; } public string ApiKey => ParseApiKey(Kv(EnvVarNames.ApiKey, _apiKey, Origin)); @@ -160,6 +163,9 @@ public MockConfigSnapshot(IApmLogger logger = null, public string HostName => ParseHostName(Kv(EnvVarNames.HostName, _hostName, Origin)); + public IReadOnlyList IgnoreMessageQueues => + ParseIgnoreMessageQueues(Kv(EnvVarNames.IgnoreMessageQueues, _ignoreMessageQueues, Origin)); + public LogLevel LogLevel => ParseLogLevel(Kv(EnvVarNames.LogLevel, _logLevel, Origin)); public int MaxBatchEventCount => ParseMaxBatchEventCount(Kv(EnvVarNames.MaxBatchEventCount, _maxBatchEventCount, Origin)); public int MaxQueueEventCount => ParseMaxQueueEventCount(Kv(EnvVarNames.MaxQueueEventCount, _maxQueueEventCount, Origin)); diff --git a/test/Elastic.Apm.Tests/ConstructorTests.cs b/test/Elastic.Apm.Tests/ConstructorTests.cs index e8d84f1bf..b40ecea14 100644 --- a/test/Elastic.Apm.Tests/ConstructorTests.cs +++ b/test/Elastic.Apm.Tests/ConstructorTests.cs @@ -64,6 +64,7 @@ private class LogConfig : IConfigSnapshot public string ServiceName { get; } public string ServiceVersion { get; } public IReadOnlyList DisableMetrics => ConfigConsts.DefaultValues.DisableMetrics; + public IReadOnlyList IgnoreMessageQueues => ConfigConsts.DefaultValues.IgnoreMessageQueues; public double SpanFramesMinDurationInMilliseconds => ConfigConsts.DefaultValues.SpanFramesMinDurationInMilliseconds; public int StackTraceLimit => ConfigConsts.DefaultValues.StackTraceLimit; public double TransactionSampleRate => ConfigConsts.DefaultValues.TransactionSampleRate; From 7c627e0d8f51e0d4678edbfa9ee23842d2f234e4 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 15 Mar 2021 20:14:53 +1000 Subject: [PATCH 05/32] Microsoft.Azure.ServiceBus integration --- ...essagingServiceBusDiagnosticsSubscriber.cs | 2 +- ...rosoftAzureServiceBusDiagnosticListener.cs | 289 ++++++++++++++++++ ...oftAzureServiceBusDiagnosticsSubscriber.cs | 31 ++ .../Reflection/ExpressionBuilder.cs | 13 + .../Azure/AzureServiceBusTestEnvironment.cs | 7 + ...sagingServiceBusDiagnosticListenerTests.cs | 3 +- ...pm.Azure.Messaging.ServiceBus.Tests.csproj | 1 + ...tAzureServiceBusDiagnosticListenerTests.cs | 282 +++++++++++++++++ 8 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs create mode 100644 src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs create mode 100644 test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs index 9b9c80600..d593f51e0 100644 --- a/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs +++ b/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs @@ -12,7 +12,7 @@ namespace Elastic.Apm.Azure.Messaging.ServiceBus public class AzureMessagingServiceBusDiagnosticsSubscriber : IDiagnosticsSubscriber { /// - /// Start listening for HttpClient diagnostic source events. + /// Start listening for Azure.Messaging.ServiceBus diagnostic source events. /// public IDisposable Subscribe(IApmAgent agent) { diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs new file mode 100644 index 000000000..4fa67e588 --- /dev/null +++ b/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -0,0 +1,289 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.Helpers; +using Elastic.Apm.Logging; +using Elastic.Apm.Reflection; + +namespace Elastic.Apm.Azure.Messaging.ServiceBus +{ + /// + /// Creates spans for diagnostic events from Microsoft.Azure.ServiceBus + /// + public class MicrosoftAzureServiceBusDiagnosticListener: IDiagnosticListener + { + private readonly IApmAgent _agent; + private readonly ApmAgent _realAgent; + private readonly ConcurrentDictionary _processingSegments = + new ConcurrentDictionary(); + + private readonly ConcurrentDictionary> _sendProperties = + new ConcurrentDictionary>(); + + private readonly ConcurrentDictionary> _scheduleProperties = + new ConcurrentDictionary>(); + + private readonly ConcurrentDictionary> _receiveProperties = + new ConcurrentDictionary>(); + + private readonly ConcurrentDictionary> _receiveDeferredProperties = + new ConcurrentDictionary>(); + + private readonly ConcurrentDictionary> _exceptionProperties = + new ConcurrentDictionary>(); + + internal IApmLogger Logger { get; } + + public string Name { get; } = "Microsoft.Azure.ServiceBus"; + + public MicrosoftAzureServiceBusDiagnosticListener(IApmAgent agent) + { + _agent = agent; + _realAgent = agent as ApmAgent; + Logger = _agent.Logger.Scoped(nameof(AzureMessagingServiceBusDiagnosticListener)); + } + + public void OnCompleted() => Logger.Trace()?.Log("Completed"); + + public void OnError(Exception error) => Logger.Error()?.LogExceptionWithCaller(error, nameof(OnError)); + + public void OnNext(KeyValuePair kv) + { + Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); + + if (string.IsNullOrEmpty(kv.Key)) + { + Logger.Trace()?.Log($"Key is {(kv.Key == null ? "null" : "an empty string")} - exiting"); + return; + } + + switch (kv.Key) + { + case "Microsoft.Azure.ServiceBus.Send.Start": + OnSendStart(kv, "SEND", _sendProperties); + break; + case "Microsoft.Azure.ServiceBus.Send.Stop": + OnStop(kv, _sendProperties); + break; + case "Microsoft.Azure.ServiceBus.Schedule.Start": + OnSendStart(kv, "SCHEDULE", _scheduleProperties); + break; + case "Microsoft.Azure.ServiceBus.Schedule.Stop": + OnStop(kv, _scheduleProperties); + break; + case "Microsoft.Azure.ServiceBus.Receive.Start": + OnReceiveStart(kv, "RECEIVE", _receiveProperties); + break; + case "Microsoft.Azure.ServiceBus.Receive.Stop": + OnStop(kv, _receiveProperties); + break; + case "Microsoft.Azure.ServiceBus.ReceiveDeferred.Start": + OnReceiveStart(kv, "RECEIVEDEFERRED", _receiveDeferredProperties); + break; + case "Microsoft.Azure.ServiceBus.ReceiveDeferred.Stop": + OnStop(kv, _receiveDeferredProperties); + break; + case "Microsoft.Azure.ServiceBus.Exception": + OnException(kv, _exceptionProperties); + break; + default: + Logger.Trace()?.Log("`{DiagnosticEventKey}' key is not a traced diagnostic event", kv.Key); + break; + } + } + + private void OnReceiveStart( + KeyValuePair kv, + string action, + ConcurrentDictionary> cachedProperties) + { + if (kv.Value is null) + { + Logger.Trace()?.Log("Value is null - exiting"); + return; + } + + var activity = Activity.Current; + var entityGetter = cachedProperties.GetOrAdd( + "Entity", + k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); + var endpointGetter = cachedProperties.GetOrAdd( + "Endpoint", + k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); + + var queueName = entityGetter(kv.Value) as string; + var destinationAddress = endpointGetter(kv.Value) as Uri; + + if (MatchesIgnoreMessageQueues(queueName)) + return; + + var transactionName = queueName is null + ? $"AzureServiceBus {action}" + : $"AzureServiceBus {action} from {queueName}"; + + DistributedTracingData tracingData = null; + + var transaction = _agent.Tracer.StartTransaction(transactionName, "messaging", tracingData); + + // transaction creation will create an activity, so use this as the key. + // TODO: change when existing activity is used. + var activityId = Activity.Current.Id; + + transaction.Context.Service = Service.GetDefaultService(_agent.ConfigurationReader, _agent.Logger); + transaction.Context.Service.Framework = new Framework { Name = "AzureServiceBus" }; + + if (!_processingSegments.TryAdd(activityId, transaction)) + { + Logger.Error()?.Log( + "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", + action, + transaction.Id, + activity.Id); + } + } + + private bool MatchesIgnoreMessageQueues(string name) + { + if (name != null && _realAgent != null) + { + var matcher = WildcardMatcher.AnyMatch(_realAgent.ConfigStore.CurrentSnapshot.IgnoreMessageQueues, name); + if (matcher != null) + { + Logger.Debug()?.Log( + "Not tracing message from {QueueName} because it matched IgnoreMessageQueues pattern {Matcher}", + name, + matcher.GetMatcher()); + return true; + } + } + + return false; + } + + private void OnSendStart( + KeyValuePair kv, + string action, + ConcurrentDictionary> cachedProperties + ) + { + var currentSegment = _agent.GetCurrentExecutionSegment(); + if (currentSegment is null) + { + Logger.Trace()?.Log("No current transaction or span - exiting"); + return; + } + + if (kv.Value is null) + { + Logger.Trace()?.Log("Value is null - exiting"); + return; + } + + var activity = Activity.Current; + + var entityGetter = cachedProperties.GetOrAdd( + "Entity", + k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); + var endpointGetter = cachedProperties.GetOrAdd( + "Endpoint", + k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); + + var queueName = entityGetter(kv.Value) as string; + var destinationAddress = endpointGetter(kv.Value) as Uri; + + if (MatchesIgnoreMessageQueues(queueName)) + return; + + var spanName = queueName is null + ? $"AzureServiceBus {action}" + : $"AzureServiceBus {action} to {queueName}"; + + var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", action.ToLowerInvariant()); + + span.Context.Destination = new Destination + { + Address = destinationAddress?.AbsoluteUri, + Service = new Destination.DestinationService + { + Name = "azureservicebus", + Resource = queueName is null ? "azureservicebus" : $"azureservicebus/{queueName}", + Type = "messaging" + } + }; + + if (!_processingSegments.TryAdd(activity.Id, span)) + { + Logger.Error()?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + action, + span.Id, + activity.Id); + } + } + + private void OnStop( + KeyValuePair kv, + ConcurrentDictionary> cachedProperties) + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + return; + + var statusGetter = cachedProperties.GetOrAdd( + "Status", + k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); + + var status = statusGetter(kv.Value) as TaskStatus?; + var outcome = status switch + { + TaskStatus.RanToCompletion => Outcome.Success, + TaskStatus.Canceled => Outcome.Failure, + TaskStatus.Faulted => Outcome.Failure, + _ => Outcome.Unknown + }; + + segment.Outcome = outcome; + segment.End(); + } + + private void OnException( + KeyValuePair kv, + ConcurrentDictionary> cachedProperties) + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + return; + + var exceptionGetter = cachedProperties.GetOrAdd( + "Exception", + k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); + + if (exceptionGetter(kv.Value) is Exception exception) + segment.CaptureException(exception); + + segment.Outcome = Outcome.Failure; + segment.End(); + } + } +} diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs new file mode 100644 index 000000000..41821438d --- /dev/null +++ b/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs @@ -0,0 +1,31 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Diagnostics; +using Elastic.Apm.DiagnosticSource; + +namespace Elastic.Apm.Azure.Messaging.ServiceBus +{ + public class MicrosoftAzureServiceBusDiagnosticsSubscriber : IDiagnosticsSubscriber + { + /// + /// Start listening for Azure.Messaging.ServiceBus diagnostic source events. + /// + public IDisposable Subscribe(IApmAgent agent) + { + var retVal = new CompositeDisposable(); + + var initializer = new DiagnosticInitializer(agent.Logger, new[] { new MicrosoftAzureServiceBusDiagnosticListener(agent) }); + retVal.Add(initializer); + + retVal.Add(DiagnosticListener + .AllListeners + .Subscribe(initializer)); + + return retVal; + } + } +} diff --git a/src/Elastic.Apm/Reflection/ExpressionBuilder.cs b/src/Elastic.Apm/Reflection/ExpressionBuilder.cs index c4c8ba4c5..03c9efa45 100644 --- a/src/Elastic.Apm/Reflection/ExpressionBuilder.cs +++ b/src/Elastic.Apm/Reflection/ExpressionBuilder.cs @@ -34,5 +34,18 @@ public static Func BuildPropertyGetter(Type type, PropertyInfo p var returnCastExpression = Expression.Convert(memberExpression, typeof(object)); return Expression.Lambda>(returnCastExpression, parameterExpression).Compile(); } + + /// + /// Builds a delegate to get a property from an object. is cast to , + /// with the returned property cast to . + /// + public static Func BuildPropertyGetter(Type type, string propertyName) + { + var parameterExpression = Expression.Parameter(typeof(object), "value"); + var parameterCastExpression = Expression.Convert(parameterExpression, type); + var memberExpression = Expression.Property(parameterCastExpression, propertyName); + var returnCastExpression = Expression.Convert(memberExpression, typeof(object)); + return Expression.Lambda>(returnCastExpression, parameterExpression).Compile(); + } } } diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs index f1049eb53..075d9eb18 100644 --- a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs @@ -9,10 +9,17 @@ using Azure.Messaging.ServiceBus; using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Terraform; using Elastic.Apm.Tests.Utilities; +using Xunit; using Xunit.Abstractions; namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure { + [CollectionDefinition("AzureServiceBus")] + public class AzureServiceBusTestEnvironmentCollection : ICollectionFixture + { + + } + /// /// A test environment for Azure Service Bus that deploys and configures an Azure Service Bus namespace /// in a given region and location diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs index 5b1cb3ed4..cd31bd0e7 100644 --- a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -12,7 +12,8 @@ namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests { - public class AzureMessagingServiceBusDiagnosticListenerTests : IClassFixture, IDisposable, IAsyncDisposable + [Collection("AzureServiceBus")] + public class AzureMessagingServiceBusDiagnosticListenerTests : IDisposable, IAsyncDisposable { private readonly AzureServiceBusTestEnvironment _environment; private readonly ApmAgent _agent; diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj index b59942ea2..0d569090d 100644 --- a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs new file mode 100644 index 000000000..6688cf4cd --- /dev/null +++ b/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs @@ -0,0 +1,282 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus.Administration; +using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.XUnit; +using FluentAssertions; +using Microsoft.Azure.ServiceBus; +using Microsoft.Azure.ServiceBus.Core; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests +{ + [Collection("AzureServiceBus")] + public class MicrosoftAzureServiceBusDiagnosticListenerTests : IDisposable + { + private readonly AzureServiceBusTestEnvironment _environment; + private readonly ApmAgent _agent; + private readonly MockPayloadSender _sender; + private readonly ServiceBusAdministrationClient _adminClient; + + public MicrosoftAzureServiceBusDiagnosticListenerTests(AzureServiceBusTestEnvironment environment, ITestOutputHelper output) + { + _environment = environment; + + var logger = new XUnitLogger(LogLevel.Trace, output); + _sender = new MockPayloadSender(logger); + _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); + _agent.Subscribe(new MicrosoftAzureServiceBusDiagnosticsSubscriber()); + _adminClient = new ServiceBusAdministrationClient(environment.ServiceBusConnectionString); + } + + [Fact] + public async Task Capture_Span_When_Send_To_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.QueueName); + + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendAsync(new Message(Encoding.UTF8.GetBytes("test message"))).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SEND to {scope.QueueName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("send"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Span_When_Send_To_Topic() + { + await using var scope = await TopicScope.CreateWithTopic(_adminClient); + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.TopicName); + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendAsync(new Message(Encoding.UTF8.GetBytes("test message"))).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SEND to {scope.TopicName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("send"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Span_When_Schedule_To_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.QueueName); + await _agent.Tracer.CaptureTransaction("Schedule AzureServiceBus Message", "message", async () => + { + await sender.ScheduleMessageAsync( + new Message(Encoding.UTF8.GetBytes("test message")), + DateTimeOffset.Now.AddSeconds(10)).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.QueueName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("schedule"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Span_When_Schedule_To_Topic() + { + await using var scope = await TopicScope.CreateWithTopic(_adminClient); + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.TopicName); + await _agent.Tracer.CaptureTransaction("Schedule AzureServiceBus Message", "message", async () => + { + await sender.ScheduleMessageAsync( + new Message(Encoding.UTF8.GetBytes("test message")), + DateTimeOffset.Now.AddSeconds(10)).ConfigureAwait(false); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.TopicName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azureservicebus"); + span.Action.Should().Be("schedule"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); + destination.Service.Name.Should().Be("azureservicebus"); + destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); + destination.Service.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_Receive_From_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.QueueName); + var receiver = new MessageReceiver(_environment.ServiceBusConnectionString, scope.QueueName, ReceiveMode.PeekLock); + + await sender.SendAsync( + new Message(Encoding.UTF8.GetBytes("test message"))).ConfigureAwait(false); + + await receiver.ReceiveAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2))) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(1); + var transaction = _sender.FirstTransaction; + + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_Receive_From_Topic_Subscription() + { + await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); + + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.TopicName); + var receiver = new MessageReceiver(_environment.ServiceBusConnectionString, + EntityNameHelper.FormatSubscriptionPath(scope.TopicName, scope.SubscriptionName)); + + await sender.SendAsync( + new Message(Encoding.UTF8.GetBytes("test message"))).ConfigureAwait(false); + + await receiver.ReceiveAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2))) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(1); + var transaction = _sender.FirstTransaction; + + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_ReceiveDeferred_From_Queue() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.QueueName); + var receiver = new MessageReceiver(_environment.ServiceBusConnectionString, scope.QueueName, ReceiveMode.PeekLock); + + await sender.SendAsync( + new Message(Encoding.UTF8.GetBytes("test message"))).ConfigureAwait(false); + + var message = await receiver.ReceiveAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + await receiver.DeferAsync(message.SystemProperties.LockToken).ConfigureAwait(false); + + await receiver.ReceiveDeferredMessageAsync(message.SystemProperties.SequenceNumber).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2), count: 2)) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(2); + + var transaction = _sender.FirstTransaction; + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be("messaging"); + + var secondTransaction = _sender.Transactions[1]; + secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.QueueName}"); + secondTransaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Capture_Transaction_When_ReceiveDeferred_From_Topic_Subscription() + { + await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); + + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.TopicName); + var receiver = new MessageReceiver(_environment.ServiceBusConnectionString, + EntityNameHelper.FormatSubscriptionPath(scope.TopicName, scope.SubscriptionName)); + + await sender.SendAsync( + new Message(Encoding.UTF8.GetBytes("test message"))).ConfigureAwait(false); + + var message = await receiver.ReceiveAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + await receiver.DeferAsync(message.SystemProperties.LockToken).ConfigureAwait(false); + + await receiver.ReceiveDeferredMessageAsync(message.SystemProperties.SequenceNumber).ConfigureAwait(false); + + if (!_sender.WaitForTransactions(TimeSpan.FromMinutes(2), count: 2)) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(2); + + var transaction = _sender.FirstTransaction; + transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be("messaging"); + + var secondTransaction = _sender.Transactions[1]; + secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + secondTransaction.Type.Should().Be("messaging"); + } + + [Fact] + public async Task Does_Not_Capture_Span_When_QueueName_Matches_IgnoreMessageQueues() + { + await using var scope = await QueueScope.CreateWithQueue(_adminClient); + var sender = new MessageSender(_environment.ServiceBusConnectionString, scope.QueueName); + _agent.ConfigStore.CurrentSnapshot = new MockConfigSnapshot(ignoreMessageQueues: scope.QueueName); + + await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message", async () => + { + await sender.SendAsync(new Message(Encoding.UTF8.GetBytes("test message"))).ConfigureAwait(false); + }); + + _sender.SignalEndSpans(); + _sender.WaitForSpans(); + _sender.Spans.Should().HaveCount(0); + } + + public void Dispose() => _agent.Dispose(); + } +} From 09e4382999c0117cff451809bed53caaa5d659d2 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 16 Mar 2021 12:00:58 +1000 Subject: [PATCH 06/32] Rename to Elastic.Apm.Azure.ServiceBus --- ElasticApmAgent.sln | 4 ++-- .../AzureMessagingServiceBusDiagnosticListener.cs | 0 .../AzureMessagingServiceBusDiagnosticsSubscriber.cs | 0 .../Elastic.Apm.Azure.ServiceBus.csproj} | 2 +- .../MicrosoftAzureServiceBusDiagnosticListener.cs | 0 .../MicrosoftAzureServiceBusDiagnosticsSubscriber.cs | 0 src/Elastic.Apm/Elastic.Apm.csproj | 4 ++-- .../Azure/AzureCredentials.cs | 0 .../Azure/AzureServiceBusTestEnvironment.cs | 0 .../Azure/QueueScope.cs | 0 .../Azure/TopicScope.cs | 0 .../AzureMessagingServiceBusDiagnosticListenerTests.cs | 0 .../Elastic.Apm.Azure.ServiceBus.Tests.csproj} | 4 ++-- .../MicrosoftAzureServiceBusDiagnosticListenerTests.cs | 0 .../Terraform/TerraformResourceException.cs | 0 .../Terraform/TerraformResources.cs | 0 .../Elastic.Apm.Tests.Utilities.csproj | 2 +- 17 files changed, 8 insertions(+), 8 deletions(-) rename src/{Elastic.Apm.Azure.Messaging.ServiceBus => Elastic.Apm.Azure.ServiceBus}/AzureMessagingServiceBusDiagnosticListener.cs (100%) rename src/{Elastic.Apm.Azure.Messaging.ServiceBus => Elastic.Apm.Azure.ServiceBus}/AzureMessagingServiceBusDiagnosticsSubscriber.cs (100%) rename src/{Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj => Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj} (73%) rename src/{Elastic.Apm.Azure.Messaging.ServiceBus => Elastic.Apm.Azure.ServiceBus}/MicrosoftAzureServiceBusDiagnosticListener.cs (100%) rename src/{Elastic.Apm.Azure.Messaging.ServiceBus => Elastic.Apm.Azure.ServiceBus}/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/Azure/AzureCredentials.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/Azure/AzureServiceBusTestEnvironment.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/Azure/QueueScope.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/Azure/TopicScope.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/AzureMessagingServiceBusDiagnosticListenerTests.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj => Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj} (83%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/MicrosoftAzureServiceBusDiagnosticListenerTests.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/Terraform/TerraformResourceException.cs (100%) rename test/{Elastic.Apm.Azure.Messaging.ServiceBus.Tests => Elastic.Apm.Azure.ServiceBus.Tests}/Terraform/TerraformResources.cs (100%) diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index 3f3341fa5..4f1391099 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -131,9 +131,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.Extensions.Logg EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Extensions.Logging.Tests", "test\Elastic.Apm.Extensions.Logging.Tests\Elastic.Apm.Extensions.Logging.Tests.csproj", "{B235B13F-42AE-42DA-A3C8-20D047F38685}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.Messaging.ServiceBus", "src\Elastic.Apm.Azure.Messaging.ServiceBus\Elastic.Apm.Azure.Messaging.ServiceBus.csproj", "{1D43C8C5-4116-45C5-9F4B-56C1D926ED29}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus", "src\Elastic.Apm.Azure.ServiceBus\Elastic.Apm.Azure.ServiceBus.csproj", "{1D43C8C5-4116-45C5-9F4B-56C1D926ED29}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.Messaging.ServiceBus.Tests", "test\Elastic.Apm.Azure.Messaging.ServiceBus.Tests\Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj", "{D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus.Tests", "test\Elastic.Apm.Azure.ServiceBus.Tests\Elastic.Apm.Azure.ServiceBus.Tests.csproj", "{D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs similarity index 100% rename from src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs rename to src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs similarity index 100% rename from src/Elastic.Apm.Azure.Messaging.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs rename to src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj similarity index 73% rename from src/Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj rename to src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj index fc90103e1..0cada5b07 100644 --- a/src/Elastic.Apm.Azure.Messaging.ServiceBus/Elastic.Apm.Azure.Messaging.ServiceBus.csproj +++ b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj @@ -2,7 +2,7 @@ netstandard2.0 - Elastic.Apm.Azure.Messaging.ServiceBus + Elastic.Apm.Azure.ServiceBus diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs similarity index 100% rename from src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs rename to src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs diff --git a/src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs similarity index 100% rename from src/Elastic.Apm.Azure.Messaging.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs rename to src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index ca3eb22ca..cf704d881 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -44,8 +44,8 @@ - - + + diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureCredentials.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureCredentials.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/QueueScope.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/QueueScope.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/QueueScope.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/QueueScope.cs diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/TopicScope.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/TopicScope.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Azure/TopicScope.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/TopicScope.cs diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj similarity index 83% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj rename to test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj index 0d569090d..f951564a5 100644 --- a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Elastic.Apm.Azure.Messaging.ServiceBus.Tests.csproj +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj @@ -3,7 +3,7 @@ net5.0 false - Elastic.Apm.Azure.Messaging.ServiceBus.Tests + Elastic.Apm.Azure.ServiceBus.Tests @@ -22,7 +22,7 @@ - + diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResourceException.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResourceException.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs diff --git a/test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResources.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs similarity index 100% rename from test/Elastic.Apm.Azure.Messaging.ServiceBus.Tests/Terraform/TerraformResources.cs rename to test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs diff --git a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj index 859acea5e..b0fbca0f1 100644 --- a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj +++ b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj @@ -23,7 +23,7 @@ - + From 61b984878a459f08665c31ba6fd259e00269106d Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 16 Mar 2021 14:04:23 +1000 Subject: [PATCH 07/32] Use PropertyFetcher to get properties --- .../AspNetCoreDiagnosticListener.cs | 1 + ...reMessagingServiceBusDiagnosticListener.cs | 24 ++--- ...essagingServiceBusDiagnosticsSubscriber.cs | 7 +- ...rosoftAzureServiceBusDiagnosticListener.cs | 87 +++++++------------ ...oftAzureServiceBusDiagnosticsSubscriber.cs | 7 +- .../SqlClientDiagnosticListener.cs | 1 + .../CascadePropertyFetcher.cs | 2 +- .../PropertyFetcher.cs | 10 +-- .../Reflection/PropertyFetcherCollection.cs | 38 ++++++++ .../Azure/AzureCredentials.cs | 2 +- .../Azure/AzureServiceBusTestEnvironment.cs | 4 +- .../Azure/QueueScope.cs | 2 +- .../Azure/TopicScope.cs | 2 +- ...sagingServiceBusDiagnosticListenerTests.cs | 4 +- ...tAzureServiceBusDiagnosticListenerTests.cs | 4 +- .../Terraform/TerraformResourceException.cs | 2 +- .../Terraform/TerraformResources.cs | 4 +- .../PropertyFetcherBenchmark.cs | 1 + .../HelpersTests/PropertyFetcherTests.cs | 1 + 19 files changed, 114 insertions(+), 89 deletions(-) rename src/Elastic.Apm/{Helpers => Reflection}/CascadePropertyFetcher.cs (94%) rename src/Elastic.Apm/{Helpers => Reflection}/PropertyFetcher.cs (91%) create mode 100644 src/Elastic.Apm/Reflection/PropertyFetcherCollection.cs diff --git a/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs b/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs index 2259b4687..06965ce6f 100644 --- a/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs +++ b/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs @@ -12,6 +12,7 @@ using Elastic.Apm.Helpers; using Elastic.Apm.Logging; using Elastic.Apm.Model; +using Elastic.Apm.Reflection; using Microsoft.AspNetCore.Http; namespace Elastic.Apm.AspNetCore.DiagnosticListener diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index 4abc07527..672f49a3c 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -12,7 +12,7 @@ using Elastic.Apm.Helpers; using Elastic.Apm.Logging; -namespace Elastic.Apm.Azure.Messaging.ServiceBus +namespace Elastic.Apm.Azure.ServiceBus { /// /// Creates spans for diagnostic events from Azure.Messaging.ServiceBus @@ -213,7 +213,7 @@ private void OnSendStart(KeyValuePair kv, string action) if (!_processingSegments.TryAdd(activity.Id, span)) { - Logger.Error()?.Log( + Logger.Trace()?.Log( "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", action, span.Id, @@ -231,18 +231,15 @@ private void OnStop() } if (!_processingSegments.TryRemove(activity.Id, out var segment)) - return; - - // TODO: Get from current activity when current activity is reused when starting transaction. - var parent = activity.Parent; - if (parent?.Links != null) { - foreach (var link in parent.Links) - { - // Do something with links - } + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); + return; } + // TODO: Get the linked Activit(y/ies) from DiagnosticActivity when https://github.com/elastic/apm/issues/122 is finalized + segment.Outcome = Outcome.Success; segment.End(); } @@ -257,7 +254,12 @@ private void OnException(KeyValuePair kv) } if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; + } if (kv.Value is Exception e) segment.CaptureException(e); diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs index d593f51e0..446e1cddd 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticsSubscriber.cs @@ -7,12 +7,15 @@ using System.Diagnostics; using Elastic.Apm.DiagnosticSource; -namespace Elastic.Apm.Azure.Messaging.ServiceBus +namespace Elastic.Apm.Azure.ServiceBus { + /// + /// Subscribes to diagnostic source events from Azure.Messaging.ServiceBus + /// public class AzureMessagingServiceBusDiagnosticsSubscriber : IDiagnosticsSubscriber { /// - /// Start listening for Azure.Messaging.ServiceBus diagnostic source events. + /// Subscribes diagnostic source events. /// public IDisposable Subscribe(IApmAgent agent) { diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs index 4fa67e588..c40f25ade 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -14,7 +14,7 @@ using Elastic.Apm.Logging; using Elastic.Apm.Reflection; -namespace Elastic.Apm.Azure.Messaging.ServiceBus +namespace Elastic.Apm.Azure.ServiceBus { /// /// Creates spans for diagnostic events from Microsoft.Azure.ServiceBus @@ -23,23 +23,12 @@ public class MicrosoftAzureServiceBusDiagnosticListener: IDiagnosticListener { private readonly IApmAgent _agent; private readonly ApmAgent _realAgent; - private readonly ConcurrentDictionary _processingSegments = - new ConcurrentDictionary(); - - private readonly ConcurrentDictionary> _sendProperties = - new ConcurrentDictionary>(); - - private readonly ConcurrentDictionary> _scheduleProperties = - new ConcurrentDictionary>(); - - private readonly ConcurrentDictionary> _receiveProperties = - new ConcurrentDictionary>(); - - private readonly ConcurrentDictionary> _receiveDeferredProperties = - new ConcurrentDictionary>(); - - private readonly ConcurrentDictionary> _exceptionProperties = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); + private readonly PropertyFetcherCollection _sendProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; + private readonly PropertyFetcherCollection _scheduleProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; + private readonly PropertyFetcherCollection _receiveProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; + private readonly PropertyFetcherCollection _receiveDeferredProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; + private readonly PropertyFetcher _exceptionProperty = new PropertyFetcher("Exception"); internal IApmLogger Logger { get; } @@ -93,7 +82,7 @@ public void OnNext(KeyValuePair kv) OnStop(kv, _receiveDeferredProperties); break; case "Microsoft.Azure.ServiceBus.Exception": - OnException(kv, _exceptionProperties); + OnException(kv); break; default: Logger.Trace()?.Log("`{DiagnosticEventKey}' key is not a traced diagnostic event", kv.Key); @@ -104,7 +93,7 @@ public void OnNext(KeyValuePair kv) private void OnReceiveStart( KeyValuePair kv, string action, - ConcurrentDictionary> cachedProperties) + PropertyFetcherCollection cachedProperties) { if (kv.Value is null) { @@ -113,15 +102,7 @@ private void OnReceiveStart( } var activity = Activity.Current; - var entityGetter = cachedProperties.GetOrAdd( - "Entity", - k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); - var endpointGetter = cachedProperties.GetOrAdd( - "Endpoint", - k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); - - var queueName = entityGetter(kv.Value) as string; - var destinationAddress = endpointGetter(kv.Value) as Uri; + var queueName = cachedProperties.Fetch(kv.Value,"Entity") as string; if (MatchesIgnoreMessageQueues(queueName)) return; @@ -130,6 +111,7 @@ private void OnReceiveStart( ? $"AzureServiceBus {action}" : $"AzureServiceBus {action} from {queueName}"; + // TODO: initialize tracing data from linked messages, once https://github.com/elastic/apm/issues/122 is finalized DistributedTracingData tracingData = null; var transaction = _agent.Tracer.StartTransaction(transactionName, "messaging", tracingData); @@ -143,7 +125,7 @@ private void OnReceiveStart( if (!_processingSegments.TryAdd(activityId, transaction)) { - Logger.Error()?.Log( + Logger.Trace()?.Log( "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", action, transaction.Id, @@ -172,7 +154,7 @@ private bool MatchesIgnoreMessageQueues(string name) private void OnSendStart( KeyValuePair kv, string action, - ConcurrentDictionary> cachedProperties + PropertyFetcherCollection cachedProperties ) { var currentSegment = _agent.GetCurrentExecutionSegment(); @@ -189,16 +171,8 @@ ConcurrentDictionary> cachedProperties } var activity = Activity.Current; - - var entityGetter = cachedProperties.GetOrAdd( - "Entity", - k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); - var endpointGetter = cachedProperties.GetOrAdd( - "Endpoint", - k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); - - var queueName = entityGetter(kv.Value) as string; - var destinationAddress = endpointGetter(kv.Value) as Uri; + var queueName = cachedProperties.Fetch(kv.Value,"Entity") as string; + var destinationAddress = cachedProperties.Fetch(kv.Value, "Endpoint") as Uri; if (MatchesIgnoreMessageQueues(queueName)) return; @@ -222,8 +196,8 @@ ConcurrentDictionary> cachedProperties if (!_processingSegments.TryAdd(activity.Id, span)) { - Logger.Error()?.Log( - "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + Logger.Trace()?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked segments", action, span.Id, activity.Id); @@ -232,7 +206,7 @@ ConcurrentDictionary> cachedProperties private void OnStop( KeyValuePair kv, - ConcurrentDictionary> cachedProperties) + PropertyFetcherCollection cachedProperties) { var activity = Activity.Current; if (activity is null) @@ -242,13 +216,14 @@ private void OnStop( } if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; + } - var statusGetter = cachedProperties.GetOrAdd( - "Status", - k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); - - var status = statusGetter(kv.Value) as TaskStatus?; + var status = cachedProperties.Fetch(kv.Value, "Status") as TaskStatus?; var outcome = status switch { TaskStatus.RanToCompletion => Outcome.Success, @@ -262,8 +237,7 @@ private void OnStop( } private void OnException( - KeyValuePair kv, - ConcurrentDictionary> cachedProperties) + KeyValuePair kv) { var activity = Activity.Current; if (activity is null) @@ -273,13 +247,14 @@ private void OnException( } if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; + } - var exceptionGetter = cachedProperties.GetOrAdd( - "Exception", - k => ExpressionBuilder.BuildPropertyGetter(kv.Value.GetType(), k)); - - if (exceptionGetter(kv.Value) is Exception exception) + if (_exceptionProperty.Fetch(kv.Value) is Exception exception) segment.CaptureException(exception); segment.Outcome = Outcome.Failure; diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs index 41821438d..e2e012abc 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticsSubscriber.cs @@ -7,12 +7,15 @@ using System.Diagnostics; using Elastic.Apm.DiagnosticSource; -namespace Elastic.Apm.Azure.Messaging.ServiceBus +namespace Elastic.Apm.Azure.ServiceBus { + /// + /// Subscribes to diagnostic source events from Microsoft.Azure.ServiceBus + /// public class MicrosoftAzureServiceBusDiagnosticsSubscriber : IDiagnosticsSubscriber { /// - /// Start listening for Azure.Messaging.ServiceBus diagnostic source events. + /// Subscribes diagnostic source events. /// public IDisposable Subscribe(IApmAgent agent) { diff --git a/src/Elastic.Apm.SqlClient/SqlClientDiagnosticListener.cs b/src/Elastic.Apm.SqlClient/SqlClientDiagnosticListener.cs index 756295aae..7bd28cc2f 100644 --- a/src/Elastic.Apm.SqlClient/SqlClientDiagnosticListener.cs +++ b/src/Elastic.Apm.SqlClient/SqlClientDiagnosticListener.cs @@ -12,6 +12,7 @@ using Elastic.Apm.Helpers; using Elastic.Apm.Logging; using Elastic.Apm.Model; +using Elastic.Apm.Reflection; namespace Elastic.Apm.SqlClient { diff --git a/src/Elastic.Apm/Helpers/CascadePropertyFetcher.cs b/src/Elastic.Apm/Reflection/CascadePropertyFetcher.cs similarity index 94% rename from src/Elastic.Apm/Helpers/CascadePropertyFetcher.cs rename to src/Elastic.Apm/Reflection/CascadePropertyFetcher.cs index 65b51295e..09470bcd6 100644 --- a/src/Elastic.Apm/Helpers/CascadePropertyFetcher.cs +++ b/src/Elastic.Apm/Reflection/CascadePropertyFetcher.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Apm.Helpers +namespace Elastic.Apm.Reflection { internal class CascadePropertyFetcher : PropertyFetcher { diff --git a/src/Elastic.Apm/Helpers/PropertyFetcher.cs b/src/Elastic.Apm/Reflection/PropertyFetcher.cs similarity index 91% rename from src/Elastic.Apm/Helpers/PropertyFetcher.cs rename to src/Elastic.Apm/Reflection/PropertyFetcher.cs index 39524f792..71f2c4bba 100644 --- a/src/Elastic.Apm/Helpers/PropertyFetcher.cs +++ b/src/Elastic.Apm/Reflection/PropertyFetcher.cs @@ -6,11 +6,11 @@ using System.Linq; using System.Reflection; -namespace Elastic.Apm.Helpers +namespace Elastic.Apm.Reflection { internal class PropertyFetcher { - private readonly string _propertyName; + public string PropertyName { get; } private PropertyFetch _innerFetcher; public PropertyFetcher(string propertyName) @@ -18,7 +18,7 @@ public PropertyFetcher(string propertyName) if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("The value must be non-empty, non-null or non-whitespace", nameof(propertyName)); - _propertyName = propertyName; + PropertyName = propertyName; } public virtual object Fetch(object obj) @@ -26,10 +26,10 @@ public virtual object Fetch(object obj) if (_innerFetcher == null) { var type = obj.GetType().GetTypeInfo(); - var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, _propertyName, StringComparison.OrdinalIgnoreCase)); + var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, PropertyName, StringComparison.OrdinalIgnoreCase)); if (property == null) { - property = type.GetProperty(_propertyName); + property = type.GetProperty(PropertyName); } _innerFetcher = PropertyFetch.FetcherForProperty(property); diff --git a/src/Elastic.Apm/Reflection/PropertyFetcherCollection.cs b/src/Elastic.Apm/Reflection/PropertyFetcherCollection.cs new file mode 100644 index 000000000..ab2a78f81 --- /dev/null +++ b/src/Elastic.Apm/Reflection/PropertyFetcherCollection.cs @@ -0,0 +1,38 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Collections; +using System.Collections.Generic; + +namespace Elastic.Apm.Reflection +{ + /// + /// A collection of property fetchers, used to retrieve property values + /// from objects at runtime. + /// + internal class PropertyFetcherCollection : IEnumerable + { + private readonly Dictionary _propertyFetchers; + + public PropertyFetcherCollection() => + _propertyFetchers = new Dictionary(); + + public PropertyFetcherCollection(int capacity) => + _propertyFetchers = new Dictionary(capacity); + + public void Add(PropertyFetcher propertyFetcher) => + _propertyFetchers.Add(propertyFetcher.PropertyName, propertyFetcher); + + public void Add(string propertyName) => + _propertyFetchers.Add(propertyName, new PropertyFetcher(propertyName)); + + public object Fetch(object obj, string propertyName) => + _propertyFetchers.TryGetValue(propertyName, out var fetcher) ? fetcher.Fetch(obj) : null; + + public IEnumerator GetEnumerator() => _propertyFetchers.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs index d32bb8a7e..e47cd74e0 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs @@ -10,7 +10,7 @@ using Elastic.Apm.Tests.Utilities; using Newtonsoft.Json; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure +namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure { public class AzureCredentials { diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs index 075d9eb18..efaf37fbc 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs @@ -7,12 +7,12 @@ using System.Collections.Generic; using System.IO; using Azure.Messaging.ServiceBus; -using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Terraform; +using Elastic.Apm.Azure.ServiceBus.Tests.Terraform; using Elastic.Apm.Tests.Utilities; using Xunit; using Xunit.Abstractions; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure +namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure { [CollectionDefinition("AzureServiceBus")] public class AzureServiceBusTestEnvironmentCollection : ICollectionFixture diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/QueueScope.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/QueueScope.cs index 44879e113..dda9a97c0 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/QueueScope.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/QueueScope.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Azure.Messaging.ServiceBus.Administration; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure +namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure { public class QueueScope : IAsyncDisposable { diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/TopicScope.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/TopicScope.cs index a54b151ef..7f931caf8 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/TopicScope.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/TopicScope.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Azure.Messaging.ServiceBus.Administration; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure +namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure { public class TopicScope : IAsyncDisposable { diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs index cd31bd0e7..b9ef3f17f 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus.Administration; -using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure; +using Elastic.Apm.Azure.ServiceBus.Tests.Azure; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; using Elastic.Apm.Tests.Utilities.XUnit; @@ -10,7 +10,7 @@ using Xunit; using Xunit.Abstractions; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests +namespace Elastic.Apm.Azure.ServiceBus.Tests { [Collection("AzureServiceBus")] public class AzureMessagingServiceBusDiagnosticListenerTests : IDisposable, IAsyncDisposable diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs index 6688cf4cd..30400bf5d 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs @@ -2,7 +2,7 @@ using System.Text; using System.Threading.Tasks; using Azure.Messaging.ServiceBus.Administration; -using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure; +using Elastic.Apm.Azure.ServiceBus.Tests.Azure; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; using Elastic.Apm.Tests.Utilities.XUnit; @@ -12,7 +12,7 @@ using Xunit; using Xunit.Abstractions; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests +namespace Elastic.Apm.Azure.ServiceBus.Tests { [Collection("AzureServiceBus")] public class MicrosoftAzureServiceBusDiagnosticListenerTests : IDisposable diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs index 4917102c1..d7da010f1 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Linq; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Terraform +namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform { /// /// An exception from interacting with terraform resources. diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs index 0a24cd6ee..339fcc940 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs @@ -8,13 +8,13 @@ using System.IO; using System.Runtime.ExceptionServices; using System.Text; -using Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Azure; +using Elastic.Apm.Azure.ServiceBus.Tests.Azure; using ProcNet; using ProcNet.Std; using Xunit.Abstractions; using Xunit.Sdk; -namespace Elastic.Apm.Azure.Messaging.ServiceBus.Tests.Terraform +namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform { /// /// Interact with Terraform templates to apply and destroy resources diff --git a/test/Elastic.Apm.Benchmarks/PropertyFetcherBenchmark.cs b/test/Elastic.Apm.Benchmarks/PropertyFetcherBenchmark.cs index f35a871b6..3c80144f8 100644 --- a/test/Elastic.Apm.Benchmarks/PropertyFetcherBenchmark.cs +++ b/test/Elastic.Apm.Benchmarks/PropertyFetcherBenchmark.cs @@ -6,6 +6,7 @@ using System.Reflection; using BenchmarkDotNet.Attributes; using Elastic.Apm.Helpers; +using Elastic.Apm.Reflection; using Microsoft.Data.SqlClient; namespace Elastic.Apm.Benchmarks diff --git a/test/Elastic.Apm.Tests/HelpersTests/PropertyFetcherTests.cs b/test/Elastic.Apm.Tests/HelpersTests/PropertyFetcherTests.cs index 6739d449b..8ad9e3cc6 100644 --- a/test/Elastic.Apm.Tests/HelpersTests/PropertyFetcherTests.cs +++ b/test/Elastic.Apm.Tests/HelpersTests/PropertyFetcherTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Apm.Helpers; +using Elastic.Apm.Reflection; using FluentAssertions; using Xunit; From 34edd2f6932fffa2586fbed4e053510b36bbd93a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 16 Mar 2021 15:09:03 +1000 Subject: [PATCH 08/32] Add packaging information --- .../Elastic.Apm.Azure.ServiceBus.csproj | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj index 0cada5b07..1c5322f46 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj +++ b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj @@ -1,8 +1,14 @@ - netstandard2.0 - Elastic.Apm.Azure.ServiceBus + netstandard2.0 + Elastic.Apm.Azure.ServiceBus + Elastic.Apm.Azure.ServiceBus + Elastic.Apm.Azure.ServiceBus + Elastic APM for Azure Service Bus. This package contains auto instrumentation for Azure.Messaging.ServiceBus + and Microsoft.Azure.ServiceBus packages. + apm, monitoring, elastic, elasticapm, analytics, azure, service, bus, servicebus + true From 06feaf85d890375e3cd713bae52b039efc243d86 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 16 Mar 2021 15:09:16 +1000 Subject: [PATCH 09/32] test with minimally supported versions --- .../Elastic.Apm.Azure.ServiceBus.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj index f951564a5..65a695ff7 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Elastic.Apm.Azure.ServiceBus.Tests.csproj @@ -7,8 +7,8 @@ - - + + From bf4b7de260766b9b286a4be6003b144cd7fb5ec9 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 16 Mar 2021 15:09:30 +1000 Subject: [PATCH 10/32] Add documentation --- docs/configuration.asciidoc | 32 ++++++++++++++++++++- docs/setup.asciidoc | 42 ++++++++++++++++++++++++++++ docs/supported-technologies.asciidoc | 30 +++++++++++++++----- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 552ea50aa..2d3c85126 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -892,10 +892,39 @@ When this setting is `true`, the agent will also add the header `elasticapm-trac | `true` | Boolean |============ +[[config-messaging]] +=== Messaging configuration options + +[float] +[[config-ignore-message-queues]] +==== `IgnoreMessageQueues` (added[1.9]) + +Used to filter out specific messaging queues/topics/exchanges from being traced. When set, sends-to and receives-from the +specified queues/topics/exchanges will be ignored. + +This config accepts a comma separated string of wildcard patterns of queues/topics/exchange names which should be ignored. + +The wildcard, `*`, matches zero or more characters, and matching is case insensitive by default. +Prepending an element with `(?-i)` makes the matching case sensitive. +Examples: `/foo/*/bar/*/baz*`, `*foo*`. + +[options="header"] +|============ +| Default | Type +| | String +|============ + +[options="header"] +|============ +| Environment variable name | IConfiguration or Web.config key +| `ELASTIC_APM_IGNORE_MESSAGE_QUEUES` | `ElasticApm:IgnoreMessageQueues` +|============ + + [[config-stacktrace]] === Stacktrace configuration options -[float] +[float] [[config-application-namespaces]] ==== `ApplicationNamespaces` (added[1.5]) @@ -1040,6 +1069,7 @@ you must instead set the `LogLevel` for the internal APM logger under the `Loggi | <> | No | Stacktrace | <> | No | Reporter | <> | No | Core +| <> | Yes | Messaging, Performance | <> | No | Core | <> | Yes | Supportability | <> | No | Reporter diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index 37ec3725b..9eb5f5149 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -14,6 +14,7 @@ On .NET Core the agent also supports auto instrumentation without any code chang * <> * <> * <> +* <> * <> [float] @@ -53,6 +54,14 @@ https://www.nuget.org/packages/Elastic.Apm.SqlClient[**Elastic.Apm.SqlClient**]: This package contains https://www.nuget.org/packages/System.Data.SqlClient[System.Data.SqlClient] and https://www.nuget.org/packages/Microsoft.Data.SqlClient[Microsoft.Data.SqlClient] monitoring related code. +https://www.nuget.org/packages/Elastic.Apm.StackExchange.Redis[**Elastic.Apm.StackExchange.Redis**]:: + +This packages contains instrumentation to capture spans for commands sent to redis with https://www.nuget.org/packages/StackExchange.Redis/[StackExchange.Redis] package. + +https://www.nuget.org/packages/Elastic.Apm.StackExchange.Redis[**Elastic.Apm.Azure.ServiceBus**]:: + +This packages contains instrumentation to capture transactions and spans for messages sent and received from Azure Service Bus with https://www.nuget.org/packages/Microsoft.Azure.ServiceBus/[Microsoft.Azure.ServiceBus] and https://www.nuget.org/packages/Azure.Messaging.ServiceBus/[Azure.Messaging.ServiceBus] packages. + [[setup-dotnet-net-core]] === .NET Core @@ -361,6 +370,39 @@ connection.UseElasticApm(); A callback is registered with the `IConnectionMultiplexer` to provide a profiling session for each transaction and span that captures redis commands sent with `IConnectionMultiplexer`. +[[setup-azure-servicebus]] +=== Azure Service Bus + +[float] +==== Quick start + +Instrumentation can be enabled for Azure Service Bus by referencing https://www.nuget.org/packages/Elastic.Apm.Azure.ServiceBus[`Elastic.Apm.Azure.ServiceBus`] package and subscribing to diagnostic events +using one of the subscribers: + +. If the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, the subscribers will be automatically subscribed with the agent, and no further action is required. +. If you're using `Microsoft.Azure.ServiceBus`, subscribe `MicrosoftAzureServiceBusDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new MicrosoftAzureServiceBusDiagnosticsSubscriber()); +---- +. If you're using `Azure.Messaging.ServiceBus`, subscribe `AzureMessagingServiceBusDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureMessagingServiceBusDiagnosticsSubscriber()); +---- + +A new transaction is created when + +* one or more messages are received from a queue or topic subscription. +* a message is receive deferred from a queue or topic subscription. + +A new span is created when there is a current transaction, and when + +* one or more messages are sent to a queue or topic. +* one or more messages are scheduled to a queue or a topic. + [[setup-general]] === Other .NET applications diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index eb5dcaafe..c96b4eb53 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -64,7 +64,7 @@ Streaming is not supported. In practice this means for streaming use-cases the a |Framework |Supported versions |Supported since agent's version |gRPC on .NET Core -|2.23.2 and later +|2.23.2+ |1.7 |=== @@ -78,24 +78,24 @@ We support automatic instrumentation for the following data access technologies. |Data access technology |Supported versions |Notes |Supported since agent's version |Entity Framework (EF) Core -|2.x +|2.x+ |A DB span is automatically created for each access to underlying database performed by Entity Framework Core. |1.0 |Entity Framework (EF) 6 -|6.2 and later +|6.2+ |A DB span is automatically created for each access to underlying database performed by Entity Framework 6. |1.2 | Elasticsearch (Elasticsearch.Net and NEST) -| 7.6.0 +| 7.6.0+ | __If you're using 7.10.1 or 7.11.0, upgrade to at least 7.11.1 which fixes a bug in capture__ -| 1.6.0 +| 1.6 | Redis (StackExchange.Redis) -| 2.0.495 +| 2.0.495+ | A DB span is automatically created for each profiled redis command peformed by StackExchange.Redis -| 1.8.0 +| 1.8 |=== [float] @@ -118,3 +118,19 @@ The spans are named after the schema ` `, for example `GET elastic |1.1 |=== + +[float] +[[supported-cloud-services]] +=== Cloud services + +Automatic instrumentation for the following cloud services + +|=== +| Cloud services | Supported versions | Notes | Supported since agent's version + +| Azure Service Bus +| 3.0.0+ for Microsoft.Azure.ServiceBus, + 7.0.0+ for Azure.Messaging.ServiceBus +| A new transaction is created for received and +receive deferred messages. A new span is created for sent and scheduled messages if there's a current transaction. +| 1.9 \ No newline at end of file From 5f1d28f625f4186880853b85dc1b0fc48de50d61 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 16 Mar 2021 15:47:28 +1000 Subject: [PATCH 11/32] add example credential file --- .credentials.example.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .credentials.example.json diff --git a/.credentials.example.json b/.credentials.example.json new file mode 100644 index 000000000..9b7e1c794 --- /dev/null +++ b/.credentials.example.json @@ -0,0 +1,6 @@ +{ + "ClientId": "", + "ClientSecret": "", + "TenantId": "", + "SubscriptionId": "" +} \ No newline at end of file From 1626540974705cf8e6807119553bd9599926df57 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 16 Mar 2021 17:58:52 +1000 Subject: [PATCH 12/32] Use DiagnosticListenerBase --- ...reMessagingServiceBusDiagnosticListener.cs | 27 ++++++------------ ...rosoftAzureServiceBusDiagnosticListener.cs | 28 ++++++------------- 2 files changed, 16 insertions(+), 39 deletions(-) diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index 672f49a3c..bbc623474 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Diagnostics; using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticListeners; using Elastic.Apm.DiagnosticSource; using Elastic.Apm.Helpers; using Elastic.Apm.Logging; @@ -17,28 +18,16 @@ namespace Elastic.Apm.Azure.ServiceBus /// /// Creates spans for diagnostic events from Azure.Messaging.ServiceBus /// - public class AzureMessagingServiceBusDiagnosticListener: IDiagnosticListener + public class AzureMessagingServiceBusDiagnosticListener: DiagnosticListenerBase { - private readonly IApmAgent _agent; private readonly ApmAgent _realAgent; private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); - internal IApmLogger Logger { get; } + public override string Name { get; } = "Azure.Messaging.ServiceBus"; - public string Name { get; } = "Azure.Messaging.ServiceBus"; + public AzureMessagingServiceBusDiagnosticListener(IApmAgent agent) : base(agent) => _realAgent = agent as ApmAgent; - public AzureMessagingServiceBusDiagnosticListener(IApmAgent agent) - { - _agent = agent; - _realAgent = agent as ApmAgent; - Logger = _agent.Logger.Scoped(nameof(AzureMessagingServiceBusDiagnosticListener)); - } - - public void OnCompleted() => Logger.Trace()?.Log("Completed"); - - public void OnError(Exception error) => Logger.Error()?.LogExceptionWithCaller(error, nameof(OnError)); - - public void OnNext(KeyValuePair kv) + protected override void HandleOnNext(KeyValuePair kv) { Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); @@ -122,13 +111,13 @@ private void OnReceiveStart(KeyValuePair kv, string action) DistributedTracingData tracingData = null; - var transaction = _agent.Tracer.StartTransaction(transactionName, "messaging", tracingData); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging", tracingData); // transaction creation will create an activity, so use this as the key. // TODO: change when existing activity is used. var activityId = Activity.Current.Id; - transaction.Context.Service = Service.GetDefaultService(_agent.ConfigurationReader, _agent.Logger); + transaction.Context.Service = Service.GetDefaultService(ApmAgent.ConfigurationReader, ApmAgent.Logger); transaction.Context.Service.Framework = new Framework { Name = "AzureServiceBus" }; if (!_processingSegments.TryAdd(activityId, transaction)) @@ -161,7 +150,7 @@ private bool MatchesIgnoreMessageQueues(string name) private void OnSendStart(KeyValuePair kv, string action) { - var currentSegment = _agent.GetCurrentExecutionSegment(); + var currentSegment = ApmAgent.GetCurrentExecutionSegment(); if (currentSegment is null) { Logger.Trace()?.Log("No current transaction or span - exiting"); diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs index c40f25ade..34e57e144 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -9,7 +9,7 @@ using System.Diagnostics; using System.Threading.Tasks; using Elastic.Apm.Api; -using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.DiagnosticListeners; using Elastic.Apm.Helpers; using Elastic.Apm.Logging; using Elastic.Apm.Reflection; @@ -19,9 +19,8 @@ namespace Elastic.Apm.Azure.ServiceBus /// /// Creates spans for diagnostic events from Microsoft.Azure.ServiceBus /// - public class MicrosoftAzureServiceBusDiagnosticListener: IDiagnosticListener + public class MicrosoftAzureServiceBusDiagnosticListener: DiagnosticListenerBase { - private readonly IApmAgent _agent; private readonly ApmAgent _realAgent; private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); private readonly PropertyFetcherCollection _sendProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; @@ -30,22 +29,11 @@ public class MicrosoftAzureServiceBusDiagnosticListener: IDiagnosticListener private readonly PropertyFetcherCollection _receiveDeferredProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; private readonly PropertyFetcher _exceptionProperty = new PropertyFetcher("Exception"); - internal IApmLogger Logger { get; } + public override string Name { get; } = "Microsoft.Azure.ServiceBus"; - public string Name { get; } = "Microsoft.Azure.ServiceBus"; + public MicrosoftAzureServiceBusDiagnosticListener(IApmAgent agent) : base(agent) => _realAgent = agent as ApmAgent; - public MicrosoftAzureServiceBusDiagnosticListener(IApmAgent agent) - { - _agent = agent; - _realAgent = agent as ApmAgent; - Logger = _agent.Logger.Scoped(nameof(AzureMessagingServiceBusDiagnosticListener)); - } - - public void OnCompleted() => Logger.Trace()?.Log("Completed"); - - public void OnError(Exception error) => Logger.Error()?.LogExceptionWithCaller(error, nameof(OnError)); - - public void OnNext(KeyValuePair kv) + protected override void HandleOnNext(KeyValuePair kv) { Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); @@ -114,13 +102,13 @@ private void OnReceiveStart( // TODO: initialize tracing data from linked messages, once https://github.com/elastic/apm/issues/122 is finalized DistributedTracingData tracingData = null; - var transaction = _agent.Tracer.StartTransaction(transactionName, "messaging", tracingData); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging", tracingData); // transaction creation will create an activity, so use this as the key. // TODO: change when existing activity is used. var activityId = Activity.Current.Id; - transaction.Context.Service = Service.GetDefaultService(_agent.ConfigurationReader, _agent.Logger); + transaction.Context.Service = Service.GetDefaultService(ApmAgent.ConfigurationReader, ApmAgent.Logger); transaction.Context.Service.Framework = new Framework { Name = "AzureServiceBus" }; if (!_processingSegments.TryAdd(activityId, transaction)) @@ -157,7 +145,7 @@ private void OnSendStart( PropertyFetcherCollection cachedProperties ) { - var currentSegment = _agent.GetCurrentExecutionSegment(); + var currentSegment = ApmAgent.GetCurrentExecutionSegment(); if (currentSegment is null) { Logger.Trace()?.Log("No current transaction or span - exiting"); From aa66bd8e610bb0c0e5c7d8a1b27e89affe2ab977 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 17 Mar 2021 11:13:49 +1000 Subject: [PATCH 13/32] Create and reuse same Service instance for message transactions --- ...reMessagingServiceBusDiagnosticListener.cs | 19 +++++----- ...rosoftAzureServiceBusDiagnosticListener.cs | 35 +++++++------------ 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index bbc623474..6a373b691 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -22,10 +22,16 @@ public class AzureMessagingServiceBusDiagnosticListener: DiagnosticListenerBase { private readonly ApmAgent _realAgent; private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); + private readonly Service _service; public override string Name { get; } = "Azure.Messaging.ServiceBus"; - public AzureMessagingServiceBusDiagnosticListener(IApmAgent agent) : base(agent) => _realAgent = agent as ApmAgent; + public AzureMessagingServiceBusDiagnosticListener(IApmAgent agent) : base(agent) + { + _realAgent = agent as ApmAgent; + _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); + _service.Framework = new Framework { Name = "AzureServiceBus" }; + } protected override void HandleOnNext(KeyValuePair kv) { @@ -109,17 +115,13 @@ private void OnReceiveStart(KeyValuePair kv, string action) ? $"AzureServiceBus {action}" : $"AzureServiceBus {action} from {queueName}"; - DistributedTracingData tracingData = null; - - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging", tracingData); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging"); + transaction.Context.Service = _service; // transaction creation will create an activity, so use this as the key. // TODO: change when existing activity is used. var activityId = Activity.Current.Id; - transaction.Context.Service = Service.GetDefaultService(ApmAgent.ConfigurationReader, ApmAgent.Logger); - transaction.Context.Service.Framework = new Framework { Name = "AzureServiceBus" }; - if (!_processingSegments.TryAdd(activityId, transaction)) { Logger.Error()?.Log( @@ -188,7 +190,6 @@ private void OnSendStart(KeyValuePair kv, string action) : $"AzureServiceBus {action} to {queueName}"; var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", action.ToLowerInvariant()); - span.Context.Destination = new Destination { Address = destinationAddress, @@ -227,8 +228,6 @@ private void OnStop() return; } - // TODO: Get the linked Activit(y/ies) from DiagnosticActivity when https://github.com/elastic/apm/issues/122 is finalized - segment.Outcome = Outcome.Success; segment.End(); } diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs index 34e57e144..6edbff916 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -28,10 +28,16 @@ public class MicrosoftAzureServiceBusDiagnosticListener: DiagnosticListenerBase private readonly PropertyFetcherCollection _receiveProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; private readonly PropertyFetcherCollection _receiveDeferredProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; private readonly PropertyFetcher _exceptionProperty = new PropertyFetcher("Exception"); + private readonly Service _service; public override string Name { get; } = "Microsoft.Azure.ServiceBus"; - public MicrosoftAzureServiceBusDiagnosticListener(IApmAgent agent) : base(agent) => _realAgent = agent as ApmAgent; + public MicrosoftAzureServiceBusDiagnosticListener(IApmAgent agent) : base(agent) + { + _realAgent = agent as ApmAgent; + _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); + _service.Framework = new Framework { Name = "AzureServiceBus" }; + } protected override void HandleOnNext(KeyValuePair kv) { @@ -78,10 +84,7 @@ protected override void HandleOnNext(KeyValuePair kv) } } - private void OnReceiveStart( - KeyValuePair kv, - string action, - PropertyFetcherCollection cachedProperties) + private void OnReceiveStart(KeyValuePair kv, string action, PropertyFetcherCollection cachedProperties) { if (kv.Value is null) { @@ -99,18 +102,13 @@ private void OnReceiveStart( ? $"AzureServiceBus {action}" : $"AzureServiceBus {action} from {queueName}"; - // TODO: initialize tracing data from linked messages, once https://github.com/elastic/apm/issues/122 is finalized - DistributedTracingData tracingData = null; - - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging", tracingData); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging"); + transaction.Context.Service = _service; // transaction creation will create an activity, so use this as the key. // TODO: change when existing activity is used. var activityId = Activity.Current.Id; - transaction.Context.Service = Service.GetDefaultService(ApmAgent.ConfigurationReader, ApmAgent.Logger); - transaction.Context.Service.Framework = new Framework { Name = "AzureServiceBus" }; - if (!_processingSegments.TryAdd(activityId, transaction)) { Logger.Trace()?.Log( @@ -139,11 +137,7 @@ private bool MatchesIgnoreMessageQueues(string name) return false; } - private void OnSendStart( - KeyValuePair kv, - string action, - PropertyFetcherCollection cachedProperties - ) + private void OnSendStart(KeyValuePair kv, string action, PropertyFetcherCollection cachedProperties) { var currentSegment = ApmAgent.GetCurrentExecutionSegment(); if (currentSegment is null) @@ -192,9 +186,7 @@ PropertyFetcherCollection cachedProperties } } - private void OnStop( - KeyValuePair kv, - PropertyFetcherCollection cachedProperties) + private void OnStop(KeyValuePair kv, PropertyFetcherCollection cachedProperties) { var activity = Activity.Current; if (activity is null) @@ -224,8 +216,7 @@ private void OnStop( segment.End(); } - private void OnException( - KeyValuePair kv) + private void OnException(KeyValuePair kv) { var activity = Activity.Current; if (activity is null) From bb29131ebf39e9f060df7028a8d1aef6710cbdf0 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 17 Mar 2021 21:06:12 +1000 Subject: [PATCH 14/32] Run terraform locally with authenticated az account Integration tests that run azure resources will be skipped if credentials are not available. --- .gitignore | 6 +- .../Azure/AzureCredentials.cs | 144 ++++++++++-------- .../Azure/AzureCredentialsFactAttribute.cs | 21 +++ ...sagingServiceBusDiagnosticListenerTests.cs | 18 +-- ...tAzureServiceBusDiagnosticListenerTests.cs | 18 +-- .../Terraform/TerraformResources.cs | 13 +- 6 files changed, 131 insertions(+), 89 deletions(-) create mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs diff --git a/.gitignore b/.gitignore index 768915e11..97f50ba83 100644 --- a/.gitignore +++ b/.gitignore @@ -342,9 +342,9 @@ build/output/ # Generated .NET core sln file ElasticApmAgent.NetCore.sln -.credentials.json - +# Terraform configuration state files .terraform .terraform.lock.hcl terraform.tfstate -terraform.tfstate.backup \ No newline at end of file +terraform.tfstate.backup +.terraform.tfstate.lock.info \ No newline at end of file diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs index e47cd74e0..3b4b9cb82 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs @@ -5,75 +5,114 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Runtime.InteropServices; using System.Threading; -using Elastic.Apm.Tests.Utilities; using Newtonsoft.Json; +using ProcNet; namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure { - public class AzureCredentials + public class Unauthenticated : AzureCredentials { - // ReSharper disable InconsistentNaming - private const string ARM_CLIENT_ID = nameof(ARM_CLIENT_ID); - private const string ARM_CLIENT_SECRET = nameof(ARM_CLIENT_SECRET); - private const string ARM_TENANT_ID = nameof(ARM_TENANT_ID); - private const string ARM_SUBSCRIPTION_ID = nameof(ARM_SUBSCRIPTION_ID); - - private const string CredentialsJsonFile = ".credentials.json"; - // ReSharper restore InconsistentNaming + } - private static readonly Lazy _lazyCredentials = - new Lazy(LoadCredentials, LazyThreadSafetyMode.ExecutionAndPublication); + public class AzureUserAccount : AzureCredentials + { + } + public class ServicePrincipal : AzureCredentials + { [JsonConstructor] - private AzureCredentials() { } + private ServicePrincipal() { } + + [JsonProperty] + public string ClientId { get; private set; } + + [JsonProperty] + public string ClientSecret { get; private set; } - private AzureCredentials(string clientId, string clientSecret, string tenantId, string subscriptionId) + [JsonProperty] + public string TenantId { get; private set; } + + [JsonProperty] + public string SubscriptionId { get; private set; } + + public ServicePrincipal(string clientId, string clientSecret, string tenantId, string subscriptionId) { ClientId = clientId; ClientSecret = clientSecret; TenantId = tenantId; SubscriptionId = subscriptionId; } + public override void AddToArguments(StartArguments startArguments) + { + startArguments.Environment ??= new Dictionary(); + startArguments.Environment[ARM_CLIENT_ID] = ClientId; + startArguments.Environment[ARM_CLIENT_SECRET] = ClientSecret; + startArguments.Environment[ARM_SUBSCRIPTION_ID] = SubscriptionId; + startArguments.Environment[ARM_TENANT_ID] = TenantId; + } + } + + public abstract class AzureCredentials + { + // ReSharper disable InconsistentNaming + protected const string ARM_CLIENT_ID = nameof(ARM_CLIENT_ID); + protected const string ARM_CLIENT_SECRET = nameof(ARM_CLIENT_SECRET); + protected const string ARM_TENANT_ID = nameof(ARM_TENANT_ID); + protected const string ARM_SUBSCRIPTION_ID = nameof(ARM_SUBSCRIPTION_ID); + // ReSharper restore InconsistentNaming + + private static readonly Lazy _lazyCredentials = + new Lazy(LoadCredentials, LazyThreadSafetyMode.ExecutionAndPublication); private static AzureCredentials LoadCredentials() { var runningInCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_ID")); if (runningInCi) { - var clientId = GetEnvironmentVariable(ARM_CLIENT_ID); - var clientSecret = GetEnvironmentVariable(ARM_CLIENT_SECRET); - var tenantId = GetEnvironmentVariable(ARM_TENANT_ID); - var subscriptionId = GetEnvironmentVariable(ARM_SUBSCRIPTION_ID); - return new AzureCredentials(clientId, clientSecret, tenantId, subscriptionId); + var clientId = Environment.GetEnvironmentVariable(ARM_CLIENT_ID); + var clientSecret = Environment.GetEnvironmentVariable(ARM_CLIENT_SECRET); + var tenantId = Environment.GetEnvironmentVariable(ARM_TENANT_ID); + var subscriptionId = Environment.GetEnvironmentVariable(ARM_SUBSCRIPTION_ID); + + if (string.IsNullOrEmpty(clientId) || + string.IsNullOrEmpty(clientSecret) || + string.IsNullOrEmpty(tenantId) || + string.IsNullOrEmpty(subscriptionId)) + return new Unauthenticated(); + + return new ServicePrincipal(clientId, clientSecret, tenantId, subscriptionId); } - return LoadCredentialsFromFile(); - } - - private static AzureCredentials LoadCredentialsFromFile() - { - var path = Path.Combine(SolutionPaths.Root, CredentialsJsonFile); - - if (!File.Exists(path)) - throw new FileNotFoundException($"{CredentialsJsonFile} file does not exist at ${path}", CredentialsJsonFile); - - using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - using var streamReader = new StreamReader(fileStream); - using var jsonTextReader = new JsonTextReader(streamReader); - - var serializer = new JsonSerializer(); - return serializer.Deserialize(jsonTextReader); + return LoggedIntoAccountWithAzureCli() ? new AzureUserAccount() : new Unauthenticated(); } - private static string GetEnvironmentVariable(string name) + /// + /// Checks that Azure CLI is installed and in the PATH, and is logged into an account + /// + /// true if logged in + private static bool LoggedIntoAccountWithAzureCli() { - var value = Environment.GetEnvironmentVariable(name); - if (string.IsNullOrEmpty(value)) - throw new ArgumentException($"{name} environment variable is null or empty"); - - return value; + try + { + // run azure CLI using cmd on Windows so that %~dp0 in az.cmd expands to + // the path containing the cmd file. + var binary = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "cmd" + : "az"; + var args = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? new[] { "/c", "az", "account", "show" } + : new[] { "account", "show" }; + + var result = Proc.Start(new StartArguments(binary, args)); + return result.Completed && result.ExitCode == 0; + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } } /// @@ -81,25 +120,6 @@ private static string GetEnvironmentVariable(string name) /// public static AzureCredentials Instance => _lazyCredentials.Value; - [JsonProperty] - public string ClientId { get; private set; } - - [JsonProperty] - public string ClientSecret { get; private set; } - - [JsonProperty] - public string TenantId { get; private set; } - - [JsonProperty] - public string SubscriptionId { get; private set; } - - public IDictionary ToTerraformEnvironmentVariables() => - new Dictionary - { - [ARM_CLIENT_ID] = ClientId, - [ARM_CLIENT_SECRET] = ClientSecret, - [ARM_SUBSCRIPTION_ID] = SubscriptionId, - [ARM_TENANT_ID] = TenantId, - }; + public virtual void AddToArguments(StartArguments startArguments) { } } } diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs new file mode 100644 index 000000000..e486568ee --- /dev/null +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Xunit; + +namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure +{ + /// + /// Attribute applied to a test that should be run by the test runner if Azure credentials are available + /// + public class AzureCredentialsFactAttribute : FactAttribute + { + public AzureCredentialsFactAttribute() + { + if (AzureCredentials.Instance is Unauthenticated) + Skip = "Azure credentials not available. If running locally, run `az login` to login"; + } + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs index b9ef3f17f..41280021f 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -34,7 +34,7 @@ public AzureMessagingServiceBusDiagnosticListenerTests(AzureServiceBusTestEnviro _client = new ServiceBusClient(environment.ServiceBusConnectionString); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Send_To_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -63,7 +63,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Send_To_Topic() { await using var scope = await TopicScope.CreateWithTopic(_adminClient); @@ -92,7 +92,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Schedule_To_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -123,7 +123,7 @@ await sender.ScheduleMessageAsync( destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Schedule_To_Topic() { await using var scope = await TopicScope.CreateWithTopic(_adminClient); @@ -154,7 +154,7 @@ await sender.ScheduleMessageAsync( destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_Receive_From_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -176,7 +176,7 @@ await sender.SendMessageAsync( transaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_Receive_From_Topic_Subscription() { await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); @@ -199,7 +199,7 @@ await sender.SendMessageAsync( transaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_ReceiveDeferred_From_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -230,7 +230,7 @@ await sender.SendMessageAsync( secondTransaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_ReceiveDeferred_From_Topic_Subscription() { await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); @@ -260,7 +260,7 @@ await sender.SendMessageAsync( secondTransaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Does_Not_Capture_Span_When_QueueName_Matches_IgnoreMessageQueues() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs index 30400bf5d..c3b866335 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs @@ -33,7 +33,7 @@ public MicrosoftAzureServiceBusDiagnosticListenerTests(AzureServiceBusTestEnviro _adminClient = new ServiceBusAdministrationClient(environment.ServiceBusConnectionString); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Send_To_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -63,7 +63,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Send_To_Topic() { await using var scope = await TopicScope.CreateWithTopic(_adminClient); @@ -92,7 +92,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Schedule_To_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -123,7 +123,7 @@ await sender.ScheduleMessageAsync( destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Span_When_Schedule_To_Topic() { await using var scope = await TopicScope.CreateWithTopic(_adminClient); @@ -154,7 +154,7 @@ await sender.ScheduleMessageAsync( destination.Service.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_Receive_From_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -176,7 +176,7 @@ await sender.SendAsync( transaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_Receive_From_Topic_Subscription() { await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); @@ -200,7 +200,7 @@ await sender.SendAsync( transaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_ReceiveDeferred_From_Queue() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); @@ -229,7 +229,7 @@ await sender.SendAsync( secondTransaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Capture_Transaction_When_ReceiveDeferred_From_Topic_Subscription() { await using var scope = await TopicScope.CreateWithTopicAndSubscription(_adminClient); @@ -260,7 +260,7 @@ await sender.SendAsync( secondTransaction.Type.Should().Be("messaging"); } - [Fact] + [AzureCredentialsFact] public async Task Does_Not_Capture_Span_When_QueueName_Matches_IgnoreMessageQueues() { await using var scope = await QueueScope.CreateWithQueue(_adminClient); diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs index 339fcc940..c56a8a654 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs @@ -21,10 +21,11 @@ namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform /// public class TerraformResources { - private readonly string _resourceDirectory; - private readonly IDictionary _environment; private static readonly TimeSpan _defaultTimeout = TimeSpan.FromMinutes(10); - private IMessageSink _messageSink; + + private readonly string _resourceDirectory; + private readonly IMessageSink _messageSink; + private readonly AzureCredentials _credentials; public TerraformResources(string resourceDirectory, AzureCredentials credentials, IMessageSink messageSink = null) { @@ -35,7 +36,7 @@ public TerraformResources(string resourceDirectory, AzureCredentials credentials throw new DirectoryNotFoundException($"Directory does not exist {resourceDirectory}"); _resourceDirectory = resourceDirectory; - _environment = credentials.ToTerraformEnvironmentVariables(); + _credentials = credentials; _messageSink = messageSink; } @@ -43,9 +44,9 @@ private ObservableProcess CreateProcess(params string[] arguments) { var startArguments = new StartArguments("terraform", arguments) { - WorkingDirectory = _resourceDirectory, - Environment = _environment + WorkingDirectory = _resourceDirectory }; + _credentials.AddToArguments(startArguments); return new ObservableProcess(startArguments); } From d9981b921b6a57cbf71ee6f719220199231722b1 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 17 Mar 2021 21:08:31 +1000 Subject: [PATCH 15/32] delete example credentials file Not needed --- .credentials.example.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .credentials.example.json diff --git a/.credentials.example.json b/.credentials.example.json deleted file mode 100644 index 9b7e1c794..000000000 --- a/.credentials.example.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ClientId": "", - "ClientSecret": "", - "TenantId": "", - "SubscriptionId": "" -} \ No newline at end of file From f003524071d763ee35b8d2fcd24748718894ed90 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 18 Mar 2021 16:12:42 +1000 Subject: [PATCH 16/32] add table terminator --- docs/supported-technologies.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index c96b4eb53..bd145e3cb 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -133,4 +133,6 @@ Automatic instrumentation for the following cloud services 7.0.0+ for Azure.Messaging.ServiceBus | A new transaction is created for received and receive deferred messages. A new span is created for sent and scheduled messages if there's a current transaction. -| 1.9 \ No newline at end of file +| 1.9 + +|=== \ No newline at end of file From af7d1a2219ec3dce2e2cd3f2a594d4bab9f4b437 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 24 Mar 2021 14:05:41 +1000 Subject: [PATCH 17/32] Load credentials in CI from .credentials.json --- .gitignore | 5 ++- .../Azure/AzureCredentials.cs | 39 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 97f50ba83..34e8fcd7f 100644 --- a/.gitignore +++ b/.gitignore @@ -347,4 +347,7 @@ ElasticApmAgent.NetCore.sln .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup -.terraform.tfstate.lock.info \ No newline at end of file +.terraform.tfstate.lock.info + +# Azure credentials file +.credentials.json \ No newline at end of file diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs index 3b4b9cb82..1b33e2a4c 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Runtime.InteropServices; using System.Threading; +using Elastic.Apm.Tests.Utilities; using Newtonsoft.Json; using ProcNet; @@ -25,16 +27,16 @@ public class ServicePrincipal : AzureCredentials [JsonConstructor] private ServicePrincipal() { } - [JsonProperty] + [JsonProperty("clientId")] public string ClientId { get; private set; } - [JsonProperty] + [JsonProperty("clientSecret")] public string ClientSecret { get; private set; } - [JsonProperty] + [JsonProperty("tenantId")] public string TenantId { get; private set; } - [JsonProperty] + [JsonProperty("subscriptionId")] public string SubscriptionId { get; private set; } public ServicePrincipal(string clientId, string clientSecret, string tenantId, string subscriptionId) @@ -71,21 +73,28 @@ private static AzureCredentials LoadCredentials() var runningInCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_ID")); if (runningInCi) { - var clientId = Environment.GetEnvironmentVariable(ARM_CLIENT_ID); - var clientSecret = Environment.GetEnvironmentVariable(ARM_CLIENT_SECRET); - var tenantId = Environment.GetEnvironmentVariable(ARM_TENANT_ID); - var subscriptionId = Environment.GetEnvironmentVariable(ARM_SUBSCRIPTION_ID); - - if (string.IsNullOrEmpty(clientId) || - string.IsNullOrEmpty(clientSecret) || - string.IsNullOrEmpty(tenantId) || - string.IsNullOrEmpty(subscriptionId)) + var credentialsFile = Path.Combine(SolutionPaths.Root, ".credentials.json"); + if (!File.Exists(credentialsFile)) return new Unauthenticated(); - return new ServicePrincipal(clientId, clientSecret, tenantId, subscriptionId); + try + { + using var fileStream = new FileStream(credentialsFile, FileMode.Open, FileAccess.Read, FileShare.Read); + using var streamReader = new StreamReader(fileStream); + using var jsonTextReader = new JsonTextReader(streamReader); + var serializer = new JsonSerializer(); + return serializer.Deserialize(jsonTextReader); + } + catch (Exception e) + { + Console.WriteLine(e); + return new Unauthenticated(); + } } - return LoggedIntoAccountWithAzureCli() ? new AzureUserAccount() : new Unauthenticated(); + return LoggedIntoAccountWithAzureCli() + ? new AzureUserAccount() + : new Unauthenticated(); } /// From 1fb40df3fdf4761c5f3a2a59b39721eeb998ceea Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 24 Mar 2021 15:22:44 +1000 Subject: [PATCH 18/32] Add Azure Service Bus sample application --- ElasticApmAgent.sln | 7 +++ ...Elastic.Apm.Azure.ServiceBus.Sample.csproj | 17 ++++++ .../Program.cs | 58 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj create mode 100644 sample/Elastic.Apm.Azure.ServiceBus.Sample/Program.cs diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index 4f1391099..1883fd81b 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -135,6 +135,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus.Tests", "test\Elastic.Apm.Azure.ServiceBus.Tests\Elastic.Apm.Azure.ServiceBus.Tests.csproj", "{D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus.Sample", "sample\Elastic.Apm.Azure.ServiceBus.Sample\Elastic.Apm.Azure.ServiceBus.Sample.csproj", "{27563B4E-ECB1-4F1B-B9F1-22C2C165B270}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution test\Elastic.Apm.DatabaseTests.Common\Elastic.Apm.DatabaseTests.Common.projitems*{968e1e85-e996-42de-9845-d20dae16165a}*SharedItemsImports = 5 @@ -336,6 +338,10 @@ Global {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D}.Release|Any CPU.Build.0 = Release|Any CPU + {27563B4E-ECB1-4F1B-B9F1-22C2C165B270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27563B4E-ECB1-4F1B-B9F1-22C2C165B270}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27563B4E-ECB1-4F1B-B9F1-22C2C165B270}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27563B4E-ECB1-4F1B-B9F1-22C2C165B270}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -389,6 +395,7 @@ Global {B235B13F-42AE-42DA-A3C8-20D047F38685} = {267A241E-571F-458F-B04C-B6C4DE79E735} {1D43C8C5-4116-45C5-9F4B-56C1D926ED29} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D} = {267A241E-571F-458F-B04C-B6C4DE79E735} + {27563B4E-ECB1-4F1B-B9F1-22C2C165B270} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69E02FD9-C9DE-412C-AB6B-5B8BECC6BFA5} diff --git a/sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj b/sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj new file mode 100644 index 000000000..a1e58c2ee --- /dev/null +++ b/sample/Elastic.Apm.Azure.ServiceBus.Sample/Elastic.Apm.Azure.ServiceBus.Sample.csproj @@ -0,0 +1,17 @@ + + + + Exe + net5.0 + + + + + + + + + + + + diff --git a/sample/Elastic.Apm.Azure.ServiceBus.Sample/Program.cs b/sample/Elastic.Apm.Azure.ServiceBus.Sample/Program.cs new file mode 100644 index 000000000..c135f30ae --- /dev/null +++ b/sample/Elastic.Apm.Azure.ServiceBus.Sample/Program.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; + +namespace Elastic.Apm.Azure.ServiceBus.Sample +{ + internal class Program + { + private static async Task Main(string[] args) + { + if (args.Length == 0) + { + Console.Error.WriteLine("An Azure Service Bus connection string must be passed as the first argument"); + return 1; + } + + Agent.Subscribe(new AzureMessagingServiceBusDiagnosticsSubscriber()); + + var connectionString = args[0]; + var adminClient = new ServiceBusAdministrationClient(connectionString); + var client = new ServiceBusClient(connectionString); + + var queueName = Guid.NewGuid().ToString("D"); + + Console.WriteLine($"Creating queue {queueName}"); + + var response = await adminClient.CreateQueueAsync(queueName).ConfigureAwait(false); + + var sender = client.CreateSender(queueName); + + Console.WriteLine("Sending messages to queue"); + + await Agent.Tracer.CaptureTransaction("Send AzureServiceBus Messages", "messaging", async () => + { + for (var i = 0; i < 10; i++) + await sender.SendMessageAsync(new ServiceBusMessage($"test message {i}")).ConfigureAwait(false); + }); + + var receiver = client.CreateReceiver(queueName); + + Console.WriteLine("Receiving messages from queue"); + + var messages = await receiver.ReceiveMessagesAsync(9) + .ConfigureAwait(false); + + Console.WriteLine("Receiving message from queue"); + + var message = await receiver.ReceiveMessageAsync() + .ConfigureAwait(false); + + Console.WriteLine("Press any key to continue..."); + Console.ReadKey(); + + return 0; + } + } +} From 1cfac4ee0c407b034a6e6ed25387e7ec3379be97 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 24 Mar 2021 16:08:54 +1000 Subject: [PATCH 19/32] Extract service bus related values out to constants --- ...reMessagingServiceBusDiagnosticListener.cs | 47 +++++------- .../Elastic.Apm.Azure.ServiceBus.csproj | 4 ++ ...rosoftAzureServiceBusDiagnosticListener.cs | 25 +++---- .../ServiceBus.cs | 14 ++++ ...sagingServiceBusDiagnosticListenerTests.cs | 72 +++++++++---------- ...tAzureServiceBusDiagnosticListenerTests.cs | 72 +++++++++---------- 6 files changed, 118 insertions(+), 116 deletions(-) create mode 100644 src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index 6a373b691..c2686f50c 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -30,7 +30,7 @@ public AzureMessagingServiceBusDiagnosticListener(IApmAgent agent) : base(agent) { _realAgent = agent as ApmAgent; _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); - _service.Framework = new Framework { Name = "AzureServiceBus" }; + _service.Framework = new Framework { Name = ServiceBus.SegmentName }; } protected override void HandleOnNext(KeyValuePair kv) @@ -48,36 +48,24 @@ protected override void HandleOnNext(KeyValuePair kv) case "ServiceBusSender.Send.Start": OnSendStart(kv, "SEND"); break; - case "ServiceBusSender.Send.Stop": - OnStop(); - break; - case "ServiceBusSender.Send.Exception": - OnException(kv); - break; case "ServiceBusSender.Schedule.Start": OnSendStart(kv, "SCHEDULE"); break; - case "ServiceBusSender.Schedule.Stop": - OnStop(); - break; - case "ServiceBusSender.Schedule.Exception": - OnException(kv); - break; case "ServiceBusReceiver.Receive.Start": OnReceiveStart(kv, "RECEIVE"); break; - case "ServiceBusReceiver.Receive.Stop": - OnStop(); - break; - case "ServiceBusReceiver.Receive.Exception": - OnException(kv); - break; case "ServiceBusReceiver.ReceiveDeferred.Start": OnReceiveStart(kv, "RECEIVEDEFERRED"); break; + case "ServiceBusSender.Send.Stop": + case "ServiceBusSender.Schedule.Stop": + case "ServiceBusReceiver.Receive.Stop": case "ServiceBusReceiver.ReceiveDeferred.Stop": OnStop(); break; + case "ServiceBusSender.Send.Exception": + case "ServiceBusSender.Schedule.Exception": + case "ServiceBusReceiver.Receive.Exception": case "ServiceBusReceiver.ReceiveDeferred.Exception": OnException(kv); break; @@ -112,14 +100,13 @@ private void OnReceiveStart(KeyValuePair kv, string action) return; var transactionName = queueName is null - ? $"AzureServiceBus {action}" - : $"AzureServiceBus {action} from {queueName}"; + ? $"{ServiceBus.SegmentName} {action}" + : $"{ServiceBus.SegmentName} {action} from {queueName}"; - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging"); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ServiceBus.Type); transaction.Context.Service = _service; // transaction creation will create an activity, so use this as the key. - // TODO: change when existing activity is used. var activityId = Activity.Current.Id; if (!_processingSegments.TryAdd(activityId, transaction)) @@ -128,7 +115,7 @@ private void OnReceiveStart(KeyValuePair kv, string action) "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", action, transaction.Id, - activity.Id); + activityId); } } @@ -186,18 +173,18 @@ private void OnSendStart(KeyValuePair kv, string action) return; var spanName = queueName is null - ? $"AzureServiceBus {action}" - : $"AzureServiceBus {action} to {queueName}"; + ? $"{ServiceBus.SegmentName} {action}" + : $"{ServiceBus.SegmentName} {action} to {queueName}"; - var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", action.ToLowerInvariant()); + var span = currentSegment.StartSpan(spanName, ServiceBus.Type, ServiceBus.SubType, action.ToLowerInvariant()); span.Context.Destination = new Destination { Address = destinationAddress, Service = new Destination.DestinationService { - Name = "azureservicebus", - Resource = queueName is null ? "azureservicebus" : $"azureservicebus/{queueName}", - Type = "messaging" + Name = ServiceBus.SubType, + Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}", + Type = ServiceBus.Type } }; diff --git a/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj index 1c5322f46..98e2bc686 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj +++ b/src/Elastic.Apm.Azure.ServiceBus/Elastic.Apm.Azure.ServiceBus.csproj @@ -11,6 +11,10 @@ true + + + + diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs index 6edbff916..62647c49a 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -36,7 +36,7 @@ public MicrosoftAzureServiceBusDiagnosticListener(IApmAgent agent) : base(agent) { _realAgent = agent as ApmAgent; _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); - _service.Framework = new Framework { Name = "AzureServiceBus" }; + _service.Framework = new Framework { Name = ServiceBus.SegmentName }; } protected override void HandleOnNext(KeyValuePair kv) @@ -92,21 +92,18 @@ private void OnReceiveStart(KeyValuePair kv, string action, Prop return; } - var activity = Activity.Current; var queueName = cachedProperties.Fetch(kv.Value,"Entity") as string; - if (MatchesIgnoreMessageQueues(queueName)) return; var transactionName = queueName is null - ? $"AzureServiceBus {action}" - : $"AzureServiceBus {action} from {queueName}"; + ? $"{ServiceBus.SegmentName} {action}" + : $"{ServiceBus.SegmentName} {action} from {queueName}"; - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging"); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ServiceBus.Type); transaction.Context.Service = _service; // transaction creation will create an activity, so use this as the key. - // TODO: change when existing activity is used. var activityId = Activity.Current.Id; if (!_processingSegments.TryAdd(activityId, transaction)) @@ -115,7 +112,7 @@ private void OnReceiveStart(KeyValuePair kv, string action, Prop "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", action, transaction.Id, - activity.Id); + activityId); } } @@ -160,19 +157,19 @@ private void OnSendStart(KeyValuePair kv, string action, Propert return; var spanName = queueName is null - ? $"AzureServiceBus {action}" - : $"AzureServiceBus {action} to {queueName}"; + ? $"{ServiceBus.SegmentName} {action}" + : $"{ServiceBus.SegmentName} {action} to {queueName}"; - var span = currentSegment.StartSpan(spanName, "messaging", "azureservicebus", action.ToLowerInvariant()); + var span = currentSegment.StartSpan(spanName, ServiceBus.Type, ServiceBus.SubType, action.ToLowerInvariant()); span.Context.Destination = new Destination { Address = destinationAddress?.AbsoluteUri, Service = new Destination.DestinationService { - Name = "azureservicebus", - Resource = queueName is null ? "azureservicebus" : $"azureservicebus/{queueName}", - Type = "messaging" + Name = ServiceBus.SubType, + Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}", + Type = ServiceBus.Type } }; diff --git a/src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs b/src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs new file mode 100644 index 000000000..d5f5108b6 --- /dev/null +++ b/src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs @@ -0,0 +1,14 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Apm.Azure.ServiceBus +{ + internal static class ServiceBus + { + public const string SegmentName = "AzureServiceBus"; + public const string SubType = "azureservicebus"; + public const string Type = "messaging"; + } +} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs index 41280021f..32034e279 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -50,17 +50,17 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SEND to {scope.QueueName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.QueueName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -79,17 +79,17 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SEND to {scope.TopicName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.TopicName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -110,17 +110,17 @@ await sender.ScheduleMessageAsync( _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.QueueName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.QueueName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -141,17 +141,17 @@ await sender.ScheduleMessageAsync( _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.TopicName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.TopicName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -172,8 +172,8 @@ await sender.SendMessageAsync( _sender.Transactions.Should().HaveCount(1); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -195,8 +195,8 @@ await sender.SendMessageAsync( _sender.Transactions.Should().HaveCount(1); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -222,12 +222,12 @@ await sender.SendMessageAsync( _sender.Transactions.Should().HaveCount(2); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be(ServiceBus.Type); var secondTransaction = _sender.Transactions[1]; - secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.QueueName}"); - secondTransaction.Type.Should().Be("messaging"); + secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.QueueName}"); + secondTransaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -252,12 +252,12 @@ await sender.SendMessageAsync( _sender.Transactions.Should().HaveCount(2); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be(ServiceBus.Type); var secondTransaction = _sender.Transactions[1]; - secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - secondTransaction.Type.Should().Be("messaging"); + secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + secondTransaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs index c3b866335..9b5f76c7f 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs @@ -50,17 +50,17 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SEND to {scope.QueueName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.QueueName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -79,17 +79,17 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SEND to {scope.TopicName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.TopicName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -110,17 +110,17 @@ await sender.ScheduleMessageAsync( _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.QueueName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.QueueName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.QueueName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -141,17 +141,17 @@ await sender.ScheduleMessageAsync( _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureServiceBus SCHEDULE to {scope.TopicName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azureservicebus"); + span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.TopicName}"); + span.Type.Should().Be(ServiceBus.Type); + span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); - destination.Service.Name.Should().Be("azureservicebus"); - destination.Service.Resource.Should().Be($"azureservicebus/{scope.TopicName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(ServiceBus.SubType); + destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); + destination.Service.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -172,8 +172,8 @@ await sender.SendAsync( _sender.Transactions.Should().HaveCount(1); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -196,8 +196,8 @@ await sender.SendAsync( _sender.Transactions.Should().HaveCount(1); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -221,12 +221,12 @@ await sender.SendAsync( _sender.Transactions.Should().HaveCount(2); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); + transaction.Type.Should().Be(ServiceBus.Type); var secondTransaction = _sender.Transactions[1]; - secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.QueueName}"); - secondTransaction.Type.Should().Be("messaging"); + secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.QueueName}"); + secondTransaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] @@ -252,12 +252,12 @@ await sender.SendAsync( _sender.Transactions.Should().HaveCount(2); var transaction = _sender.FirstTransaction; - transaction.Name.Should().Be($"AzureServiceBus RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be("messaging"); + transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + transaction.Type.Should().Be(ServiceBus.Type); var secondTransaction = _sender.Transactions[1]; - secondTransaction.Name.Should().Be($"AzureServiceBus RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - secondTransaction.Type.Should().Be("messaging"); + secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); + secondTransaction.Type.Should().Be(ServiceBus.Type); } [AzureCredentialsFact] From 995f266891561cf4ee9ff0ded2f8a71d68c17954 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 25 Mar 2021 10:10:35 +1000 Subject: [PATCH 20/32] Add Azure Service Bus to NetCoreAll --- .../ApmMiddlewareExtension.cs | 21 +++++++++++++++---- .../Elastic.Apm.NetCoreAll.csproj | 1 + .../HostBuilderExtensions.cs | 19 +++++++++++++---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs b/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs index 6737493d5..04576f747 100644 --- a/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs +++ b/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Apm.Azure.ServiceBus; using Elastic.Apm.DiagnosticSource; using Elastic.Apm.Elasticsearch; using Elastic.Apm.EntityFrameworkCore; @@ -15,8 +16,14 @@ namespace Elastic.Apm.NetCoreAll public static class ApmMiddlewareExtension { /// - /// Adds the Elastic APM Middleware to the ASP.NET Core pipeline and enables , - /// , and . + /// Adds the Elastic APM Middleware to the ASP.NET Core pipeline and enables + /// , + /// , + /// , + /// . + /// , + /// , + /// and /// This method turns on ASP.NET Core monitoring with every other related monitoring components, for example the agent /// will also automatically trace outgoing HTTP requests and database statements. /// @@ -31,7 +38,13 @@ public static IApplicationBuilder UseAllElasticApm( this IApplicationBuilder builder, IConfiguration configuration = null ) => AspNetCore.ApmMiddlewareExtension - .UseElasticApm(builder, configuration, new HttpDiagnosticsSubscriber(), new EfCoreDiagnosticsSubscriber(), - new SqlClientDiagnosticSubscriber(), new ElasticsearchDiagnosticsSubscriber(), new GrpcClientDiagnosticSubscriber()); + .UseElasticApm(builder, configuration, + new HttpDiagnosticsSubscriber(), + new EfCoreDiagnosticsSubscriber(), + new SqlClientDiagnosticSubscriber(), + new ElasticsearchDiagnosticsSubscriber(), + new GrpcClientDiagnosticSubscriber(), + new AzureMessagingServiceBusDiagnosticsSubscriber(), + new MicrosoftAzureServiceBusDiagnosticsSubscriber()); } } diff --git a/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj b/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj index 26934ad5f..968c76b99 100644 --- a/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj +++ b/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs b/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs index e732be73e..511442be7 100644 --- a/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs +++ b/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs @@ -4,6 +4,7 @@ // See the LICENSE file in the project root for more information using Elastic.Apm.AspNetCore.DiagnosticListener; +using Elastic.Apm.Azure.ServiceBus; using Elastic.Apm.DiagnosticSource; using Elastic.Apm.Elasticsearch; using Elastic.Apm.EntityFrameworkCore; @@ -17,15 +18,25 @@ namespace Elastic.Apm.NetCoreAll public static class HostBuilderExtensions { /// - /// Register Elastic APM .NET Agent with components in the container and enables , - /// , and . + /// Register Elastic APM .NET Agent with components in the container and enables + /// , + /// , + /// , + /// , + /// . + /// , + /// , + /// and /// /// Builder. - public static IHostBuilder UseAllElasticApm(this IHostBuilder builder) => builder.UseElasticApm(new HttpDiagnosticsSubscriber(), + public static IHostBuilder UseAllElasticApm(this IHostBuilder builder) => builder.UseElasticApm( + new HttpDiagnosticsSubscriber(), new AspNetCoreDiagnosticSubscriber(), new EfCoreDiagnosticsSubscriber(), new SqlClientDiagnosticSubscriber(), new ElasticsearchDiagnosticsSubscriber(), - new GrpcClientDiagnosticSubscriber()); + new GrpcClientDiagnosticSubscriber(), + new AzureMessagingServiceBusDiagnosticsSubscriber(), + new MicrosoftAzureServiceBusDiagnosticsSubscriber()); } } From 608db100a0f6ad1e6e267acdde276abdcc126d8a Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 25 Mar 2021 10:19:19 +1000 Subject: [PATCH 21/32] Fix rebase --- .../ElasticsearchDiagnosticsListenerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs b/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs index 96a572fdd..ce4d94570 100644 --- a/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs +++ b/src/Elastic.Apm.Elasticsearch/ElasticsearchDiagnosticsListenerBase.cs @@ -50,7 +50,7 @@ internal bool TryStartElasticsearchSpan(string name, out Span span, Uri instance if (transaction == null) return false; - span = (Span)_agent.GetCurrentExecutionSegment() + span = (Span)ApmAgent.GetCurrentExecutionSegment() .StartSpan( name, ApiConstants.TypeDb, From bed17d78633f074d32eceb513664cd354dbe0ee9 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 25 Mar 2021 10:55:02 +1000 Subject: [PATCH 22/32] Install terraform for CI --- .ci/docker/sdk-linux/Dockerfile | 5 +++++ .ci/windows/test-tools.ps1 | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/.ci/docker/sdk-linux/Dockerfile b/.ci/docker/sdk-linux/Dockerfile index ff770aaad..82c6510f8 100644 --- a/.ci/docker/sdk-linux/Dockerfile +++ b/.ci/docker/sdk-linux/Dockerfile @@ -11,6 +11,11 @@ RUN /bin/bash ./dotnet-install.sh --install-dir "${DOTNET_ROOT}" -version "2.1.5 RUN /bin/bash ./dotnet-install.sh --install-dir "${DOTNET_ROOT}" -version "3.0.103" RUN /bin/bash ./dotnet-install.sh --install-dir "${DOTNET_ROOT}" -version "3.1.100" +# Install terraform +RUN curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add - \ + && apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ + && apt-get update \ + && apt-get install terraform # Install docker RUN apt update \ diff --git a/.ci/windows/test-tools.ps1 b/.ci/windows/test-tools.ps1 index 0598f015b..bb8272de4 100644 --- a/.ci/windows/test-tools.ps1 +++ b/.ci/windows/test-tools.ps1 @@ -9,3 +9,9 @@ if (!$codecov) { dotnet tool install -g Codecov.Tool --version 1.2.0 } +# Install terraform +choco install terraform -m -y --no-progress --force -r --version=0.14.8 +if ($LASTEXITCODE -ne 0) { + Write-Host "terraform installation failed." + exit 1 +} From a4aadd95e1700f55901d12506e97a20ee3b6e4cc Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 25 Mar 2021 11:02:43 +1000 Subject: [PATCH 23/32] install docker first --- .ci/docker/sdk-linux/Dockerfile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.ci/docker/sdk-linux/Dockerfile b/.ci/docker/sdk-linux/Dockerfile index 82c6510f8..8ae73a692 100644 --- a/.ci/docker/sdk-linux/Dockerfile +++ b/.ci/docker/sdk-linux/Dockerfile @@ -11,12 +11,6 @@ RUN /bin/bash ./dotnet-install.sh --install-dir "${DOTNET_ROOT}" -version "2.1.5 RUN /bin/bash ./dotnet-install.sh --install-dir "${DOTNET_ROOT}" -version "3.0.103" RUN /bin/bash ./dotnet-install.sh --install-dir "${DOTNET_ROOT}" -version "3.1.100" -# Install terraform -RUN curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add - \ - && apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ - && apt-get update \ - && apt-get install terraform - # Install docker RUN apt update \ && apt-get -qq install -y apt-transport-https ca-certificates curl \ @@ -26,4 +20,10 @@ RUN apt update \ && apt -qq update \ && apt-get -qq install -y docker-ce docker-ce-cli containerd.io \ --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* \ No newline at end of file + && rm -rf /var/lib/apt/lists/* + +# Install terraform +RUN curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add - \ + && apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ + && apt-get update \ + && apt-get install terraform \ No newline at end of file From 5afeb852ca8793301c465fb3458d9762a3fa049b Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 30 Mar 2021 16:22:40 +1000 Subject: [PATCH 24/32] Address PR feedback --- ...reMessagingServiceBusDiagnosticListener.cs | 13 ++++----- ...rosoftAzureServiceBusDiagnosticListener.cs | 6 ++-- .../ServiceBus.cs | 1 - src/Elastic.Apm/Api/ApiConstants.cs | 1 + .../CentralConfig/CentralConfigFetcher.cs | 2 +- .../CentralConfig/CentralConfigReader.cs | 4 +++ .../CentralConfigResponseParser.cs | 12 +++++--- .../Config/AbstractConfigurationReader.cs | 7 +++-- ...sagingServiceBusDiagnosticListenerTests.cs | 29 ++++++++++--------- ...tAzureServiceBusDiagnosticListenerTests.cs | 29 ++++++++++--------- 10 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index c2686f50c..219c4467a 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -22,15 +22,14 @@ public class AzureMessagingServiceBusDiagnosticListener: DiagnosticListenerBase { private readonly ApmAgent _realAgent; private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); - private readonly Service _service; + private readonly Framework _framework; public override string Name { get; } = "Azure.Messaging.ServiceBus"; public AzureMessagingServiceBusDiagnosticListener(IApmAgent agent) : base(agent) { _realAgent = agent as ApmAgent; - _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); - _service.Framework = new Framework { Name = ServiceBus.SegmentName }; + _framework = new Framework { Name = ServiceBus.SegmentName }; } protected override void HandleOnNext(KeyValuePair kv) @@ -103,8 +102,8 @@ private void OnReceiveStart(KeyValuePair kv, string action) ? $"{ServiceBus.SegmentName} {action}" : $"{ServiceBus.SegmentName} {action} from {queueName}"; - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ServiceBus.Type); - transaction.Context.Service = _service; + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ApiConstants.TypeMessaging); + transaction.Context.Service = new Service(null, null) { Framework = _framework }; // transaction creation will create an activity, so use this as the key. var activityId = Activity.Current.Id; @@ -176,7 +175,7 @@ private void OnSendStart(KeyValuePair kv, string action) ? $"{ServiceBus.SegmentName} {action}" : $"{ServiceBus.SegmentName} {action} to {queueName}"; - var span = currentSegment.StartSpan(spanName, ServiceBus.Type, ServiceBus.SubType, action.ToLowerInvariant()); + var span = currentSegment.StartSpan(spanName, ApiConstants.TypeMessaging, ServiceBus.SubType, action.ToLowerInvariant()); span.Context.Destination = new Destination { Address = destinationAddress, @@ -184,7 +183,7 @@ private void OnSendStart(KeyValuePair kv, string action) { Name = ServiceBus.SubType, Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}", - Type = ServiceBus.Type + Type = ApiConstants.TypeMessaging } }; diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs index 62647c49a..781467911 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -100,7 +100,7 @@ private void OnReceiveStart(KeyValuePair kv, string action, Prop ? $"{ServiceBus.SegmentName} {action}" : $"{ServiceBus.SegmentName} {action} from {queueName}"; - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ServiceBus.Type); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ApiConstants.TypeMessaging); transaction.Context.Service = _service; // transaction creation will create an activity, so use this as the key. @@ -160,7 +160,7 @@ private void OnSendStart(KeyValuePair kv, string action, Propert ? $"{ServiceBus.SegmentName} {action}" : $"{ServiceBus.SegmentName} {action} to {queueName}"; - var span = currentSegment.StartSpan(spanName, ServiceBus.Type, ServiceBus.SubType, action.ToLowerInvariant()); + var span = currentSegment.StartSpan(spanName, ApiConstants.TypeMessaging, ServiceBus.SubType, action.ToLowerInvariant()); span.Context.Destination = new Destination { @@ -169,7 +169,7 @@ private void OnSendStart(KeyValuePair kv, string action, Propert { Name = ServiceBus.SubType, Resource = queueName is null ? ServiceBus.SubType : $"{ServiceBus.SubType}/{queueName}", - Type = ServiceBus.Type + Type = ApiConstants.TypeMessaging } }; diff --git a/src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs b/src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs index d5f5108b6..45c9a80cc 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/ServiceBus.cs @@ -9,6 +9,5 @@ internal static class ServiceBus { public const string SegmentName = "AzureServiceBus"; public const string SubType = "azureservicebus"; - public const string Type = "messaging"; } } diff --git a/src/Elastic.Apm/Api/ApiConstants.cs b/src/Elastic.Apm/Api/ApiConstants.cs index 0135c1396..e6984cdea 100644 --- a/src/Elastic.Apm/Api/ApiConstants.cs +++ b/src/Elastic.Apm/Api/ApiConstants.cs @@ -23,5 +23,6 @@ public struct ApiConstants public const string TypeDb = "db"; public const string TypeExternal = "external"; + public const string TypeMessaging = "messaging"; } } diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs index a1414805f..f5fec2a99 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigFetcher.cs @@ -266,7 +266,7 @@ internal WrappingConfigSnapshot(IConfigSnapshot wrapped, CentralConfigReader cen public string HostName => _wrapped.HostName; - public IReadOnlyList IgnoreMessageQueues => _wrapped.IgnoreMessageQueues; + public IReadOnlyList IgnoreMessageQueues => _centralConfig.IgnoreMessageQueues ?? _wrapped.IgnoreMessageQueues; public LogLevel LogLevel => _centralConfig.LogLevel ?? _wrapped.LogLevel; diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigReader.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigReader.cs index cff86567a..28464e1a4 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigReader.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigReader.cs @@ -35,6 +35,8 @@ public CentralConfigReader(IApmLogger logger, CentralConfigResponseParser.Centra internal string ETag { get; } + internal IReadOnlyList IgnoreMessageQueues { get; private set; } + internal LogLevel? LogLevel { get; private set; } internal bool? Recording { get; private set; } @@ -71,6 +73,8 @@ private void UpdateConfigurationValues() GetConfigurationValue(CentralConfigResponseParser.CentralConfigPayload.SanitizeFieldNames, ParseSanitizeFieldNames); TransactionIgnoreUrls = GetConfigurationValue(CentralConfigResponseParser.CentralConfigPayload.TransactionIgnoreUrls, ParseTransactionIgnoreUrls); + IgnoreMessageQueues = + GetConfigurationValue(CentralConfigResponseParser.CentralConfigPayload.IgnoreMessageQueues, ParseIgnoreMessageQueuesImpl); } private ConfigurationKeyValue BuildKv(string key, string value) => diff --git a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigResponseParser.cs b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigResponseParser.cs index b304f6a4b..1a86c6451 100644 --- a/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigResponseParser.cs +++ b/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigResponseParser.cs @@ -136,8 +136,8 @@ internal class CentralConfigPayload { internal const string CaptureBodyContentTypesKey = "capture_body_content_types"; internal const string CaptureBodyKey = "capture_body"; - internal const string CaptureHeadersKey = "capture_headers"; + internal const string IgnoreMessageQueues = "ignore_message_queues"; internal const string LogLevelKey = "log_level"; internal const string Recording = "recording"; internal const string SanitizeFieldNames = "sanitize_field_names"; @@ -151,12 +151,16 @@ internal class CentralConfigPayload { CaptureBodyKey, CaptureBodyContentTypesKey, - TransactionMaxSpansKey, - TransactionSampleRateKey, CaptureHeadersKey, + IgnoreMessageQueues, LogLevelKey, + Recording, + SanitizeFieldNames, SpanFramesMinDurationKey, - StackTraceLimitKey + StackTraceLimitKey, + TransactionIgnoreUrls, + TransactionMaxSpansKey, + TransactionSampleRateKey, }; private readonly IDictionary _keyValues; diff --git a/src/Elastic.Apm/Config/AbstractConfigurationReader.cs b/src/Elastic.Apm/Config/AbstractConfigurationReader.cs index eec63c988..cac132ac1 100644 --- a/src/Elastic.Apm/Config/AbstractConfigurationReader.cs +++ b/src/Elastic.Apm/Config/AbstractConfigurationReader.cs @@ -135,14 +135,15 @@ protected IReadOnlyList ParseIgnoreMessageQueues(ConfigurationK _cachedWildcardMatchersIgnoreMessageQueues.IfNotInited?.InitOrGet(() => ParseIgnoreMessageQueuesImpl(kv)) ?? _cachedWildcardMatchersIgnoreMessageQueues.Value; - private IReadOnlyList ParseIgnoreMessageQueuesImpl(ConfigurationKeyValue kv) + internal IReadOnlyList ParseIgnoreMessageQueuesImpl(ConfigurationKeyValue kv) { - if (kv?.Value == null) return DefaultValues.IgnoreMessageQueues; + if (kv?.Value == null || string.IsNullOrWhiteSpace(kv.Value)) + return DefaultValues.IgnoreMessageQueues; try { _logger?.Trace()?.Log("Try parsing IgnoreMessageQueues, values: {IgnoreMessageQueues}", kv.Value); - var ignoreMessageQueues = kv.Value.Split(',').Where(n => !string.IsNullOrEmpty(n)).ToList(); + var ignoreMessageQueues = kv.Value.Split(',').Where(n => !string.IsNullOrWhiteSpace(n)).ToList(); var retVal = new List(ignoreMessageQueues.Count); foreach (var item in ignoreMessageQueues) retVal.Add(WildcardMatcher.ValueOf(item.Trim())); diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs index 32034e279..942048535 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Azure.Messaging.ServiceBus.Administration; +using Elastic.Apm.Api; using Elastic.Apm.Azure.ServiceBus.Tests.Azure; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; @@ -51,7 +52,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.QueueName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); @@ -60,7 +61,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -80,7 +81,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.TopicName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); @@ -89,7 +90,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -111,7 +112,7 @@ await sender.ScheduleMessageAsync( var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.QueueName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); @@ -120,7 +121,7 @@ await sender.ScheduleMessageAsync( destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -142,7 +143,7 @@ await sender.ScheduleMessageAsync( var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.TopicName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); @@ -151,7 +152,7 @@ await sender.ScheduleMessageAsync( destination.Address.Should().Be(_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -173,7 +174,7 @@ await sender.SendMessageAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -196,7 +197,7 @@ await sender.SendMessageAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -223,11 +224,11 @@ await sender.SendMessageAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); var secondTransaction = _sender.Transactions[1]; secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.QueueName}"); - secondTransaction.Type.Should().Be(ServiceBus.Type); + secondTransaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -253,11 +254,11 @@ await sender.SendMessageAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); var secondTransaction = _sender.Transactions[1]; secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - secondTransaction.Type.Should().Be(ServiceBus.Type); + secondTransaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs index 9b5f76c7f..1c281225c 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs @@ -2,6 +2,7 @@ using System.Text; using System.Threading.Tasks; using Azure.Messaging.ServiceBus.Administration; +using Elastic.Apm.Api; using Elastic.Apm.Azure.ServiceBus.Tests.Azure; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; @@ -51,7 +52,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.QueueName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); @@ -60,7 +61,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -80,7 +81,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SEND to {scope.TopicName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("send"); span.Context.Destination.Should().NotBeNull(); @@ -89,7 +90,7 @@ await _agent.Tracer.CaptureTransaction("Send AzureServiceBus Message", "message" destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -111,7 +112,7 @@ await sender.ScheduleMessageAsync( var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.QueueName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); @@ -120,7 +121,7 @@ await sender.ScheduleMessageAsync( destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.QueueName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -142,7 +143,7 @@ await sender.ScheduleMessageAsync( var span = _sender.FirstSpan; span.Name.Should().Be($"{ServiceBus.SegmentName} SCHEDULE to {scope.TopicName}"); - span.Type.Should().Be(ServiceBus.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(ServiceBus.SubType); span.Action.Should().Be("schedule"); span.Context.Destination.Should().NotBeNull(); @@ -151,7 +152,7 @@ await sender.ScheduleMessageAsync( destination.Address.Should().Be($"sb://{_environment.ServiceBusConnectionStringProperties.FullyQualifiedNamespace}/"); destination.Service.Name.Should().Be(ServiceBus.SubType); destination.Service.Resource.Should().Be($"{ServiceBus.SubType}/{scope.TopicName}"); - destination.Service.Type.Should().Be(ServiceBus.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -173,7 +174,7 @@ await sender.SendAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -197,7 +198,7 @@ await sender.SendAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -222,11 +223,11 @@ await sender.SendAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.QueueName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); var secondTransaction = _sender.Transactions[1]; secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.QueueName}"); - secondTransaction.Type.Should().Be(ServiceBus.Type); + secondTransaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] @@ -253,11 +254,11 @@ await sender.SendAsync( var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVE from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - transaction.Type.Should().Be(ServiceBus.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); var secondTransaction = _sender.Transactions[1]; secondTransaction.Name.Should().Be($"{ServiceBus.SegmentName} RECEIVEDEFERRED from {scope.TopicName}/Subscriptions/{scope.SubscriptionName}"); - secondTransaction.Type.Should().Be(ServiceBus.Type); + secondTransaction.Type.Should().Be(ApiConstants.TypeMessaging); } [AzureCredentialsFact] From 52fad70956b7d96230860ec99a74063d4f3993ac Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 6 Apr 2021 14:31:47 +1000 Subject: [PATCH 25/32] Address PR feedback round 2 --- .ci/linux/deploy.sh | 3 +- docs/configuration.asciidoc | 2 +- .../AspNetCoreDiagnosticListener.cs | 1 - ...reMessagingServiceBusDiagnosticListener.cs | 3 +- ...rosoftAzureServiceBusDiagnosticListener.cs | 9 ++-- .../CentralConfigFetcherTests.cs | 47 ++++++++++++++++++- 6 files changed, 54 insertions(+), 11 deletions(-) diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 4357c80ef..83f69ce75 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -18,7 +18,8 @@ declare -a projectsToPublish=( "Elastic.Apm.Extensions.Hosting" "Elastic.Apm.GrpcClient" "Elastic.Apm.Extensions.Logging" -"Elastic.Apm.StackExchange.Redis") +"Elastic.Apm.StackExchange.Redis" +"Elastic.Apm.Azure.ServiceBus") for project in "${projectsToPublish[@]}" do diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 2d3c85126..2e832a608 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -897,7 +897,7 @@ When this setting is `true`, the agent will also add the header `elasticapm-trac [float] [[config-ignore-message-queues]] -==== `IgnoreMessageQueues` (added[1.9]) +==== `IgnoreMessageQueues` (added[1.10]) Used to filter out specific messaging queues/topics/exchanges from being traced. When set, sends-to and receives-from the specified queues/topics/exchanges will be ignored. diff --git a/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs b/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs index 06965ce6f..2f745e029 100644 --- a/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs +++ b/src/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs @@ -9,7 +9,6 @@ using Elastic.Apm.Api; using Elastic.Apm.AspNetCore.Extensions; using Elastic.Apm.DiagnosticListeners; -using Elastic.Apm.Helpers; using Elastic.Apm.Logging; using Elastic.Apm.Model; using Elastic.Apm.Reflection; diff --git a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs index 219c4467a..092b82d37 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/AzureMessagingServiceBusDiagnosticListener.cs @@ -9,7 +9,6 @@ using System.Diagnostics; using Elastic.Apm.Api; using Elastic.Apm.DiagnosticListeners; -using Elastic.Apm.DiagnosticSource; using Elastic.Apm.Helpers; using Elastic.Apm.Logging; @@ -18,7 +17,7 @@ namespace Elastic.Apm.Azure.ServiceBus /// /// Creates spans for diagnostic events from Azure.Messaging.ServiceBus /// - public class AzureMessagingServiceBusDiagnosticListener: DiagnosticListenerBase + internal class AzureMessagingServiceBusDiagnosticListener: DiagnosticListenerBase { private readonly ApmAgent _realAgent; private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); diff --git a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs index 781467911..b45f980b7 100644 --- a/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.ServiceBus/MicrosoftAzureServiceBusDiagnosticListener.cs @@ -19,7 +19,7 @@ namespace Elastic.Apm.Azure.ServiceBus /// /// Creates spans for diagnostic events from Microsoft.Azure.ServiceBus /// - public class MicrosoftAzureServiceBusDiagnosticListener: DiagnosticListenerBase + internal class MicrosoftAzureServiceBusDiagnosticListener: DiagnosticListenerBase { private readonly ApmAgent _realAgent; private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); @@ -28,15 +28,14 @@ public class MicrosoftAzureServiceBusDiagnosticListener: DiagnosticListenerBase private readonly PropertyFetcherCollection _receiveProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; private readonly PropertyFetcherCollection _receiveDeferredProperties = new PropertyFetcherCollection { "Entity", "Endpoint", "Status" }; private readonly PropertyFetcher _exceptionProperty = new PropertyFetcher("Exception"); - private readonly Service _service; + private readonly Framework _framework; public override string Name { get; } = "Microsoft.Azure.ServiceBus"; public MicrosoftAzureServiceBusDiagnosticListener(IApmAgent agent) : base(agent) { _realAgent = agent as ApmAgent; - _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); - _service.Framework = new Framework { Name = ServiceBus.SegmentName }; + _framework = new Framework { Name = ServiceBus.SegmentName }; } protected override void HandleOnNext(KeyValuePair kv) @@ -101,7 +100,7 @@ private void OnReceiveStart(KeyValuePair kv, string action, Prop : $"{ServiceBus.SegmentName} {action} from {queueName}"; var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ApiConstants.TypeMessaging); - transaction.Context.Service = _service; + transaction.Context.Service = new Service(null, null) { Framework = _framework }; // transaction creation will create an activity, so use this as the key. var activityId = Activity.Current.Id; diff --git a/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs b/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs index 9529168ed..c7df9289a 100644 --- a/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs +++ b/test/Elastic.Apm.Tests/BackendCommTests/CentralConfig/CentralConfigFetcherTests.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System; -using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -132,6 +132,51 @@ public void Should_Not_Update_Logger_That_Is_Not_ILogLevelSwitchable() testLogger.LogLevelSwitch.Level.Should().Be(LogLevel.Trace); } + [Fact] + public void Should_Update_IgnoreMessageQueues_Configuration() + { + var configSnapshotFromReader = new MockConfigSnapshot(LoggerBase, ignoreMessageQueues: ""); + var configStore = new ConfigStore(configSnapshotFromReader, LoggerBase); + + configStore.CurrentSnapshot.IgnoreMessageQueues.Should().BeEmpty(); + + var service = Service.GetDefaultService(configSnapshotFromReader, LoggerBase); + var waitHandle = new ManualResetEvent(false); + var handler = new MockHttpMessageHandler(); + var configUrl = BackendCommUtils.ApmServerEndpoints + .BuildGetConfigAbsoluteUrl(configSnapshotFromReader.ServerUrl, service); + + handler.When(configUrl.AbsoluteUri) + .Respond(_ => + { + waitHandle.Set(); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Headers = { ETag = new EntityTagHeaderValue("\"etag\"") }, + Content = new StringContent("{ \"ignore_message_queues\": \"foo\" }", Encoding.UTF8) + }; + }); + + var centralConfigFetcher = new CentralConfigFetcher(LoggerBase, configStore, service, handler); + + using var agent = new ApmAgent(new TestAgentComponents(LoggerBase, + centralConfigFetcher: centralConfigFetcher, + payloadSender: new NoopPayloadSender())); + + centralConfigFetcher.IsRunning.Should().BeTrue(); + waitHandle.WaitOne(); + + // wait up to 60 seconds for configuration to change. Change can often be slower in CI + var count = 0; + while (count < 60 && !configStore.CurrentSnapshot.IgnoreMessageQueues.Any()) + { + count++; + Thread.Sleep(TimeSpan.FromSeconds(1)); + } + + configStore.CurrentSnapshot.IgnoreMessageQueues.Should().NotBeEmpty().And.Contain(m => m.GetMatcher() == "foo"); + } + [Fact] public void Dispose_stops_the_thread() { From ee060bf7b2424d8cd5b47434a98e8666f71911e2 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 22 Mar 2021 18:31:40 +1000 Subject: [PATCH 26/32] Capture send and receive messages with Azure Queue Storage This commit captures sending messages to, and receiving messages from Azure Queue Storage. Move common functionality for working with Terraform and Azure credentials to Elastic.Apm.Test.Utilities. --- ElasticApmAgent.sln | 14 + .../terraform/azure/storage/test_resources.tf | 76 ++++++ .../AzureQueueStorageDiagnosticListener.cs | 246 ++++++++++++++++++ .../AzureQueueStorageDiagnosticsSubscriber.cs | 34 +++ .../Elastic.Apm.Azure.Storage.csproj | 11 + src/Elastic.Apm/Elastic.Apm.csproj | 2 + .../Azure/AzureServiceBusTestEnvironment.cs | 10 +- ...sagingServiceBusDiagnosticListenerTests.cs | 1 + ...tAzureServiceBusDiagnosticListenerTests.cs | 1 + ...zureQueueStorageDiagnosticListenerTests.cs | 106 ++++++++ .../AzureStorageTestEnvironment.cs | 118 +++++++++ .../Elastic.Apm.Azure.Storage.Tests.csproj | 29 +++ .../Azure/AzureCredentials.cs | 14 +- .../Azure/AzureCredentialsFactAttribute.cs | 2 +- .../Elastic.Apm.Tests.Utilities.csproj | 2 + .../Terraform/TerraformResourceException.cs | 4 +- .../Terraform/TerraformResources.cs | 6 +- 17 files changed, 665 insertions(+), 11 deletions(-) create mode 100644 build/terraform/azure/storage/test_resources.tf create mode 100644 src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs create mode 100644 src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticsSubscriber.cs create mode 100644 src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj create mode 100644 test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs create mode 100644 test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs create mode 100644 test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Tests.Utilities}/Azure/AzureCredentials.cs (87%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Tests.Utilities}/Azure/AzureCredentialsFactAttribute.cs (89%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Tests.Utilities}/Terraform/TerraformResourceException.cs (84%) rename test/{Elastic.Apm.Azure.ServiceBus.Tests => Elastic.Apm.Tests.Utilities}/Terraform/TerraformResources.cs (93%) diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index 1883fd81b..a69519cda 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -137,6 +137,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.ServiceBus.Sample", "sample\Elastic.Apm.Azure.ServiceBus.Sample\Elastic.Apm.Azure.ServiceBus.Sample.csproj", "{27563B4E-ECB1-4F1B-B9F1-22C2C165B270}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.Storage", "src\Elastic.Apm.Azure.Storage\Elastic.Apm.Azure.Storage.csproj", "{E9C84C9D-7BEB-49E2-A955-04AD999C4266}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Azure.Storage.Tests", "test\Elastic.Apm.Azure.Storage.Tests\Elastic.Apm.Azure.Storage.Tests.csproj", "{37BD6194-A47B-4D17-BB9A-642E8909DED9}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution test\Elastic.Apm.DatabaseTests.Common\Elastic.Apm.DatabaseTests.Common.projitems*{968e1e85-e996-42de-9845-d20dae16165a}*SharedItemsImports = 5 @@ -342,6 +346,14 @@ Global {27563B4E-ECB1-4F1B-B9F1-22C2C165B270}.Debug|Any CPU.Build.0 = Debug|Any CPU {27563B4E-ECB1-4F1B-B9F1-22C2C165B270}.Release|Any CPU.ActiveCfg = Release|Any CPU {27563B4E-ECB1-4F1B-B9F1-22C2C165B270}.Release|Any CPU.Build.0 = Release|Any CPU + {E9C84C9D-7BEB-49E2-A955-04AD999C4266}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9C84C9D-7BEB-49E2-A955-04AD999C4266}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9C84C9D-7BEB-49E2-A955-04AD999C4266}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9C84C9D-7BEB-49E2-A955-04AD999C4266}.Release|Any CPU.Build.0 = Release|Any CPU + {37BD6194-A47B-4D17-BB9A-642E8909DED9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37BD6194-A47B-4D17-BB9A-642E8909DED9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37BD6194-A47B-4D17-BB9A-642E8909DED9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37BD6194-A47B-4D17-BB9A-642E8909DED9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -396,6 +408,8 @@ Global {1D43C8C5-4116-45C5-9F4B-56C1D926ED29} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} {D9CC53B2-5F6B-434B-8689-2350F3A9FB2D} = {267A241E-571F-458F-B04C-B6C4DE79E735} {27563B4E-ECB1-4F1B-B9F1-22C2C165B270} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {E9C84C9D-7BEB-49E2-A955-04AD999C4266} = {3734A52F-2222-454B-BF58-1BA5C1F29D77} + {37BD6194-A47B-4D17-BB9A-642E8909DED9} = {267A241E-571F-458F-B04C-B6C4DE79E735} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69E02FD9-C9DE-412C-AB6B-5B8BECC6BFA5} diff --git a/build/terraform/azure/storage/test_resources.tf b/build/terraform/azure/storage/test_resources.tf new file mode 100644 index 000000000..339f2ac2d --- /dev/null +++ b/build/terraform/azure/storage/test_resources.tf @@ -0,0 +1,76 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=2.46.0" + } + } +} + +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" { +} + +resource "random_uuid" "variables" { +} + +variable "resource_group" { + type = string + description = "The name of the resource group to create" +} + +variable "location" { + type = string + description = "The Azure location in which to deploy resources" + default = "westus" +} + +variable "storage_account_name" { + type = string + description = "The name of the storage account to create" +} + + +resource "azurerm_resource_group" "storage_resource_group" { + name = var.resource_group + location = var.location +} + +resource "azurerm_storage_account" "storage_account" { + name = var.storage_account_name + resource_group_name = azurerm_resource_group.storage_resource_group.name + location = azurerm_resource_group.storage_resource_group.location + account_tier = "Standard" + account_replication_type = "LRS" + enable_https_traffic_only = true +} + +# random name to generate for the contributor role assignment +resource "random_uuid" "contributor_role" { + keepers = { + client_id = data.azurerm_client_config.current.client_id + } +} + +resource "azurerm_role_assignment" "contributor_role" { + name = random_uuid.contributor_role.result + principal_id = data.azurerm_client_config.current.object_id + role_definition_name = "Contributor" + scope = azurerm_resource_group.storage_resource_group.id + depends_on = [azurerm_storage_account.storage_account] +} + + +# following role assignment, there can be a delay of up to ~1 minute +# for the assignments to propagate in Azure. You may need to introduce +# a wait before using the Azure resources created. + +output "connection_string" { + value = azurerm_storage_account.storage_account.primary_connection_string + description = "The service bus primary connection string" + sensitive = true +} + diff --git a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs new file mode 100644 index 000000000..eaab8c592 --- /dev/null +++ b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs @@ -0,0 +1,246 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticListeners; +using Elastic.Apm.Helpers; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Azure.Storage +{ + /// + /// Creates transactions and spans for diagnostic events from Azure.Storage.Queues + /// + public class AzureQueueStorageDiagnosticListener : DiagnosticListenerBase + { + private readonly ApmAgent _realAgent; + private readonly Service _service; + private readonly ConcurrentDictionary _processingSegments = + new ConcurrentDictionary(); + + public AzureQueueStorageDiagnosticListener(IApmAgent agent) : base(agent) + { + _realAgent = agent as ApmAgent; + _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); + _service.Framework = new Framework { Name = "AzureQueue" }; + } + + public override string Name { get; } = "Azure.Storage.Queues"; + + protected override void HandleOnNext(KeyValuePair kv) + { + Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); + + if (string.IsNullOrEmpty(kv.Key)) + { + Logger.Trace()?.Log($"Key is {(kv.Key == null ? "null" : "an empty string")} - exiting"); + return; + } + + switch (kv.Key) + { + case "QueueClient.ReceiveMessage.Start": + case "QueueClient.ReceiveMessages.Start": + OnReceiveStart(kv); + break; + case "QueueClient.SendMessage.Start": + OnSendStart(kv); + break; + case "QueueClient.ReceiveMessage.Stop": + case "QueueClient.ReceiveMessages.Stop": + case "QueueClient.SendMessage.Stop": + OnStop(); + break; + case "QueueClient.ReceiveMessage.Exception": + case "QueueClient.ReceiveMessages.Exception": + case "QueueClient.SendMessage.Exception": + OnException(kv); + break; + + default: + Logger.Trace()?.Log("`{DiagnosticEventKey}' key is not a traced diagnostic event", kv.Key); + break; + } + } + + private void OnSendStart(KeyValuePair kv) + { + var currentSegment = ApmAgent.GetCurrentExecutionSegment(); + if (currentSegment is null) + { + Logger.Trace()?.Log("No current transaction or span - exiting"); + return; + } + + if (!(kv.Value is Activity activity)) + { + Logger.Trace()?.Log("Value is not an activity - exiting"); + return; + } + + string queueName = null; + string destinationAddress = null; + + var urlTag = activity.Tags.FirstOrDefault(t => t.Key == "url").Value; + if (!string.IsNullOrEmpty(urlTag)) + { + var queueUrl = new QueueUrl(urlTag); + queueName = queueUrl.QueueName; + destinationAddress = queueUrl.FullyQualifiedNamespace; + } + + if (MatchesIgnoreMessageQueues(queueName)) + return; + + var spanName = queueName is null + ? $"AzureQueue SEND" + : $"AzureQueue SEND to {queueName}"; + + var span = currentSegment.StartSpan(spanName, "messaging", "azurequeue", "send"); + span.Context.Destination = new Destination + { + Address = destinationAddress, + Service = new Destination.DestinationService + { + Name = "azurequeue", + Resource = queueName is null ? "azurequeue" : $"azurequeue/{queueName}", + Type = "messaging" + } + }; + + if (!_processingSegments.TryAdd(activity.Id, span)) + { + Logger.Trace()?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + "SEND", + span.Id, + activity.Id); + } + } + + private void OnReceiveStart(KeyValuePair kv) + { + if (!(kv.Value is Activity activity)) + { + Logger.Trace()?.Log("Value is not an activity - exiting"); + return; + } + + var urlTag = activity.Tags.FirstOrDefault(t => t.Key == "url").Value; + var queueName = !string.IsNullOrEmpty(urlTag) + ? new QueueUrl(urlTag).QueueName + : null; + + if (MatchesIgnoreMessageQueues(queueName)) + return; + + var transactionName = queueName is null + ? $"AzureQueue RECEIVE" + : $"AzureQueue RECEIVE from {queueName}"; + + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging"); + transaction.Context.Service = _service; + + // transaction creation will create an activity, so use this as the key. + // TODO: change when existing activity is used. + var activityId = Activity.Current.Id; + + if (!_processingSegments.TryAdd(activityId, transaction)) + { + Logger.Error()?.Log( + "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", + "RECEIVE", + transaction.Id, + activity.Id); + } + } + + private bool MatchesIgnoreMessageQueues(string name) + { + if (name != null && _realAgent != null) + { + var matcher = WildcardMatcher.AnyMatch(_realAgent.ConfigStore.CurrentSnapshot.IgnoreMessageQueues, name); + if (matcher != null) + { + Logger.Debug()?.Log( + "Not tracing message from {QueueName} because it matched IgnoreMessageQueues pattern {Matcher}", + name, + matcher.GetMatcher()); + return true; + } + } + + return false; + } + + private void OnStop() + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); + return; + } + + segment.Outcome = Outcome.Success; + segment.End(); + } + + private void OnException(KeyValuePair kv) + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); + return; + } + + if (kv.Value is Exception e) + segment.CaptureException(e); + + segment.Outcome = Outcome.Failure; + segment.End(); + } + + /// + /// Working with a queue url to extract the queue name and address. + /// + private class QueueUrl + { + private readonly UriBuilder _builder; + + public QueueUrl(string url) => _builder = new UriBuilder(url); + + public string QueueName => _builder.Uri.Segments.Length > 2 + ? _builder.Uri.Segments[1].TrimEnd('/') + : null; + + public string FullyQualifiedNamespace => _builder.Uri.GetLeftPart(UriPartial.Authority) + "/"; + } + } + + +} diff --git a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticsSubscriber.cs new file mode 100644 index 000000000..5d45c9db9 --- /dev/null +++ b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticsSubscriber.cs @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Diagnostics; +using Elastic.Apm.DiagnosticSource; + +namespace Elastic.Apm.Azure.Storage +{ + /// + /// Subscribes to diagnostic source events from Azure.Storage.Queues + /// + public class AzureQueueStorageDiagnosticsSubscriber : IDiagnosticsSubscriber + { + /// + /// Subscribes diagnostic source events. + /// + public IDisposable Subscribe(IApmAgent agent) + { + var retVal = new CompositeDisposable(); + + var initializer = new DiagnosticInitializer(agent.Logger, new[] { new AzureQueueStorageDiagnosticListener(agent) }); + retVal.Add(initializer); + + retVal.Add(DiagnosticListener + .AllListeners + .Subscribe(initializer)); + + return retVal; + } + } +} diff --git a/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj b/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj new file mode 100644 index 000000000..593e18099 --- /dev/null +++ b/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index cf704d881..9716a8b9c 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -46,6 +46,8 @@ + + diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs index efaf37fbc..bec3d60b4 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs @@ -7,8 +7,9 @@ using System.Collections.Generic; using System.IO; using Azure.Messaging.ServiceBus; -using Elastic.Apm.Azure.ServiceBus.Tests.Terraform; using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Azure; +using Elastic.Apm.Tests.Utilities.Terraform; using Xunit; using Xunit.Abstractions; @@ -17,7 +18,6 @@ namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure [CollectionDefinition("AzureServiceBus")] public class AzureServiceBusTestEnvironmentCollection : ICollectionFixture { - } /// @@ -39,6 +39,10 @@ public AzureServiceBusTestEnvironment(IMessageSink messageSink) var terraformResourceDirectory = Path.Combine(solutionRoot, "build", "terraform", "azure", "service_bus"); var credentials = AzureCredentials.Instance; + // don't try to run terraform if not authenticated. + if (credentials is Unauthenticated) + return; + _terraform = new TerraformResources(terraformResourceDirectory, credentials, messageSink); var machineName = Environment.MachineName.ToLowerInvariant(); @@ -62,6 +66,6 @@ public AzureServiceBusTestEnvironment(IMessageSink messageSink) public ServiceBusConnectionStringProperties ServiceBusConnectionStringProperties { get; } - public void Dispose() => _terraform.Destroy(_variables); + public void Dispose() => _terraform?.Destroy(_variables); } } diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs index 942048535..717ef4073 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/AzureMessagingServiceBusDiagnosticListenerTests.cs @@ -6,6 +6,7 @@ using Elastic.Apm.Azure.ServiceBus.Tests.Azure; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Azure; using Elastic.Apm.Tests.Utilities.XUnit; using FluentAssertions; using Xunit; diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs index 1c281225c..da76d9443 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/MicrosoftAzureServiceBusDiagnosticListenerTests.cs @@ -6,6 +6,7 @@ using Elastic.Apm.Azure.ServiceBus.Tests.Azure; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Azure; using Elastic.Apm.Tests.Utilities.XUnit; using FluentAssertions; using Microsoft.Azure.ServiceBus; diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs new file mode 100644 index 000000000..f1aa971c4 --- /dev/null +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading.Tasks; +using Azure.Storage.Queues; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Azure; +using Elastic.Apm.Tests.Utilities.XUnit; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Azure.Storage.Tests +{ + [Collection("AzureStorage")] + public class AzureQueueStorageDiagnosticListenerTests + { + private readonly AzureStorageTestEnvironment _environment; + private readonly MockPayloadSender _sender; + private readonly ApmAgent _agent; + + public AzureQueueStorageDiagnosticListenerTests(AzureStorageTestEnvironment environment, ITestOutputHelper output) + { + _environment = environment; + + var logger = new XUnitLogger(LogLevel.Trace, output); + _sender = new MockPayloadSender(logger); + _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); + _agent.Subscribe(new AzureQueueStorageDiagnosticsSubscriber()); + + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Receives_From_Queue() + { + var queueName = Guid.NewGuid().ToString(); + var client = new QueueClient(_environment.StorageAccountConnectionString, queueName); + + var createResponse = await client.CreateAsync(); + var sendResponse = await client.SendMessageAsync(nameof(Capture_Span_When_Receives_From_Queue)); + + var receiveResponse = await client.ReceiveMessagesAsync(1); + + if (!_sender.WaitForTransactions()) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(1); + var transaction = _sender.FirstTransaction; + + transaction.Name.Should().Be($"AzureQueue RECEIVE from {queueName}"); + transaction.Type.Should().Be("messaging"); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Receive_From_Queue() + { + var queueName = Guid.NewGuid().ToString(); + var client = new QueueClient(_environment.StorageAccountConnectionString, queueName); + + var createResponse = await client.CreateAsync(); + var sendResponse = await client.SendMessageAsync(nameof(Capture_Span_When_Receive_From_Queue)); + + var receiveResponse = await client.ReceiveMessageAsync(); + + if (!_sender.WaitForTransactions()) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(1); + var transaction = _sender.FirstTransaction; + + transaction.Name.Should().Be($"AzureQueue RECEIVE from {queueName}"); + transaction.Type.Should().Be("messaging"); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Send_To_Queue() + { + var queueName = Guid.NewGuid().ToString(); + var client = new QueueClient(_environment.StorageAccountConnectionString, queueName); + + var createResponse = await client.CreateAsync(); + + await _agent.Tracer.CaptureTransaction("Send Azure Queue Message", "message", async () => + { + var sendResponse = await client.SendMessageAsync(nameof(Capture_Span_When_Send_To_Queue)); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"AzureQueue SEND to {queueName}"); + span.Type.Should().Be("messaging"); + span.Subtype.Should().Be("azurequeue"); + span.Action.Should().Be("send"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.QueueUrl); + destination.Service.Name.Should().Be("azurequeue"); + destination.Service.Resource.Should().Be($"azurequeue/{queueName}"); + destination.Service.Type.Should().Be("messaging"); + } + } +} diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs new file mode 100644 index 000000000..30e0e2b10 --- /dev/null +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs @@ -0,0 +1,118 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Generic; +using System.IO; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Azure; +using Elastic.Apm.Tests.Utilities.Terraform; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Azure.Storage.Tests +{ + [CollectionDefinition("AzureStorage")] + public class AzureStorageTestEnvironmentCollection : ICollectionFixture + { + + } + + /// + /// A test environment for Azure Storage that deploys and configures an Azure Storage account + /// in a given region and location + /// + /// + /// Resource name rules + /// https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules + /// + public class AzureStorageTestEnvironment : IDisposable + { + private readonly TerraformResources _terraform; + private readonly Dictionary _variables; + + public AzureStorageTestEnvironment(IMessageSink messageSink) + { + var solutionRoot = SolutionPaths.Root; + var terraformResourceDirectory = Path.Combine(solutionRoot, "build", "terraform", "azure", "storage"); + var credentials = AzureCredentials.Instance; + + // don't try to run terraform if not authenticated. + if (credentials is Unauthenticated) + return; + + _terraform = new TerraformResources(terraformResourceDirectory, credentials, messageSink); + + var machineName = Environment.MachineName.ToLowerInvariant(); + if (machineName.Length > 66) + machineName = machineName.Substring(0, 66); + + _variables = new Dictionary + { + ["resource_group"] = $"dotnet-{machineName}-storage-test", + ["storage_account_name"] = "dotnet" + Guid.NewGuid().ToString("N").Substring(0, 18), + }; + + _terraform.Init(); + _terraform.Apply(_variables); + + StorageAccountConnectionString = _terraform.Output("connection_string"); + StorageAccountConnectionStringProperties = ParseConnectionString(StorageAccountConnectionString); + } + + public StorageAccountProperties StorageAccountConnectionStringProperties { get; } + + private static StorageAccountProperties ParseConnectionString(string connectionString) + { + var parts = connectionString.Split(';'); + string accountName = null; + string endpointSuffix = null; + string defaultEndpointsProtocol = null; + + foreach (var item in parts) + { + var kv = item.Split('='); + switch (kv[0]) + { + case "AccountName": + accountName = kv[1]; + break; + case "EndpointSuffix": + endpointSuffix = kv[1]; + break; + case "DefaultEndpointsProtocol": + defaultEndpointsProtocol = kv[1]; + break; + } + } + + return new StorageAccountProperties(defaultEndpointsProtocol, accountName, endpointSuffix); + } + + public string StorageAccountConnectionString { get; } + + + public void Dispose() => _terraform?.Destroy(_variables); + } + + public class StorageAccountProperties + { + public StorageAccountProperties(string defaultEndpointsProtocol, string accountName, string endpointSuffix) + { + DefaultEndpointsProtocol = defaultEndpointsProtocol; + AccountName = accountName; + EndpointSuffix = endpointSuffix; + } + + public string AccountName { get; } + + public string EndpointSuffix { get; } + + public string DefaultEndpointsProtocol { get; } + + // https://[storage account name].queue.core.windows.net/[queue name]/messages"; + public string QueueUrl => $"{DefaultEndpointsProtocol}://{AccountName}.queue.{EndpointSuffix}/"; + } +} diff --git a/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj b/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj new file mode 100644 index 000000000..ce20d25fc --- /dev/null +++ b/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj @@ -0,0 +1,29 @@ + + + + net5.0 + false + Elastic.Apm.Azure.Storage.Tests + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs similarity index 87% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs rename to test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs index 1b33e2a4c..408587bc2 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs +++ b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentials.cs @@ -12,16 +12,25 @@ using Newtonsoft.Json; using ProcNet; -namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure +namespace Elastic.Apm.Tests.Utilities.Azure { + /// + /// Unauthenticated Azure credentials + /// public class Unauthenticated : AzureCredentials { } + /// + /// Azure credentials authentication with a User account. + /// public class AzureUserAccount : AzureCredentials { } + /// + /// Azure credentials authenticated with a Service Principal + /// public class ServicePrincipal : AzureCredentials { [JsonConstructor] @@ -125,7 +134,8 @@ private static bool LoggedIntoAccountWithAzureCli() } /// - /// A set of Azure credentials obtained from environment variables or a .credentials.json configuration file + /// A set of Azure credentials obtained from environment variables or account authenticated with Azure CLI 2.0. + /// If no credentials are found, an unauthenticated credential is returned. /// public static AzureCredentials Instance => _lazyCredentials.Value; diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentialsFactAttribute.cs similarity index 89% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs rename to test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentialsFactAttribute.cs index e486568ee..7a42dfea6 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs +++ b/test/Elastic.Apm.Tests.Utilities/Azure/AzureCredentialsFactAttribute.cs @@ -5,7 +5,7 @@ using Xunit; -namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure +namespace Elastic.Apm.Tests.Utilities.Azure { /// /// Attribute applied to a test that should be run by the test runner if Azure credentials are available diff --git a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj index b0fbca0f1..459931304 100644 --- a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj +++ b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj @@ -24,9 +24,11 @@ + + diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs b/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResourceException.cs similarity index 84% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs rename to test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResourceException.cs index d7da010f1..b6c285c22 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs +++ b/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResourceException.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Linq; -namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform +namespace Elastic.Apm.Tests.Utilities.Terraform { /// /// An exception from interacting with terraform resources. @@ -15,7 +15,7 @@ namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform public class TerraformResourceException : Exception { public TerraformResourceException(string message, int exitCode, List output) - : base(string.Join(Environment.NewLine, new [] { message, $"exit code: {exitCode}", "output:" }.Concat(output))) + : base(string.Join(Environment.NewLine, new [] { message, $"exit code: {exitCode}", "output:" }.Concat(output))) { } diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs b/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResources.cs similarity index 93% rename from test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs rename to test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResources.cs index c56a8a654..30583119a 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs +++ b/test/Elastic.Apm.Tests.Utilities/Terraform/TerraformResources.cs @@ -8,13 +8,13 @@ using System.IO; using System.Runtime.ExceptionServices; using System.Text; -using Elastic.Apm.Azure.ServiceBus.Tests.Azure; +using Elastic.Apm.Tests.Utilities.Azure; using ProcNet; using ProcNet.Std; using Xunit.Abstractions; using Xunit.Sdk; -namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform +namespace Elastic.Apm.Tests.Utilities.Terraform { /// /// Interact with Terraform templates to apply and destroy resources @@ -139,7 +139,7 @@ public string Output(string name) /// /// Destroys the terraform managed infrastructure /// - /// + /// public void Destroy(IDictionary variables = null) { var args = new List From 79a6706116327d4dd6256283a1d47c2670cac379 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 23 Mar 2021 13:50:20 +1000 Subject: [PATCH 27/32] Azure Blob storage integration This commit captures spans for the following Azure storage events - Create container - Delete container - GetBlobs in container - Create page blob - Upload block blob - Upload page blob pages - Download blob - Download streaming blob - Copy from URI into blob - Delete blob --- .../AzureBlobStorageDiagnosticListener.cs | 219 ++++++++++ .../AzureBlobStorageDiagnosticsSubscriber.cs | 34 ++ .../AzureQueueStorageDiagnosticListener.cs | 47 ++- .../Elastic.Apm.Azure.Storage.csproj | 14 +- ...AzureBlobStorageDiagnosticListenerTests.cs | 386 ++++++++++++++++++ ...zureQueueStorageDiagnosticListenerTests.cs | 1 - .../AzureStorageTestEnvironment.cs | 2 + .../BlobContainerScope.cs | 37 ++ .../Elastic.Apm.Azure.Storage.Tests.csproj | 1 + 9 files changed, 722 insertions(+), 19 deletions(-) create mode 100644 src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs create mode 100644 src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticsSubscriber.cs create mode 100644 test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs create mode 100644 test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs diff --git a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs new file mode 100644 index 000000000..0f7c31829 --- /dev/null +++ b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs @@ -0,0 +1,219 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticListeners; +using Elastic.Apm.Helpers; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Azure.Storage +{ + internal static class AzureBlobStorage + { + internal const string SpanName = "AzureBlob"; + internal const string SubType = "azureblob"; + internal const string Type = "storage"; + } + + /// + /// Creates transactions and spans for Azure Blob Storage diagnostic events from Azure.Storage.Blobs + /// + public class AzureBlobStorageDiagnosticListener : DiagnosticListenerBase + { + private readonly ConcurrentDictionary _processingSegments = + new ConcurrentDictionary(); + + public AzureBlobStorageDiagnosticListener(IApmAgent agent) : base(agent) { } + + public override string Name { get; } = "Azure.Storage.Blobs"; + + protected override void HandleOnNext(KeyValuePair kv) + { + Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); + + if (string.IsNullOrEmpty(kv.Key)) + { + Logger.Trace()?.Log($"Key is {(kv.Key == null ? "null" : "an empty string")} - exiting"); + return; + } + + switch (kv.Key) + { + case "BlobContainerClient.Create.Start": + case "PageBlobClient.Create.Start": + OnStart(kv, "Create"); + break; + case "BlobContainerClient.Delete.Start": + case "BlobBaseClient.Delete.Start": + OnStart(kv, "Delete"); + break; + case "BlobContainerClient.GetBlobs.Start": + OnStart(kv, "GetBlobs"); + break; + case "BlockBlobClient.Upload.Start": + case "PageBlobClient.UploadPages.Start": + OnStart(kv, "Upload"); + break; + case "BlobBaseClient.Download.Start": + case "BlobBaseClient.DownloadContent.Start": + case "BlobBaseClient.DownloadStreaming.Start": + OnStart(kv, "Download"); + break; + case "BlobBaseClient.StartCopyFromUri.Start": + OnStart(kv, "CopyFromUri"); + break; + case "BlobContainerClient.Create.Stop": + case "BlobContainerClient.Delete.Stop": + case "BlobBaseClient.Delete.Stop": + case "PageBlobClient.Create.Stop": + case "BlockBlobClient.Upload.Stop": + case "BlobBaseClient.Download.Stop": + case "BlobBaseClient.DownloadContent.Stop": + case "BlobBaseClient.DownloadStreaming.Stop": + case "PageBlobClient.UploadPages.Stop": + case "BlobContainerClient.GetBlobs.Stop": + case "BlobBaseClient.StartCopyFromUri.Stop": + OnStop(); + break; + case "BlobContainerClient.Create.Exception": + case "BlobContainerClient.Delete.Exception": + case "BlobBaseClient.Delete.Exception": + case "PageBlobClient.Create.Exception": + case "BlockBlobClient.Upload.Exception": + case "BlobBaseClient.Download.Exception": + case "BlobBaseClient.DownloadContent.Exception": + case "BlobBaseClient.DownloadStreaming.Exception": + case "PageBlobClient.UploadPages.Exception": + case "BlobContainerClient.GetBlobs.Exception": + case "BlobBaseClient.StartCopyFromUri.Exception": + OnException(kv); + break; + default: + Logger.Trace()?.Log("`{DiagnosticEventKey}' key is not a traced diagnostic event", kv.Key); + break; + } + } + + private void OnStart(KeyValuePair kv, string action) + { + var currentSegment = ApmAgent.GetCurrentExecutionSegment(); + if (currentSegment is null) + { + Logger.Trace()?.Log("No current transaction or span - exiting"); + return; + } + + if (!(kv.Value is Activity activity)) + { + Logger.Trace()?.Log("Value is not an activity - exiting"); + return; + } + + var urlTag = activity.Tags.FirstOrDefault(t => t.Key == "url").Value; + var blobUrl = new BlobUrl(urlTag); + + var spanName = blobUrl.BlobName is null + ? $"{AzureBlobStorage.SpanName} {action} {blobUrl.ContainerName}" + : $"{AzureBlobStorage.SpanName} {action} {blobUrl.ContainerName}/{blobUrl.BlobName}"; + + var span = currentSegment.StartSpan(spanName, AzureBlobStorage.Type, AzureBlobStorage.SubType, action); + span.Context.Destination = new Destination + { + Address = blobUrl.FullyQualifiedNamespace, + Service = new Destination.DestinationService + { + Name = AzureBlobStorage.SubType, + Resource = blobUrl.BlobName is null + ? $"{AzureBlobStorage.SubType}/{blobUrl.ContainerName}" + : $"{AzureBlobStorage.SubType}/{blobUrl.ContainerName}/{blobUrl.BlobName}", + Type = AzureBlobStorage.Type + } + }; + + if (!_processingSegments.TryAdd(activity.Id, span)) + { + Logger.Trace()?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + action, + span.Id, + activity.Id); + } + } + + private void OnStop() + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); + return; + } + + segment.Outcome = Outcome.Success; + segment.End(); + } + + private void OnException(KeyValuePair kv) + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); + return; + } + + if (kv.Value is Exception e) + segment.CaptureException(e); + + segment.Outcome = Outcome.Failure; + segment.End(); + } + + private class BlobUrl + { + public BlobUrl(string url) + { + var builder = new UriBuilder(url); + + FullyQualifiedNamespace = builder.Uri.GetLeftPart(UriPartial.Authority) + "/"; + + ContainerName = builder.Uri.Segments.Length > 1 + ? builder.Uri.Segments[1].TrimEnd('/') + : null; + + BlobName = builder.Uri.Segments.Length > 2 + ? builder.Uri.Segments[2].TrimEnd('/') + : null; + } + + public string ContainerName { get; } + + public string BlobName { get; } + + public string FullyQualifiedNamespace { get; } + } + } +} diff --git a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticsSubscriber.cs new file mode 100644 index 000000000..6befc58ee --- /dev/null +++ b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticsSubscriber.cs @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Diagnostics; +using Elastic.Apm.DiagnosticSource; + +namespace Elastic.Apm.Azure.Storage +{ + /// + /// Subscribes to diagnostic source events from Azure.Storage.Blobs + /// + public class AzureBlobStorageDiagnosticsSubscriber : IDiagnosticsSubscriber + { + /// + /// Subscribes diagnostic source events. + /// + public IDisposable Subscribe(IApmAgent agent) + { + var retVal = new CompositeDisposable(); + + var initializer = new DiagnosticInitializer(agent.Logger, new[] { new AzureBlobStorageDiagnosticListener(agent) }); + retVal.Add(initializer); + + retVal.Add(DiagnosticListener + .AllListeners + .Subscribe(initializer)); + + return retVal; + } + } +} diff --git a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs index eaab8c592..e673eca4e 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs @@ -15,8 +15,16 @@ namespace Elastic.Apm.Azure.Storage { + internal static class AzureQueueStorage + { + internal const string SpanName = "AzureQueue"; + internal const string Type = "messaging"; + internal const string SubType = "azurequeue"; + + } + /// - /// Creates transactions and spans for diagnostic events from Azure.Storage.Queues + /// Creates transactions and spans for Azure Queue Storage diagnostic events from Azure.Storage.Queues /// public class AzureQueueStorageDiagnosticListener : DiagnosticListenerBase { @@ -29,7 +37,7 @@ public AzureQueueStorageDiagnosticListener(IApmAgent agent) : base(agent) { _realAgent = agent as ApmAgent; _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); - _service.Framework = new Framework { Name = "AzureQueue" }; + _service.Framework = new Framework { Name = AzureQueueStorage.SpanName }; } public override string Name { get; } = "Azure.Storage.Queues"; @@ -100,18 +108,18 @@ private void OnSendStart(KeyValuePair kv) return; var spanName = queueName is null - ? $"AzureQueue SEND" - : $"AzureQueue SEND to {queueName}"; + ? $"{AzureQueueStorage.SpanName} SEND" + : $"{AzureQueueStorage.SpanName} SEND to {queueName}"; - var span = currentSegment.StartSpan(spanName, "messaging", "azurequeue", "send"); + var span = currentSegment.StartSpan(spanName, AzureQueueStorage.Type, AzureQueueStorage.SubType, "send"); span.Context.Destination = new Destination { Address = destinationAddress, Service = new Destination.DestinationService { - Name = "azurequeue", - Resource = queueName is null ? "azurequeue" : $"azurequeue/{queueName}", - Type = "messaging" + Name = AzureQueueStorage.SubType, + Resource = queueName is null ? AzureQueueStorage.SubType : $"{AzureQueueStorage.SubType}/{queueName}", + Type = AzureQueueStorage.Type } }; @@ -142,10 +150,10 @@ private void OnReceiveStart(KeyValuePair kv) return; var transactionName = queueName is null - ? $"AzureQueue RECEIVE" - : $"AzureQueue RECEIVE from {queueName}"; + ? $"{AzureQueueStorage.SpanName} RECEIVE" + : $"{AzureQueueStorage.SpanName} RECEIVE from {queueName}"; - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, "messaging"); + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, AzureQueueStorage.Type); transaction.Context.Service = _service; // transaction creation will create an activity, so use this as the key. @@ -230,15 +238,20 @@ private void OnException(KeyValuePair kv) /// private class QueueUrl { - private readonly UriBuilder _builder; + public QueueUrl(string url) + { + var builder = new UriBuilder(url); - public QueueUrl(string url) => _builder = new UriBuilder(url); + FullyQualifiedNamespace = builder.Uri.GetLeftPart(UriPartial.Authority) + "/"; - public string QueueName => _builder.Uri.Segments.Length > 2 - ? _builder.Uri.Segments[1].TrimEnd('/') - : null; + QueueName = builder.Uri.Segments.Length > 1 + ? builder.Uri.Segments[1].TrimEnd('/') + : null; + } + + public string QueueName { get; } - public string FullyQualifiedNamespace => _builder.Uri.GetLeftPart(UriPartial.Authority) + "/"; + public string FullyQualifiedNamespace { get; } } } diff --git a/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj b/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj index 593e18099..621b3297f 100644 --- a/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj +++ b/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj @@ -1,9 +1,21 @@ - netstandard2.0 + netstandard2.0 + + Elastic.Apm.Azure.Storage + Elastic.Apm.Azure.Storage + Elastic.Apm.Azure.Storage + Elastic APM for Azure Storage. This package contains auto instrumentation for Azure.Storage.Queues, + and Azure.Storage.Blobs packages. + apm, monitoring, elastic, elasticapm, analytics, azure, storage, queue, blob + true + + + + diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs new file mode 100644 index 000000000..4b00efcab --- /dev/null +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs @@ -0,0 +1,386 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; +using Azure.Storage.Queues; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Azure; +using Elastic.Apm.Tests.Utilities.XUnit; +using FluentAssertions; +using Microsoft.Diagnostics.Tracing.Parsers.Kernel; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Azure.Storage.Tests +{ + [Collection("AzureStorage")] + public class AzureBlobStorageDiagnosticListenerTests + { + private readonly AzureStorageTestEnvironment _environment; + private readonly ITestOutputHelper _testOutputHelper; + private readonly MockPayloadSender _sender; + private readonly ApmAgent _agent; + + public AzureBlobStorageDiagnosticListenerTests(AzureStorageTestEnvironment environment, ITestOutputHelper output, ITestOutputHelper testOutputHelper) + { + _environment = environment; + _testOutputHelper = testOutputHelper; + + var logger = new XUnitLogger(LogLevel.Trace, output); + _sender = new MockPayloadSender(logger); + _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); + _agent.Subscribe(new AzureBlobStorageDiagnosticsSubscriber()); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Create_Container() + { + var containerName = Guid.NewGuid().ToString(); + var client = new BlobContainerClient(_environment.StorageAccountConnectionString, containerName); + + await _agent.Tracer.CaptureTransaction("Create Azure Container", AzureBlobStorage.Type, async () => + { + var containerCreateResponse = await client.CreateAsync(); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Create {containerName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Create"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{containerName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Delete_Container() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + await _agent.Tracer.CaptureTransaction("Delete Azure Container", AzureBlobStorage.Type, async () => + { + var containerDeleteResponse = await scope.ContainerClient.DeleteAsync(); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Delete {scope.ContainerName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Delete"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Create_Page_Blob() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var blobName = Guid.NewGuid().ToString(); + var client = new PageBlobClient(_environment.StorageAccountConnectionString, scope.ContainerName, blobName); + + await _agent.Tracer.CaptureTransaction("Create Azure Page Blob", AzureBlobStorage.Type, async () => + { + var blobCreateResponse = await client.CreateAsync(1024); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Create {scope.ContainerName}/{blobName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Create"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Upload_Page_Blob() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var blobName = Guid.NewGuid().ToString(); + var client = new PageBlobClient(_environment.StorageAccountConnectionString, scope.ContainerName, blobName); + var blobCreateResponse = await client.CreateAsync(1024); + + await _agent.Tracer.CaptureTransaction("Upload Azure Page Blob", AzureBlobStorage.Type, async () => + { + var random = new Random(); + var bytes = new byte[512]; + random.NextBytes(bytes); + + var stream = new MemoryStream(bytes); + var uploadPagesResponse = await client.UploadPagesAsync(stream, 0); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Upload {scope.ContainerName}/{blobName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Upload"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Upload_Block_Blob() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var blobName = Guid.NewGuid().ToString(); + var client = new BlockBlobClient(_environment.StorageAccountConnectionString, scope.ContainerName, blobName); + + await _agent.Tracer.CaptureTransaction("Upload Azure Block Blob", AzureBlobStorage.Type, async () => + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); + var blobUploadResponse = await client.UploadAsync(stream); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Upload {scope.ContainerName}/{blobName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Upload"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Download_Blob() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var blobName = Guid.NewGuid().ToString(); + var client = scope.ContainerClient.GetBlobClient(blobName); + + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); + var blobUploadResponse = await client.UploadAsync(stream); + + await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", AzureBlobStorage.Type, async () => + { + var downloadResponse = await client.DownloadAsync(); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Download {scope.ContainerName}/{blobName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Download"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Download_Streaming_Blob() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var blobName = Guid.NewGuid().ToString(); + var client = scope.ContainerClient.GetBlobClient(blobName); + + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); + var blobUploadResponse = await client.UploadAsync(stream); + + await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", AzureBlobStorage.Type, async () => + { + stream.Position = 0; + var downloadResponse = await client.DownloadToAsync(stream); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Download {scope.ContainerName}/{blobName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Download"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Delete_Blob() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var blobName = Guid.NewGuid().ToString(); + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); + var blobUploadResponse = await scope.ContainerClient.UploadBlobAsync(blobName, stream); + + await _agent.Tracer.CaptureTransaction("Delete Azure Blob", AzureBlobStorage.Type, async () => + { + var containerDeleteResponse = await scope.ContainerClient.DeleteBlobAsync(blobName); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} Delete {scope.ContainerName}/{blobName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("Delete"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Copy_From_Uri() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var sourceBlobName = Guid.NewGuid().ToString(); + var client = scope.ContainerClient.GetBlobClient(sourceBlobName); + + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); + var blobUploadResponse = await client.UploadAsync(stream); + + var destinationBlobName = Guid.NewGuid().ToString(); + await _agent.Tracer.CaptureTransaction("Copy Azure Blob", AzureBlobStorage.Type, async () => + { + var otherClient = scope.ContainerClient.GetBlobClient(destinationBlobName); + var operation = await otherClient.StartCopyFromUriAsync(client.Uri); + await operation.WaitForCompletionAsync(); + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} CopyFromUri {scope.ContainerName}/{destinationBlobName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("CopyFromUri"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{destinationBlobName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Get_Blobs() + { + await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); + + var c = scope.ContainerClient.GetBlobClient("fo"); + + for (var i = 0; i < 2; i++) + { + var blobName = Guid.NewGuid().ToString(); + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); + var blobUploadResponse = await scope.ContainerClient.UploadBlobAsync(blobName, stream); + } + + await _agent.Tracer.CaptureTransaction("Get Blobs", AzureBlobStorage.Type, async () => + { + var asyncPageable = scope.ContainerClient.GetBlobsAsync(); + await foreach (var blob in asyncPageable) + { + // ReSharper disable once Xunit.XunitTestWithConsoleOutput + Console.WriteLine(blob.Name); + } + }); + + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureBlobStorage.SpanName} GetBlobs {scope.ContainerName}"); + span.Type.Should().Be(AzureBlobStorage.Type); + span.Subtype.Should().Be(AzureBlobStorage.SubType); + span.Action.Should().Be("GetBlobs"); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); + destination.Service.Name.Should().Be(AzureBlobStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}"); + destination.Service.Type.Should().Be(AzureBlobStorage.Type); + } + } +} diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs index f1aa971c4..706f7c8b7 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs @@ -26,7 +26,6 @@ public AzureQueueStorageDiagnosticListenerTests(AzureStorageTestEnvironment envi _sender = new MockPayloadSender(logger); _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); _agent.Subscribe(new AzureQueueStorageDiagnosticsSubscriber()); - } [AzureCredentialsFact] diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs index 30e0e2b10..b2e7e81c3 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs @@ -114,5 +114,7 @@ public StorageAccountProperties(string defaultEndpointsProtocol, string accountN // https://[storage account name].queue.core.windows.net/[queue name]/messages"; public string QueueUrl => $"{DefaultEndpointsProtocol}://{AccountName}.queue.{EndpointSuffix}/"; + + public string BlobUrl => $"{DefaultEndpointsProtocol}://{AccountName}.blob.{EndpointSuffix}/"; } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs b/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs new file mode 100644 index 000000000..6a266a056 --- /dev/null +++ b/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs @@ -0,0 +1,37 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; + +namespace Elastic.Apm.Azure.Storage.Tests +{ + public class BlobContainerScope : IAsyncDisposable + { + public string ContainerName { get; } + private readonly BlobContainerInfo _properties; + public BlobContainerClient ContainerClient { get; } + + private BlobContainerScope(BlobContainerClient adminClient, string containerName, BlobContainerInfo properties) + { + ContainerClient = adminClient; + ContainerName = containerName; + _properties = properties; + } + + public static async Task CreateContainer(string connectionString) + { + var containerName = Guid.NewGuid().ToString("D"); + var containerClient = new BlobContainerClient(connectionString, containerName); + var response = await containerClient.CreateAsync().ConfigureAwait(false); + return new BlobContainerScope(containerClient, containerName, response.Value); + } + + public async ValueTask DisposeAsync() => + await ContainerClient.DeleteAsync().ConfigureAwait(false); + } +} diff --git a/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj b/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj index ce20d25fc..e1b3d7832 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj +++ b/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj @@ -8,6 +8,7 @@ + From d4cc2de1e93d93a22e839e82df4270bf56b45e99 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Fri, 26 Mar 2021 17:49:31 +1000 Subject: [PATCH 28/32] Azure File share integration --- .../AzureBlobStorageDiagnosticListener.cs | 21 +- ...AzureFileShareStorageDiagnosticListener.cs | 187 ++++++++++++++++++ ...reFileShareStorageDiagnosticsSubscriber.cs | 34 ++++ ...AzureBlobStorageDiagnosticListenerTests.cs | 173 ++-------------- ...FileShareStorageDiagnosticListenerTests.cs | 184 +++++++++++++++++ ...zureQueueStorageDiagnosticListenerTests.cs | 51 ++--- .../AzureStorageTestEnvironment.cs | 3 +- .../BlobContainerScope.cs | 10 +- .../Elastic.Apm.Azure.Storage.Tests.csproj | 1 + .../FileShareScope.cs | 62 ++++++ 10 files changed, 521 insertions(+), 205 deletions(-) create mode 100644 src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs create mode 100644 src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticsSubscriber.cs create mode 100644 test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs create mode 100644 test/Elastic.Apm.Azure.Storage.Tests/FileShareScope.cs diff --git a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs index 0f7c31829..89a265418 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs @@ -119,9 +119,7 @@ private void OnStart(KeyValuePair kv, string action) var urlTag = activity.Tags.FirstOrDefault(t => t.Key == "url").Value; var blobUrl = new BlobUrl(urlTag); - var spanName = blobUrl.BlobName is null - ? $"{AzureBlobStorage.SpanName} {action} {blobUrl.ContainerName}" - : $"{AzureBlobStorage.SpanName} {action} {blobUrl.ContainerName}/{blobUrl.BlobName}"; + var spanName = $"{AzureBlobStorage.SpanName} {action} {blobUrl.ResourceName}"; var span = currentSegment.StartSpan(spanName, AzureBlobStorage.Type, AzureBlobStorage.SubType, action); span.Context.Destination = new Destination @@ -130,9 +128,7 @@ private void OnStart(KeyValuePair kv, string action) Service = new Destination.DestinationService { Name = AzureBlobStorage.SubType, - Resource = blobUrl.BlobName is null - ? $"{AzureBlobStorage.SubType}/{blobUrl.ContainerName}" - : $"{AzureBlobStorage.SubType}/{blobUrl.ContainerName}/{blobUrl.BlobName}", + Resource = $"{AzureBlobStorage.SubType}/{blobUrl.ResourceName}", Type = AzureBlobStorage.Type } }; @@ -199,19 +195,10 @@ public BlobUrl(string url) var builder = new UriBuilder(url); FullyQualifiedNamespace = builder.Uri.GetLeftPart(UriPartial.Authority) + "/"; - - ContainerName = builder.Uri.Segments.Length > 1 - ? builder.Uri.Segments[1].TrimEnd('/') - : null; - - BlobName = builder.Uri.Segments.Length > 2 - ? builder.Uri.Segments[2].TrimEnd('/') - : null; + ResourceName = builder.Uri.AbsolutePath.TrimStart('/'); } - public string ContainerName { get; } - - public string BlobName { get; } + public string ResourceName { get; } public string FullyQualifiedNamespace { get; } } diff --git a/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs new file mode 100644 index 000000000..df0cb8591 --- /dev/null +++ b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs @@ -0,0 +1,187 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticListeners; +using Elastic.Apm.Logging; + +namespace Elastic.Apm.Azure.Storage +{ + internal static class AzureFileStorage + { + internal const string SpanName = "AzureFile"; + internal const string SubType = "azurefile"; + internal const string Type = "storage"; + } + + /// + /// Creates transactions and spans for Azure File Share Storage diagnostic events from Azure.Storage.Files.Shares + /// + public class AzureFileShareStorageDiagnosticListener : DiagnosticListenerBase + { + private readonly ConcurrentDictionary _processingSegments = + new ConcurrentDictionary(); + + public AzureFileShareStorageDiagnosticListener(IApmAgent agent) : base(agent) { } + + public override string Name { get; } = "Azure.Storage.Files.Shares"; + + protected override void HandleOnNext(KeyValuePair kv) + { + Logger.Trace()?.Log("Called with key: `{DiagnosticEventKey}'", kv.Key); + + if (string.IsNullOrEmpty(kv.Key)) + { + Logger.Trace()?.Log($"Key is {(kv.Key == null ? "null" : "an empty string")} - exiting"); + return; + } + + switch (kv.Key) + { + case "ShareClient.Create.Start": + case "ShareDirectoryClient.Create.Start": + case "ShareFileClient.Create.Start": + OnStart(kv, "Create"); + break; + case "ShareClient.Delete.Start": + case "ShareDirectoryClient.Delete.Start": + case "ShareFileClient.Delete.Start": + OnStart(kv, "Delete"); + break; + case "ShareFileClient.UploadRange.Start": + case "ShareFileClient.Upload.Start": + OnStart(kv, "Upload"); + break; + case "ShareClient.Create.Stop": + case "ShareClient.Delete.Stop": + case "ShareDirectoryClient.Create.Stop": + case "ShareDirectoryClient.Delete.Stop": + case "ShareFileClient.Create.Stop": + case "ShareFileClient.Delete.Stop": + case "ShareFileClient.UploadRange.Stop": + OnStop(); + break; + case "ShareClient.Create.Exception": + case "ShareClient.Delete.Exception": + case "ShareDirectoryClient.Create.Exception": + case "ShareDirectoryClient.Delete.Exception": + case "ShareFileClient.Create.Exception": + case "ShareFileClient.Delete.Exception": + case "ShareFileClient.UploadRange.Exception": + OnException(kv); + break; + default: + Logger.Trace()?.Log("`{DiagnosticEventKey}' key is not a traced diagnostic event", kv.Key); + break; + } + } + + private void OnStart(KeyValuePair kv, string action) + { + var currentSegment = ApmAgent.GetCurrentExecutionSegment(); + if (currentSegment is null) + { + Logger.Trace()?.Log("No current transaction or span - exiting"); + return; + } + + if (!(kv.Value is Activity activity)) + { + Logger.Trace()?.Log("Value is not an activity - exiting"); + return; + } + + var urlTag = activity.Tags.FirstOrDefault(t => t.Key == "url").Value; + var fileShareUrl = new FileShareUrl(urlTag); + var spanName = $"{AzureFileStorage.SpanName} {action} {fileShareUrl.ResourceName}"; + + var span = currentSegment.StartSpan(spanName, AzureFileStorage.Type, AzureFileStorage.SubType, action); + span.Context.Destination = new Destination + { + Address = fileShareUrl.FullyQualifiedNamespace, + Service = new Destination.DestinationService + { + Name = AzureFileStorage.SubType, + Resource = $"{AzureFileStorage.SubType}/{fileShareUrl.ResourceName}", + Type = AzureFileStorage.Type + } + }; + + if (!_processingSegments.TryAdd(activity.Id, span)) + { + Logger.Trace()?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + action, + span.Id, + activity.Id); + } + } + + private void OnStop() + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); + return; + } + + segment.Outcome = Outcome.Success; + segment.End(); + } + + private void OnException(KeyValuePair kv) + { + var activity = Activity.Current; + if (activity is null) + { + Logger.Trace()?.Log("Current activity is null - exiting"); + return; + } + + if (!_processingSegments.TryRemove(activity.Id, out var segment)) + { + Logger.Trace()?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); + return; + } + + if (kv.Value is Exception e) + segment.CaptureException(e); + + segment.Outcome = Outcome.Failure; + segment.End(); + } + + private class FileShareUrl + { + public FileShareUrl(string url) + { + var builder = new UriBuilder(url); + + FullyQualifiedNamespace = builder.Uri.GetLeftPart(UriPartial.Authority) + "/"; + ResourceName = builder.Uri.AbsolutePath.TrimStart('/'); + } + + public string ResourceName { get; } + + public string FullyQualifiedNamespace { get; } + } + } +} diff --git a/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticsSubscriber.cs b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticsSubscriber.cs new file mode 100644 index 000000000..f9a53c6f8 --- /dev/null +++ b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticsSubscriber.cs @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Diagnostics; +using Elastic.Apm.DiagnosticSource; + +namespace Elastic.Apm.Azure.Storage +{ + /// + /// Subscribes to diagnostic source events from Azure.Storage.Files.Shares + /// + public class AzureFileShareStorageDiagnosticsSubscriber : IDiagnosticsSubscriber + { + /// + /// Subscribes diagnostic source events. + /// + public IDisposable Subscribe(IApmAgent agent) + { + var retVal = new CompositeDisposable(); + + var initializer = new DiagnosticInitializer(agent.Logger, new[] { new AzureFileShareStorageDiagnosticListener(agent) }); + retVal.Add(initializer); + + retVal.Add(DiagnosticListener + .AllListeners + .Subscribe(initializer)); + + return retVal; + } + } +} diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs index 4b00efcab..fb6c3116a 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs @@ -47,23 +47,7 @@ await _agent.Tracer.CaptureTransaction("Create Azure Container", AzureBlobStorag var containerCreateResponse = await client.CreateAsync(); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Create {containerName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Create"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{containerName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Create", containerName); } [AzureCredentialsFact] @@ -76,23 +60,7 @@ await _agent.Tracer.CaptureTransaction("Delete Azure Container", AzureBlobStorag var containerDeleteResponse = await scope.ContainerClient.DeleteAsync(); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Delete {scope.ContainerName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Delete"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Delete", scope.ContainerName); } [AzureCredentialsFact] @@ -108,23 +76,7 @@ await _agent.Tracer.CaptureTransaction("Create Azure Page Blob", AzureBlobStorag var blobCreateResponse = await client.CreateAsync(1024); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Create {scope.ContainerName}/{blobName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Create"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Create", $"{scope.ContainerName}/{blobName}"); } [AzureCredentialsFact] @@ -146,23 +98,7 @@ await _agent.Tracer.CaptureTransaction("Upload Azure Page Blob", AzureBlobStorag var uploadPagesResponse = await client.UploadPagesAsync(stream, 0); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Upload {scope.ContainerName}/{blobName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Upload"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Upload", $"{scope.ContainerName}/{blobName}"); } [AzureCredentialsFact] @@ -179,23 +115,7 @@ await _agent.Tracer.CaptureTransaction("Upload Azure Block Blob", AzureBlobStora var blobUploadResponse = await client.UploadAsync(stream); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Upload {scope.ContainerName}/{blobName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Upload"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Upload", $"{scope.ContainerName}/{blobName}"); } [AzureCredentialsFact] @@ -214,23 +134,7 @@ await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", AzureBlobSto var downloadResponse = await client.DownloadAsync(); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Download {scope.ContainerName}/{blobName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Download"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Download", $"{scope.ContainerName}/{blobName}"); } [AzureCredentialsFact] @@ -250,23 +154,7 @@ await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", AzureBlobSto var downloadResponse = await client.DownloadToAsync(stream); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Download {scope.ContainerName}/{blobName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Download"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Download", $"{scope.ContainerName}/{blobName}"); } [AzureCredentialsFact] @@ -283,23 +171,7 @@ await _agent.Tracer.CaptureTransaction("Delete Azure Blob", AzureBlobStorage.Typ var containerDeleteResponse = await scope.ContainerClient.DeleteBlobAsync(blobName); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} Delete {scope.ContainerName}/{blobName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("Delete"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{blobName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("Delete", $"{scope.ContainerName}/{blobName}"); } [AzureCredentialsFact] @@ -321,23 +193,7 @@ await _agent.Tracer.CaptureTransaction("Copy Azure Blob", AzureBlobStorage.Type, await operation.WaitForCompletionAsync(); }); - if (!_sender.WaitForSpans()) - throw new Exception("No span received in timeout"); - - _sender.Spans.Should().HaveCount(1); - var span = _sender.FirstSpan; - - span.Name.Should().Be($"{AzureBlobStorage.SpanName} CopyFromUri {scope.ContainerName}/{destinationBlobName}"); - span.Type.Should().Be(AzureBlobStorage.Type); - span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("CopyFromUri"); - span.Context.Destination.Should().NotBeNull(); - var destination = span.Context.Destination; - - destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); - destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}/{destinationBlobName}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + AssertSpan("CopyFromUri", $"{scope.ContainerName}/{destinationBlobName}"); } [AzureCredentialsFact] @@ -364,22 +220,27 @@ await _agent.Tracer.CaptureTransaction("Get Blobs", AzureBlobStorage.Type, async } }); + AssertSpan("GetBlobs", scope.ContainerName); + } + + private void AssertSpan(string action, string resource) + { if (!_sender.WaitForSpans()) throw new Exception("No span received in timeout"); _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"{AzureBlobStorage.SpanName} GetBlobs {scope.ContainerName}"); + span.Name.Should().Be($"{AzureBlobStorage.SpanName} {action} {resource}"); span.Type.Should().Be(AzureBlobStorage.Type); span.Subtype.Should().Be(AzureBlobStorage.SubType); - span.Action.Should().Be("GetBlobs"); + span.Action.Should().Be(action); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); destination.Service.Name.Should().Be(AzureBlobStorage.SubType); - destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{scope.ContainerName}"); + destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{resource}"); destination.Service.Type.Should().Be(AzureBlobStorage.Type); } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs new file mode 100644 index 000000000..95e636f10 --- /dev/null +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs @@ -0,0 +1,184 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Files.Shares; +using Azure.Storage.Queues; +using Elastic.Apm.Logging; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Tests.Utilities.Azure; +using Elastic.Apm.Tests.Utilities.XUnit; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Azure.Storage.Tests +{ + [Collection("AzureStorage")] + public class AzureFileShareStorageDiagnosticListenerTests + { + private readonly AzureStorageTestEnvironment _environment; + private readonly MockPayloadSender _sender; + private readonly ApmAgent _agent; + + public AzureFileShareStorageDiagnosticListenerTests(AzureStorageTestEnvironment environment, ITestOutputHelper output) + { + _environment = environment; + + var logger = new XUnitLogger(LogLevel.Trace, output); + _sender = new MockPayloadSender(logger); + _agent = new ApmAgent(new TestAgentComponents(logger: logger, payloadSender: _sender)); + _agent.Subscribe(new AzureFileShareStorageDiagnosticsSubscriber()); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Create_File_Share() + { + var shareName = Guid.NewGuid().ToString(); + var client = new ShareClient(_environment.StorageAccountConnectionString, shareName); + + await _agent.Tracer.CaptureTransaction("Create Azure File Share", AzureFileStorage.Type, async () => + { + var response = await client.CreateAsync(); + }); + + + AssertSpan("Create", shareName); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Delete_File_Share() + { + await using var scope = await FileShareScope.CreateShare(_environment.StorageAccountConnectionString); + + await _agent.Tracer.CaptureTransaction("Delete Azure File Share", AzureFileStorage.Type, async () => + { + var deleteResponse = await scope.ShareClient.DeleteAsync(); + }); + + AssertSpan("Delete", scope.ShareName); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Create_File_Share_Directory() + { + await using var scope = await FileShareScope.CreateShare(_environment.StorageAccountConnectionString); + var directoryName = Guid.NewGuid().ToString(); + var client = scope.ShareClient.GetDirectoryClient(directoryName); + + await _agent.Tracer.CaptureTransaction("Create Azure File Share Directory", AzureFileStorage.Type, async () => + { + var response = await client.CreateAsync(); + }); + + AssertSpan("Create", $"{scope.ShareName}/{directoryName}"); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Delete_File_Share_Directory() + { + await using var scope = await FileShareScope.CreateShare(_environment.StorageAccountConnectionString); + var directoryName = Guid.NewGuid().ToString(); + var client = scope.ShareClient.GetDirectoryClient(directoryName); + var createResponse = await client.CreateAsync(); + + await _agent.Tracer.CaptureTransaction("Delete Azure File Share Directory", AzureFileStorage.Type, async () => + { + var deleteResponse = await client.DeleteAsync(); + }); + + AssertSpan("Delete", $"{scope.ShareName}/{directoryName}"); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Create_File_Share_File() + { + await using var scope = await FileShareScope.CreateDirectory(_environment.StorageAccountConnectionString); + var fileName = Guid.NewGuid().ToString(); + var client = scope.DirectoryClient.GetFileClient(fileName); + + await _agent.Tracer.CaptureTransaction("Create Azure File Share File", AzureFileStorage.Type, async () => + { + await client.CreateAsync(1024); + }); + + AssertSpan("Create", $"{scope.ShareName}/{scope.DirectoryName}/{fileName}"); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Delete_File_Share_File() + { + await using var scope = await FileShareScope.CreateDirectory(_environment.StorageAccountConnectionString); + var fileName = Guid.NewGuid().ToString(); + var client = scope.DirectoryClient.GetFileClient(fileName); + var createResponse = await client.CreateAsync(1024); + + await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", AzureFileStorage.Type, async () => + { + var response = await client.DeleteAsync(); + }); + + AssertSpan("Delete", $"{scope.ShareName}/{scope.DirectoryName}/{fileName}"); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_UploadRange_File_Share_File() + { + await using var scope = await FileShareScope.CreateDirectory(_environment.StorageAccountConnectionString); + var fileName = Guid.NewGuid().ToString(); + var client = scope.DirectoryClient.GetFileClient(fileName); + + var bytes = Encoding.UTF8.GetBytes("temp file"); + var createResponse = await client.CreateAsync(bytes.Length); + + await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", AzureFileStorage.Type, async () => + { + await using var stream = new MemoryStream(bytes); + var response = await client.UploadRangeAsync(new HttpRange(0, bytes.Length), stream); + }); + + AssertSpan("Upload", $"{scope.ShareName}/{scope.DirectoryName}/{fileName}"); + } + + [AzureCredentialsFact] + public async Task Capture_Span_When_Upload_File_Share_File() + { + await using var scope = await FileShareScope.CreateDirectory(_environment.StorageAccountConnectionString); + var fileName = Guid.NewGuid().ToString(); + var client = scope.DirectoryClient.GetFileClient(fileName); + + var bytes = Encoding.UTF8.GetBytes("temp file"); + var createResponse = await client.CreateAsync(bytes.Length); + + await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", AzureFileStorage.Type, async () => + { + await using var stream = new MemoryStream(bytes); + var response = await client.UploadAsync(stream); + }); + + AssertSpan("Upload", $"{scope.ShareName}/{scope.DirectoryName}/{fileName}"); + } + + private void AssertSpan(string action, string resource) + { + if (!_sender.WaitForSpans()) + throw new Exception("No span received in timeout"); + + _sender.Spans.Should().HaveCount(1); + var span = _sender.FirstSpan; + + span.Name.Should().Be($"{AzureFileStorage.SpanName} {action} {resource}"); + span.Type.Should().Be(AzureFileStorage.Type); + span.Subtype.Should().Be(AzureFileStorage.SubType); + span.Action.Should().Be(action); + span.Context.Destination.Should().NotBeNull(); + var destination = span.Context.Destination; + + destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.FileUrl); + destination.Service.Name.Should().Be(AzureFileStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureFileStorage.SubType}/{resource}"); + destination.Service.Type.Should().Be(AzureFileStorage.Type); + } + } +} diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs index 706f7c8b7..e84aa8c02 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs @@ -36,17 +36,9 @@ public async Task Capture_Span_When_Receives_From_Queue() var createResponse = await client.CreateAsync(); var sendResponse = await client.SendMessageAsync(nameof(Capture_Span_When_Receives_From_Queue)); - var receiveResponse = await client.ReceiveMessagesAsync(1); - if (!_sender.WaitForTransactions()) - throw new Exception("No transaction received in timeout"); - - _sender.Transactions.Should().HaveCount(1); - var transaction = _sender.FirstTransaction; - - transaction.Name.Should().Be($"AzureQueue RECEIVE from {queueName}"); - transaction.Type.Should().Be("messaging"); + AssertTransaction("RECEIVE", queueName); } [AzureCredentialsFact] @@ -60,14 +52,7 @@ public async Task Capture_Span_When_Receive_From_Queue() var receiveResponse = await client.ReceiveMessageAsync(); - if (!_sender.WaitForTransactions()) - throw new Exception("No transaction received in timeout"); - - _sender.Transactions.Should().HaveCount(1); - var transaction = _sender.FirstTransaction; - - transaction.Name.Should().Be($"AzureQueue RECEIVE from {queueName}"); - transaction.Type.Should().Be("messaging"); + AssertTransaction("RECEIVE", queueName); } [AzureCredentialsFact] @@ -75,7 +60,6 @@ public async Task Capture_Span_When_Send_To_Queue() { var queueName = Guid.NewGuid().ToString(); var client = new QueueClient(_environment.StorageAccountConnectionString, queueName); - var createResponse = await client.CreateAsync(); await _agent.Tracer.CaptureTransaction("Send Azure Queue Message", "message", async () => @@ -83,23 +67,40 @@ await _agent.Tracer.CaptureTransaction("Send Azure Queue Message", "message", as var sendResponse = await client.SendMessageAsync(nameof(Capture_Span_When_Send_To_Queue)); }); + AssertSpan("SEND", queueName); + } + + private void AssertTransaction(string action, string queueName) + { + if (!_sender.WaitForTransactions()) + throw new Exception("No transaction received in timeout"); + + _sender.Transactions.Should().HaveCount(1); + var transaction = _sender.FirstTransaction; + + transaction.Name.Should().Be($"{AzureQueueStorage.SpanName} {action} from {queueName}"); + transaction.Type.Should().Be(AzureQueueStorage.Type); + } + + private void AssertSpan(string action, string queueName) + { if (!_sender.WaitForSpans()) throw new Exception("No span received in timeout"); _sender.Spans.Should().HaveCount(1); var span = _sender.FirstSpan; - span.Name.Should().Be($"AzureQueue SEND to {queueName}"); - span.Type.Should().Be("messaging"); - span.Subtype.Should().Be("azurequeue"); - span.Action.Should().Be("send"); + span.Name.Should().Be($"{AzureQueueStorage.SpanName} {action} to {queueName}"); + span.Type.Should().Be(AzureQueueStorage.Type); + span.Subtype.Should().Be(AzureQueueStorage.SubType); + span.Action.Should().Be(action.ToLowerInvariant()); span.Context.Destination.Should().NotBeNull(); var destination = span.Context.Destination; destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.QueueUrl); - destination.Service.Name.Should().Be("azurequeue"); - destination.Service.Resource.Should().Be($"azurequeue/{queueName}"); - destination.Service.Type.Should().Be("messaging"); + destination.Service.Name.Should().Be(AzureQueueStorage.SubType); + destination.Service.Resource.Should().Be($"{AzureQueueStorage.SubType}/{queueName}"); + destination.Service.Type.Should().Be(AzureQueueStorage.Type); } } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs index b2e7e81c3..dfcd945c6 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureStorageTestEnvironment.cs @@ -112,9 +112,10 @@ public StorageAccountProperties(string defaultEndpointsProtocol, string accountN public string DefaultEndpointsProtocol { get; } - // https://[storage account name].queue.core.windows.net/[queue name]/messages"; public string QueueUrl => $"{DefaultEndpointsProtocol}://{AccountName}.queue.{EndpointSuffix}/"; public string BlobUrl => $"{DefaultEndpointsProtocol}://{AccountName}.blob.{EndpointSuffix}/"; + + public string FileUrl => $"{DefaultEndpointsProtocol}://{AccountName}.file.{EndpointSuffix}/"; } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs b/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs index 6a266a056..8a9ab6836 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs @@ -13,25 +13,23 @@ namespace Elastic.Apm.Azure.Storage.Tests public class BlobContainerScope : IAsyncDisposable { public string ContainerName { get; } - private readonly BlobContainerInfo _properties; public BlobContainerClient ContainerClient { get; } - private BlobContainerScope(BlobContainerClient adminClient, string containerName, BlobContainerInfo properties) + private BlobContainerScope(BlobContainerClient adminClient, string containerName) { ContainerClient = adminClient; ContainerName = containerName; - _properties = properties; } public static async Task CreateContainer(string connectionString) { var containerName = Guid.NewGuid().ToString("D"); var containerClient = new BlobContainerClient(connectionString, containerName); - var response = await containerClient.CreateAsync().ConfigureAwait(false); - return new BlobContainerScope(containerClient, containerName, response.Value); + await containerClient.CreateAsync().ConfigureAwait(false); + return new BlobContainerScope(containerClient, containerName); } public async ValueTask DisposeAsync() => - await ContainerClient.DeleteAsync().ConfigureAwait(false); + await ContainerClient.DeleteIfExistsAsync().ConfigureAwait(false); } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj b/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj index e1b3d7832..4b7fb90ed 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj +++ b/test/Elastic.Apm.Azure.Storage.Tests/Elastic.Apm.Azure.Storage.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/test/Elastic.Apm.Azure.Storage.Tests/FileShareScope.cs b/test/Elastic.Apm.Azure.Storage.Tests/FileShareScope.cs new file mode 100644 index 000000000..4fe117117 --- /dev/null +++ b/test/Elastic.Apm.Azure.Storage.Tests/FileShareScope.cs @@ -0,0 +1,62 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Threading.Tasks; +using Azure.Storage.Files.Shares; + +namespace Elastic.Apm.Azure.Storage.Tests +{ + public class FileShareScope : IAsyncDisposable + { + public string ShareName { get; } + public ShareClient ShareClient { get; } + + private FileShareScope(ShareClient adminClient, string shareName) + { + ShareClient = adminClient; + ShareName = shareName; + } + + public static async Task CreateShare(string connectionString) + { + var shareName = Guid.NewGuid().ToString("D"); + var shareClient = new ShareClient(connectionString, shareName); + await shareClient.CreateAsync().ConfigureAwait(false); + return new FileShareScope(shareClient, shareName); + } + + public async ValueTask DisposeAsync() => + await ShareClient.DeleteIfExistsAsync().ConfigureAwait(false); + + public static async Task CreateDirectory(string connectionString) + { + var shareName = Guid.NewGuid().ToString("D"); + var shareClient = new ShareClient(connectionString, shareName); + await shareClient.CreateAsync().ConfigureAwait(false); + + var directoryName = Guid.NewGuid().ToString("D"); + var directoryClient = shareClient.GetDirectoryClient(directoryName); + await directoryClient.CreateAsync().ConfigureAwait(false); + + return new FileShareDirectoryScope(shareClient, directoryClient, shareName, directoryName); + } + + public class FileShareDirectoryScope : FileShareScope + { + public string DirectoryName { get; } + public ShareDirectoryClient DirectoryClient { get; } + + public FileShareDirectoryScope(ShareClient shareClient, ShareDirectoryClient directoryClient, string shareName, string directoryName) + : base(shareClient, shareName) + { + DirectoryClient = directoryClient; + DirectoryName = directoryName; + } + } + } + + +} From 171d5ef5a103d243aafcea8241a65cb2b1350667 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 6 Apr 2021 15:20:40 +1000 Subject: [PATCH 29/32] fix merge commit --- .../Azure/AzureCredentials.cs | 134 -------------- .../Azure/AzureCredentialsFactAttribute.cs | 21 --- .../Azure/AzureServiceBusTestEnvironment.cs | 1 - .../Terraform/TerraformResourceException.cs | 27 --- .../Terraform/TerraformResources.cs | 166 ------------------ 5 files changed, 349 deletions(-) delete mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs delete mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs delete mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs delete mode 100644 test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs deleted file mode 100644 index 1b33e2a4c..000000000 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentials.cs +++ /dev/null @@ -1,134 +0,0 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; -using Elastic.Apm.Tests.Utilities; -using Newtonsoft.Json; -using ProcNet; - -namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure -{ - public class Unauthenticated : AzureCredentials - { - } - - public class AzureUserAccount : AzureCredentials - { - } - - public class ServicePrincipal : AzureCredentials - { - [JsonConstructor] - private ServicePrincipal() { } - - [JsonProperty("clientId")] - public string ClientId { get; private set; } - - [JsonProperty("clientSecret")] - public string ClientSecret { get; private set; } - - [JsonProperty("tenantId")] - public string TenantId { get; private set; } - - [JsonProperty("subscriptionId")] - public string SubscriptionId { get; private set; } - - public ServicePrincipal(string clientId, string clientSecret, string tenantId, string subscriptionId) - { - ClientId = clientId; - ClientSecret = clientSecret; - TenantId = tenantId; - SubscriptionId = subscriptionId; - } - public override void AddToArguments(StartArguments startArguments) - { - startArguments.Environment ??= new Dictionary(); - startArguments.Environment[ARM_CLIENT_ID] = ClientId; - startArguments.Environment[ARM_CLIENT_SECRET] = ClientSecret; - startArguments.Environment[ARM_SUBSCRIPTION_ID] = SubscriptionId; - startArguments.Environment[ARM_TENANT_ID] = TenantId; - } - } - - public abstract class AzureCredentials - { - // ReSharper disable InconsistentNaming - protected const string ARM_CLIENT_ID = nameof(ARM_CLIENT_ID); - protected const string ARM_CLIENT_SECRET = nameof(ARM_CLIENT_SECRET); - protected const string ARM_TENANT_ID = nameof(ARM_TENANT_ID); - protected const string ARM_SUBSCRIPTION_ID = nameof(ARM_SUBSCRIPTION_ID); - // ReSharper restore InconsistentNaming - - private static readonly Lazy _lazyCredentials = - new Lazy(LoadCredentials, LazyThreadSafetyMode.ExecutionAndPublication); - - private static AzureCredentials LoadCredentials() - { - var runningInCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_ID")); - if (runningInCi) - { - var credentialsFile = Path.Combine(SolutionPaths.Root, ".credentials.json"); - if (!File.Exists(credentialsFile)) - return new Unauthenticated(); - - try - { - using var fileStream = new FileStream(credentialsFile, FileMode.Open, FileAccess.Read, FileShare.Read); - using var streamReader = new StreamReader(fileStream); - using var jsonTextReader = new JsonTextReader(streamReader); - var serializer = new JsonSerializer(); - return serializer.Deserialize(jsonTextReader); - } - catch (Exception e) - { - Console.WriteLine(e); - return new Unauthenticated(); - } - } - - return LoggedIntoAccountWithAzureCli() - ? new AzureUserAccount() - : new Unauthenticated(); - } - - /// - /// Checks that Azure CLI is installed and in the PATH, and is logged into an account - /// - /// true if logged in - private static bool LoggedIntoAccountWithAzureCli() - { - try - { - // run azure CLI using cmd on Windows so that %~dp0 in az.cmd expands to - // the path containing the cmd file. - var binary = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "cmd" - : "az"; - var args = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? new[] { "/c", "az", "account", "show" } - : new[] { "account", "show" }; - - var result = Proc.Start(new StartArguments(binary, args)); - return result.Completed && result.ExitCode == 0; - } - catch (Exception e) - { - Console.WriteLine(e); - return false; - } - } - - /// - /// A set of Azure credentials obtained from environment variables or a .credentials.json configuration file - /// - public static AzureCredentials Instance => _lazyCredentials.Value; - - public virtual void AddToArguments(StartArguments startArguments) { } - } -} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs deleted file mode 100644 index e486568ee..000000000 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureCredentialsFactAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using Xunit; - -namespace Elastic.Apm.Azure.ServiceBus.Tests.Azure -{ - /// - /// Attribute applied to a test that should be run by the test runner if Azure credentials are available - /// - public class AzureCredentialsFactAttribute : FactAttribute - { - public AzureCredentialsFactAttribute() - { - if (AzureCredentials.Instance is Unauthenticated) - Skip = "Azure credentials not available. If running locally, run `az login` to login"; - } - } -} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs index 2114d4374..2eefa1169 100644 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs +++ b/test/Elastic.Apm.Azure.ServiceBus.Tests/Azure/AzureServiceBusTestEnvironment.cs @@ -10,7 +10,6 @@ using Elastic.Apm.Tests.Utilities; using Elastic.Apm.Tests.Utilities.Azure; using Elastic.Apm.Tests.Utilities.Terraform; -using Elastic.Apm.Azure.ServiceBus.Tests.Terraform; using Xunit; using Xunit.Abstractions; diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs deleted file mode 100644 index d7da010f1..000000000 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResourceException.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform -{ - /// - /// An exception from interacting with terraform resources. - /// - public class TerraformResourceException : Exception - { - public TerraformResourceException(string message, int exitCode, List output) - : base(string.Join(Environment.NewLine, new [] { message, $"exit code: {exitCode}", "output:" }.Concat(output))) - { - } - - public TerraformResourceException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} diff --git a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs b/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs deleted file mode 100644 index c56a8a654..000000000 --- a/test/Elastic.Apm.Azure.ServiceBus.Tests/Terraform/TerraformResources.cs +++ /dev/null @@ -1,166 +0,0 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.ExceptionServices; -using System.Text; -using Elastic.Apm.Azure.ServiceBus.Tests.Azure; -using ProcNet; -using ProcNet.Std; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Elastic.Apm.Azure.ServiceBus.Tests.Terraform -{ - /// - /// Interact with Terraform templates to apply and destroy resources - /// - public class TerraformResources - { - private static readonly TimeSpan _defaultTimeout = TimeSpan.FromMinutes(10); - - private readonly string _resourceDirectory; - private readonly IMessageSink _messageSink; - private readonly AzureCredentials _credentials; - - public TerraformResources(string resourceDirectory, AzureCredentials credentials, IMessageSink messageSink = null) - { - if (resourceDirectory is null) - throw new ArgumentNullException(nameof(resourceDirectory)); - - if (!Directory.Exists(resourceDirectory)) - throw new DirectoryNotFoundException($"Directory does not exist {resourceDirectory}"); - - _resourceDirectory = resourceDirectory; - _credentials = credentials; - _messageSink = messageSink; - } - - private ObservableProcess CreateProcess(params string[] arguments) - { - var startArguments = new StartArguments("terraform", arguments) - { - WorkingDirectory = _resourceDirectory - }; - _credentials.AddToArguments(startArguments); - - return new ObservableProcess(startArguments); - } - - private void RunProcess(ObservableProcess process, Action onLine = null) - { - var capturedLines = new List(); - ExceptionDispatchInfo e = null; - - process.SubscribeLines(line => - { - capturedLines.Add(line.Line); - onLine?.Invoke(line); - }, - exception => e = ExceptionDispatchInfo.Capture(exception)); - - var completed = process.WaitForCompletion(_defaultTimeout); - - if (!completed) - { - process.Dispose(); - throw new TerraformResourceException( - $"terraform {_resourceDirectory} timed out after {_defaultTimeout}", -1, capturedLines); - } - - if (e != null) - { - throw new TerraformResourceException( - $"terraform {_resourceDirectory} did not succeed", e.SourceException); - } - - if (process.ExitCode != 0) - { - throw new TerraformResourceException( - $"terraform {_resourceDirectory} did not succeed", process.ExitCode.Value, capturedLines); - } - } - - public void Init() - { - using var process = CreateProcess("init", "-no-color"); - RunProcess(process, _messageSink is null ? null: line => _messageSink.OnMessage(new DiagnosticMessage(line.Line))); - } - - /// - /// Applies the terraform infrastructure with the supplied variables - /// - /// - public void Apply(IDictionary variables = null) - { - var args = new List - { - "apply", - "-auto-approve", - "-no-color", - "-input=false" - }; - - if (variables != null) - { - foreach (var variable in variables) - { - args.Add("-var"); - args.Add($"{variable.Key}={variable.Value}"); - } - } - - using var process = CreateProcess(args.ToArray()); - RunProcess(process, _messageSink is null ? null: line => _messageSink.OnMessage(new DiagnosticMessage(line.Line))); - } - - /// - /// Reads an output value from applied terraform managed infrastructure. - /// - /// The name of the output value to read - /// - public string Output(string name) - { - var output = new StringBuilder(); - using var process = CreateProcess($"output", "-raw", "-no-color", name); - RunProcess(process, line => - { - if (!line.Error) - output.Append(line.Line); - }); - - return output.ToString(); - } - - /// - /// Destroys the terraform managed infrastructure - /// - /// - public void Destroy(IDictionary variables = null) - { - var args = new List - { - "destroy", - "-auto-approve", - "-no-color", - "-input=false" - }; - - if (variables != null) - { - foreach (var variable in variables) - { - args.Add("-var"); - args.Add($"{variable.Key}={variable.Value}"); - } - } - - using var process = CreateProcess(args.ToArray()); - RunProcess(process); - } - } -} From a560b296837aa561380f2c34f361341ab48153a5 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 7 Apr 2021 11:22:30 +1000 Subject: [PATCH 30/32] Add documentation --- docs/setup.asciidoc | 53 +++++++++++++++++++++++++++- docs/supported-technologies.asciidoc | 8 ++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index 9eb5f5149..b04d3ca87 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -15,6 +15,7 @@ On .NET Core the agent also supports auto instrumentation without any code chang * <> * <> * <> +* <> * <> [float] @@ -58,10 +59,14 @@ https://www.nuget.org/packages/Elastic.Apm.StackExchange.Redis[**Elastic.Apm.Sta This packages contains instrumentation to capture spans for commands sent to redis with https://www.nuget.org/packages/StackExchange.Redis/[StackExchange.Redis] package. -https://www.nuget.org/packages/Elastic.Apm.StackExchange.Redis[**Elastic.Apm.Azure.ServiceBus**]:: +https://www.nuget.org/packages/Elastic.Apm.Azure.ServiceBus[**Elastic.Apm.Azure.ServiceBus**]:: This packages contains instrumentation to capture transactions and spans for messages sent and received from Azure Service Bus with https://www.nuget.org/packages/Microsoft.Azure.ServiceBus/[Microsoft.Azure.ServiceBus] and https://www.nuget.org/packages/Azure.Messaging.ServiceBus/[Azure.Messaging.ServiceBus] packages. +https://www.nuget.org/packages/Elastic.Apm.Azure.Storage[**Elastic.Apm.Azure.Storage**]:: + +This packages contains instrumentation to capture spans for interation with Azure Storage with https://www.nuget.org/packages/azure.storage.queues/[Azure.Storage.Queues], https://www.nuget.org/packages/azure.storage.blobs/[Azure.Storage.Blobs] and https://www.nuget.org/packages/azure.storage.files.shares/[Azure.Storage.Files.Shares] packages. + [[setup-dotnet-net-core]] === .NET Core @@ -403,6 +408,52 @@ A new span is created when there is a current transaction, and when * one or more messages are sent to a queue or topic. * one or more messages are scheduled to a queue or a topic. +[[setup-azure-storage]] +=== Azure Storage + +[float] +==== Quick start + +Instrumentation can be enabled for Azure Storage by referencing https://www.nuget.org/packages/Elastic.Apm.Azure.Storage[`Elastic.Apm.Azure.Storage`] package and subscribing to diagnostic events using one of the subscribers: + +. If the agent is included by referencing the `Elastic.Apm.NetCoreAll` package, the subscribers will be automatically subscribed with the agent, and no further action is required. +. If you're using `Azure.Storage.Blobs`, subscribe `AzureBlobStorageDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureBlobStorageDiagnosticsSubscriber()); +---- +. If you're using `Azure.Storage.Queues`, subscribe `AzureQueueStorageDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureQueueStorageDiagnosticsSubscriber()); +---- +. If you're using `Azure.Storage.Files.Shares`, subscribe `AzureFileShareStorageDiagnosticsSubscriber` with the agent ++ +[source, csharp] +---- +Agent.Subscribe(new AzureFileShareStorageDiagnosticsSubscriber()); +---- + +For Azure Queue storage, + +* A new transaction is created when one or more messages are received from a queue +* A new span is created when there is a current transaction, and when a message is sent to a queue + +For Azure Blob storage, a new span is created when there is a current transaction and when + +* A container is created, enumerated, or deleted +* A page blob is created, uploaded, downloaded, or deleted +* A block blob is created, copied, uploaded, downloaded or deleted + +For Azure File Share storage, a new span is crated when there is a current transaction and when + +* A share is created or deleted +* A directory is created or deleted +* A file is created, uploaded, or deleted. + + [[setup-general]] === Other .NET applications diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index bd145e3cb..ab3f9f912 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -133,6 +133,12 @@ Automatic instrumentation for the following cloud services 7.0.0+ for Azure.Messaging.ServiceBus | A new transaction is created for received and receive deferred messages. A new span is created for sent and scheduled messages if there's a current transaction. -| 1.9 +| 1.10 + +| Azure Storage +| 12.8.0+ for Azure.Storage.Blobs + 12.6.0+ for Azure.Storage.Queues and Azure.Storage.Files.Shares +| +| 1.10 |=== \ No newline at end of file From 701b1fdcfbd7bcf55194af08e9dc279de776cc40 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 7 Apr 2021 11:23:59 +1000 Subject: [PATCH 31/32] Tidy up Move constants, mark types internal, add to Elastic.Apm.NetCoreAll --- .ci/linux/deploy.sh | 3 ++- .../AzureBlobStorageDiagnosticListener.cs | 7 +++--- ...AzureFileShareStorageDiagnosticListener.cs | 7 +++--- .../AzureQueueStorageDiagnosticListener.cs | 17 ++++++------- .../Elastic.Apm.Azure.Storage.csproj | 3 +-- .../ApmMiddlewareExtension.cs | 11 ++++++-- .../Elastic.Apm.NetCoreAll.csproj | 1 + .../HostBuilderExtensions.cs | 11 ++++++-- src/Elastic.Apm/Api/ApiConstants.cs | 4 +-- ...AzureBlobStorageDiagnosticListenerTests.cs | 25 ++++++++++--------- ...FileShareStorageDiagnosticListenerTests.cs | 21 ++++++++-------- ...zureQueueStorageDiagnosticListenerTests.cs | 7 +++--- 12 files changed, 65 insertions(+), 52 deletions(-) diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh index 83f69ce75..b32575f14 100755 --- a/.ci/linux/deploy.sh +++ b/.ci/linux/deploy.sh @@ -19,7 +19,8 @@ declare -a projectsToPublish=( "Elastic.Apm.GrpcClient" "Elastic.Apm.Extensions.Logging" "Elastic.Apm.StackExchange.Redis" -"Elastic.Apm.Azure.ServiceBus") +"Elastic.Apm.Azure.ServiceBus" +"Elastic.Apm.Azure.Storage") for project in "${projectsToPublish[@]}" do diff --git a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs index 89a265418..2f8675508 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs @@ -19,13 +19,12 @@ internal static class AzureBlobStorage { internal const string SpanName = "AzureBlob"; internal const string SubType = "azureblob"; - internal const string Type = "storage"; } /// /// Creates transactions and spans for Azure Blob Storage diagnostic events from Azure.Storage.Blobs /// - public class AzureBlobStorageDiagnosticListener : DiagnosticListenerBase + internal class AzureBlobStorageDiagnosticListener : DiagnosticListenerBase { private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); @@ -121,7 +120,7 @@ private void OnStart(KeyValuePair kv, string action) var spanName = $"{AzureBlobStorage.SpanName} {action} {blobUrl.ResourceName}"; - var span = currentSegment.StartSpan(spanName, AzureBlobStorage.Type, AzureBlobStorage.SubType, action); + var span = currentSegment.StartSpan(spanName, ApiConstants.TypeStorage, AzureBlobStorage.SubType, action); span.Context.Destination = new Destination { Address = blobUrl.FullyQualifiedNamespace, @@ -129,7 +128,7 @@ private void OnStart(KeyValuePair kv, string action) { Name = AzureBlobStorage.SubType, Resource = $"{AzureBlobStorage.SubType}/{blobUrl.ResourceName}", - Type = AzureBlobStorage.Type + Type = ApiConstants.TypeStorage } }; diff --git a/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs index df0cb8591..2da38a86b 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs @@ -18,13 +18,12 @@ internal static class AzureFileStorage { internal const string SpanName = "AzureFile"; internal const string SubType = "azurefile"; - internal const string Type = "storage"; } /// /// Creates transactions and spans for Azure File Share Storage diagnostic events from Azure.Storage.Files.Shares /// - public class AzureFileShareStorageDiagnosticListener : DiagnosticListenerBase + internal class AzureFileShareStorageDiagnosticListener : DiagnosticListenerBase { private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); @@ -102,7 +101,7 @@ private void OnStart(KeyValuePair kv, string action) var fileShareUrl = new FileShareUrl(urlTag); var spanName = $"{AzureFileStorage.SpanName} {action} {fileShareUrl.ResourceName}"; - var span = currentSegment.StartSpan(spanName, AzureFileStorage.Type, AzureFileStorage.SubType, action); + var span = currentSegment.StartSpan(spanName, ApiConstants.TypeStorage, AzureFileStorage.SubType, action); span.Context.Destination = new Destination { Address = fileShareUrl.FullyQualifiedNamespace, @@ -110,7 +109,7 @@ private void OnStart(KeyValuePair kv, string action) { Name = AzureFileStorage.SubType, Resource = $"{AzureFileStorage.SubType}/{fileShareUrl.ResourceName}", - Type = AzureFileStorage.Type + Type = ApiConstants.TypeStorage } }; diff --git a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs index e673eca4e..46e00902c 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs @@ -18,7 +18,6 @@ namespace Elastic.Apm.Azure.Storage internal static class AzureQueueStorage { internal const string SpanName = "AzureQueue"; - internal const string Type = "messaging"; internal const string SubType = "azurequeue"; } @@ -26,18 +25,17 @@ internal static class AzureQueueStorage /// /// Creates transactions and spans for Azure Queue Storage diagnostic events from Azure.Storage.Queues /// - public class AzureQueueStorageDiagnosticListener : DiagnosticListenerBase + internal class AzureQueueStorageDiagnosticListener : DiagnosticListenerBase { private readonly ApmAgent _realAgent; - private readonly Service _service; + private readonly Framework _framework; private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); public AzureQueueStorageDiagnosticListener(IApmAgent agent) : base(agent) { _realAgent = agent as ApmAgent; - _service = Service.GetDefaultService(agent.ConfigurationReader, agent.Logger); - _service.Framework = new Framework { Name = AzureQueueStorage.SpanName }; + _framework = new Framework { Name = AzureQueueStorage.SpanName }; } public override string Name { get; } = "Azure.Storage.Queues"; @@ -111,7 +109,7 @@ private void OnSendStart(KeyValuePair kv) ? $"{AzureQueueStorage.SpanName} SEND" : $"{AzureQueueStorage.SpanName} SEND to {queueName}"; - var span = currentSegment.StartSpan(spanName, AzureQueueStorage.Type, AzureQueueStorage.SubType, "send"); + var span = currentSegment.StartSpan(spanName, ApiConstants.TypeMessaging, AzureQueueStorage.SubType, "send"); span.Context.Destination = new Destination { Address = destinationAddress, @@ -119,7 +117,7 @@ private void OnSendStart(KeyValuePair kv) { Name = AzureQueueStorage.SubType, Resource = queueName is null ? AzureQueueStorage.SubType : $"{AzureQueueStorage.SubType}/{queueName}", - Type = AzureQueueStorage.Type + Type = ApiConstants.TypeMessaging } }; @@ -153,11 +151,10 @@ private void OnReceiveStart(KeyValuePair kv) ? $"{AzureQueueStorage.SpanName} RECEIVE" : $"{AzureQueueStorage.SpanName} RECEIVE from {queueName}"; - var transaction = ApmAgent.Tracer.StartTransaction(transactionName, AzureQueueStorage.Type); - transaction.Context.Service = _service; + var transaction = ApmAgent.Tracer.StartTransaction(transactionName, ApiConstants.TypeMessaging); + transaction.Context.Service = new Service(null, null) { Framework = _framework }; // transaction creation will create an activity, so use this as the key. - // TODO: change when existing activity is used. var activityId = Activity.Current.Id; if (!_processingSegments.TryAdd(activityId, transaction)) diff --git a/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj b/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj index 621b3297f..84aa531f8 100644 --- a/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj +++ b/src/Elastic.Apm.Azure.Storage/Elastic.Apm.Azure.Storage.csproj @@ -6,8 +6,7 @@ Elastic.Apm.Azure.Storage Elastic.Apm.Azure.Storage Elastic.Apm.Azure.Storage - Elastic APM for Azure Storage. This package contains auto instrumentation for Azure.Storage.Queues, - and Azure.Storage.Blobs packages. + Elastic APM for Azure Storage. This package contains auto instrumentation for Azure.Storage.Queues, Azure.Storage.Blobs and Azure.Storage.Files.Shares packages. apm, monitoring, elastic, elasticapm, analytics, azure, storage, queue, blob true diff --git a/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs b/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs index 04576f747..85e46e241 100644 --- a/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs +++ b/src/Elastic.Apm.NetCoreAll/ApmMiddlewareExtension.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Apm.Azure.ServiceBus; +using Elastic.Apm.Azure.Storage; using Elastic.Apm.DiagnosticSource; using Elastic.Apm.Elasticsearch; using Elastic.Apm.EntityFrameworkCore; @@ -23,7 +24,10 @@ public static class ApmMiddlewareExtension /// . /// , /// , - /// and + /// , + /// , + /// , + /// and . /// This method turns on ASP.NET Core monitoring with every other related monitoring components, for example the agent /// will also automatically trace outgoing HTTP requests and database statements. /// @@ -45,6 +49,9 @@ public static IApplicationBuilder UseAllElasticApm( new ElasticsearchDiagnosticsSubscriber(), new GrpcClientDiagnosticSubscriber(), new AzureMessagingServiceBusDiagnosticsSubscriber(), - new MicrosoftAzureServiceBusDiagnosticsSubscriber()); + new MicrosoftAzureServiceBusDiagnosticsSubscriber(), + new AzureBlobStorageDiagnosticsSubscriber(), + new AzureQueueStorageDiagnosticsSubscriber(), + new AzureFileShareStorageDiagnosticsSubscriber()); } } diff --git a/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj b/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj index 968c76b99..9fab3eeea 100644 --- a/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj +++ b/src/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs b/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs index 511442be7..4f76c06b0 100644 --- a/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs +++ b/src/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs @@ -5,6 +5,7 @@ using Elastic.Apm.AspNetCore.DiagnosticListener; using Elastic.Apm.Azure.ServiceBus; +using Elastic.Apm.Azure.Storage; using Elastic.Apm.DiagnosticSource; using Elastic.Apm.Elasticsearch; using Elastic.Apm.EntityFrameworkCore; @@ -26,7 +27,10 @@ public static class HostBuilderExtensions /// . /// , /// , - /// and + /// , + /// , + /// , + /// and . /// /// Builder. public static IHostBuilder UseAllElasticApm(this IHostBuilder builder) => builder.UseElasticApm( @@ -37,6 +41,9 @@ public static IHostBuilder UseAllElasticApm(this IHostBuilder builder) => builde new ElasticsearchDiagnosticsSubscriber(), new GrpcClientDiagnosticSubscriber(), new AzureMessagingServiceBusDiagnosticsSubscriber(), - new MicrosoftAzureServiceBusDiagnosticsSubscriber()); + new MicrosoftAzureServiceBusDiagnosticsSubscriber(), + new AzureBlobStorageDiagnosticsSubscriber(), + new AzureQueueStorageDiagnosticsSubscriber(), + new AzureFileShareStorageDiagnosticsSubscriber()); } } diff --git a/src/Elastic.Apm/Api/ApiConstants.cs b/src/Elastic.Apm/Api/ApiConstants.cs index e6984cdea..7968b6074 100644 --- a/src/Elastic.Apm/Api/ApiConstants.cs +++ b/src/Elastic.Apm/Api/ApiConstants.cs @@ -6,8 +6,6 @@ namespace Elastic.Apm.Api { public struct ApiConstants { - public const string TypeRequest = "request"; - public const string ActionExec = "exec"; public const string ActionQuery = "query"; @@ -21,8 +19,10 @@ public struct ApiConstants public const string SubTypeGrpc = "grpc"; public const string SubTypeRedis = "redis"; + public const string TypeRequest = "request"; public const string TypeDb = "db"; public const string TypeExternal = "external"; public const string TypeMessaging = "messaging"; + public const string TypeStorage = "storage"; } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs index fb6c3116a..c2a313a56 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureBlobStorageDiagnosticListenerTests.cs @@ -6,6 +6,7 @@ using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; using Azure.Storage.Queues; +using Elastic.Apm.Api; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; using Elastic.Apm.Tests.Utilities.Azure; @@ -42,7 +43,7 @@ public async Task Capture_Span_When_Create_Container() var containerName = Guid.NewGuid().ToString(); var client = new BlobContainerClient(_environment.StorageAccountConnectionString, containerName); - await _agent.Tracer.CaptureTransaction("Create Azure Container", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Create Azure Container", ApiConstants.TypeStorage, async () => { var containerCreateResponse = await client.CreateAsync(); }); @@ -55,7 +56,7 @@ public async Task Capture_Span_When_Delete_Container() { await using var scope = await BlobContainerScope.CreateContainer(_environment.StorageAccountConnectionString); - await _agent.Tracer.CaptureTransaction("Delete Azure Container", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Delete Azure Container", ApiConstants.TypeStorage, async () => { var containerDeleteResponse = await scope.ContainerClient.DeleteAsync(); }); @@ -71,7 +72,7 @@ public async Task Capture_Span_When_Create_Page_Blob() var blobName = Guid.NewGuid().ToString(); var client = new PageBlobClient(_environment.StorageAccountConnectionString, scope.ContainerName, blobName); - await _agent.Tracer.CaptureTransaction("Create Azure Page Blob", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Create Azure Page Blob", ApiConstants.TypeStorage, async () => { var blobCreateResponse = await client.CreateAsync(1024); }); @@ -88,7 +89,7 @@ public async Task Capture_Span_When_Upload_Page_Blob() var client = new PageBlobClient(_environment.StorageAccountConnectionString, scope.ContainerName, blobName); var blobCreateResponse = await client.CreateAsync(1024); - await _agent.Tracer.CaptureTransaction("Upload Azure Page Blob", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Upload Azure Page Blob", ApiConstants.TypeStorage, async () => { var random = new Random(); var bytes = new byte[512]; @@ -109,7 +110,7 @@ public async Task Capture_Span_When_Upload_Block_Blob() var blobName = Guid.NewGuid().ToString(); var client = new BlockBlobClient(_environment.StorageAccountConnectionString, scope.ContainerName, blobName); - await _agent.Tracer.CaptureTransaction("Upload Azure Block Blob", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Upload Azure Block Blob", ApiConstants.TypeStorage, async () => { var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); var blobUploadResponse = await client.UploadAsync(stream); @@ -129,7 +130,7 @@ public async Task Capture_Span_When_Download_Blob() await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); var blobUploadResponse = await client.UploadAsync(stream); - await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", ApiConstants.TypeStorage, async () => { var downloadResponse = await client.DownloadAsync(); }); @@ -148,7 +149,7 @@ public async Task Capture_Span_When_Download_Streaming_Blob() await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); var blobUploadResponse = await client.UploadAsync(stream); - await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Download Azure Block Blob", ApiConstants.TypeStorage, async () => { stream.Position = 0; var downloadResponse = await client.DownloadToAsync(stream); @@ -166,7 +167,7 @@ public async Task Capture_Span_When_Delete_Blob() await using var stream = new MemoryStream(Encoding.UTF8.GetBytes("block blob")); var blobUploadResponse = await scope.ContainerClient.UploadBlobAsync(blobName, stream); - await _agent.Tracer.CaptureTransaction("Delete Azure Blob", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Delete Azure Blob", ApiConstants.TypeStorage, async () => { var containerDeleteResponse = await scope.ContainerClient.DeleteBlobAsync(blobName); }); @@ -186,7 +187,7 @@ public async Task Capture_Span_When_Copy_From_Uri() var blobUploadResponse = await client.UploadAsync(stream); var destinationBlobName = Guid.NewGuid().ToString(); - await _agent.Tracer.CaptureTransaction("Copy Azure Blob", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Copy Azure Blob", ApiConstants.TypeStorage, async () => { var otherClient = scope.ContainerClient.GetBlobClient(destinationBlobName); var operation = await otherClient.StartCopyFromUriAsync(client.Uri); @@ -210,7 +211,7 @@ public async Task Capture_Span_When_Get_Blobs() var blobUploadResponse = await scope.ContainerClient.UploadBlobAsync(blobName, stream); } - await _agent.Tracer.CaptureTransaction("Get Blobs", AzureBlobStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Get Blobs", ApiConstants.TypeStorage, async () => { var asyncPageable = scope.ContainerClient.GetBlobsAsync(); await foreach (var blob in asyncPageable) @@ -232,7 +233,7 @@ private void AssertSpan(string action, string resource) var span = _sender.FirstSpan; span.Name.Should().Be($"{AzureBlobStorage.SpanName} {action} {resource}"); - span.Type.Should().Be(AzureBlobStorage.Type); + span.Type.Should().Be(ApiConstants.TypeStorage); span.Subtype.Should().Be(AzureBlobStorage.SubType); span.Action.Should().Be(action); span.Context.Destination.Should().NotBeNull(); @@ -241,7 +242,7 @@ private void AssertSpan(string action, string resource) destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.BlobUrl); destination.Service.Name.Should().Be(AzureBlobStorage.SubType); destination.Service.Resource.Should().Be($"{AzureBlobStorage.SubType}/{resource}"); - destination.Service.Type.Should().Be(AzureBlobStorage.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeStorage); } } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs index 95e636f10..9cb2eddf8 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureFileShareStorageDiagnosticListenerTests.cs @@ -5,6 +5,7 @@ using Azure; using Azure.Storage.Files.Shares; using Azure.Storage.Queues; +using Elastic.Apm.Api; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; using Elastic.Apm.Tests.Utilities.Azure; @@ -38,7 +39,7 @@ public async Task Capture_Span_When_Create_File_Share() var shareName = Guid.NewGuid().ToString(); var client = new ShareClient(_environment.StorageAccountConnectionString, shareName); - await _agent.Tracer.CaptureTransaction("Create Azure File Share", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Create Azure File Share", ApiConstants.TypeStorage, async () => { var response = await client.CreateAsync(); }); @@ -52,7 +53,7 @@ public async Task Capture_Span_When_Delete_File_Share() { await using var scope = await FileShareScope.CreateShare(_environment.StorageAccountConnectionString); - await _agent.Tracer.CaptureTransaction("Delete Azure File Share", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Delete Azure File Share", ApiConstants.TypeStorage, async () => { var deleteResponse = await scope.ShareClient.DeleteAsync(); }); @@ -67,7 +68,7 @@ public async Task Capture_Span_When_Create_File_Share_Directory() var directoryName = Guid.NewGuid().ToString(); var client = scope.ShareClient.GetDirectoryClient(directoryName); - await _agent.Tracer.CaptureTransaction("Create Azure File Share Directory", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Create Azure File Share Directory", ApiConstants.TypeStorage, async () => { var response = await client.CreateAsync(); }); @@ -83,7 +84,7 @@ public async Task Capture_Span_When_Delete_File_Share_Directory() var client = scope.ShareClient.GetDirectoryClient(directoryName); var createResponse = await client.CreateAsync(); - await _agent.Tracer.CaptureTransaction("Delete Azure File Share Directory", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Delete Azure File Share Directory", ApiConstants.TypeStorage, async () => { var deleteResponse = await client.DeleteAsync(); }); @@ -98,7 +99,7 @@ public async Task Capture_Span_When_Create_File_Share_File() var fileName = Guid.NewGuid().ToString(); var client = scope.DirectoryClient.GetFileClient(fileName); - await _agent.Tracer.CaptureTransaction("Create Azure File Share File", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Create Azure File Share File", ApiConstants.TypeStorage, async () => { await client.CreateAsync(1024); }); @@ -114,7 +115,7 @@ public async Task Capture_Span_When_Delete_File_Share_File() var client = scope.DirectoryClient.GetFileClient(fileName); var createResponse = await client.CreateAsync(1024); - await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", ApiConstants.TypeStorage, async () => { var response = await client.DeleteAsync(); }); @@ -132,7 +133,7 @@ public async Task Capture_Span_When_UploadRange_File_Share_File() var bytes = Encoding.UTF8.GetBytes("temp file"); var createResponse = await client.CreateAsync(bytes.Length); - await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", ApiConstants.TypeStorage, async () => { await using var stream = new MemoryStream(bytes); var response = await client.UploadRangeAsync(new HttpRange(0, bytes.Length), stream); @@ -151,7 +152,7 @@ public async Task Capture_Span_When_Upload_File_Share_File() var bytes = Encoding.UTF8.GetBytes("temp file"); var createResponse = await client.CreateAsync(bytes.Length); - await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", AzureFileStorage.Type, async () => + await _agent.Tracer.CaptureTransaction("Delete Azure File Share File", ApiConstants.TypeStorage, async () => { await using var stream = new MemoryStream(bytes); var response = await client.UploadAsync(stream); @@ -169,7 +170,7 @@ private void AssertSpan(string action, string resource) var span = _sender.FirstSpan; span.Name.Should().Be($"{AzureFileStorage.SpanName} {action} {resource}"); - span.Type.Should().Be(AzureFileStorage.Type); + span.Type.Should().Be(ApiConstants.TypeStorage); span.Subtype.Should().Be(AzureFileStorage.SubType); span.Action.Should().Be(action); span.Context.Destination.Should().NotBeNull(); @@ -178,7 +179,7 @@ private void AssertSpan(string action, string resource) destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.FileUrl); destination.Service.Name.Should().Be(AzureFileStorage.SubType); destination.Service.Resource.Should().Be($"{AzureFileStorage.SubType}/{resource}"); - destination.Service.Type.Should().Be(AzureFileStorage.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeStorage); } } } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs index e84aa8c02..9f6c2beb2 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/AzureQueueStorageDiagnosticListenerTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Azure.Storage.Queues; +using Elastic.Apm.Api; using Elastic.Apm.Logging; using Elastic.Apm.Tests.Utilities; using Elastic.Apm.Tests.Utilities.Azure; @@ -79,7 +80,7 @@ private void AssertTransaction(string action, string queueName) var transaction = _sender.FirstTransaction; transaction.Name.Should().Be($"{AzureQueueStorage.SpanName} {action} from {queueName}"); - transaction.Type.Should().Be(AzureQueueStorage.Type); + transaction.Type.Should().Be(ApiConstants.TypeMessaging); } private void AssertSpan(string action, string queueName) @@ -91,7 +92,7 @@ private void AssertSpan(string action, string queueName) var span = _sender.FirstSpan; span.Name.Should().Be($"{AzureQueueStorage.SpanName} {action} to {queueName}"); - span.Type.Should().Be(AzureQueueStorage.Type); + span.Type.Should().Be(ApiConstants.TypeMessaging); span.Subtype.Should().Be(AzureQueueStorage.SubType); span.Action.Should().Be(action.ToLowerInvariant()); span.Context.Destination.Should().NotBeNull(); @@ -100,7 +101,7 @@ private void AssertSpan(string action, string queueName) destination.Address.Should().Be(_environment.StorageAccountConnectionStringProperties.QueueUrl); destination.Service.Name.Should().Be(AzureQueueStorage.SubType); destination.Service.Resource.Should().Be($"{AzureQueueStorage.SubType}/{queueName}"); - destination.Service.Type.Should().Be(AzureQueueStorage.Type); + destination.Service.Type.Should().Be(ApiConstants.TypeMessaging); } } } From 71ca99acecfc70a159301b2aee70850edf144b8c Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 8 Apr 2021 10:59:31 +1000 Subject: [PATCH 32/32] Address PR feedback --- docs/setup.asciidoc | 2 +- .../AzureBlobStorageDiagnosticListener.cs | 30 +++++----- ...AzureFileShareStorageDiagnosticListener.cs | 29 +++++----- .../AzureQueueStorageDiagnosticListener.cs | 56 ++++++++++--------- .../BlobContainerScope.cs | 1 - 5 files changed, 63 insertions(+), 55 deletions(-) diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index b04d3ca87..dbcc528c3 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -65,7 +65,7 @@ This packages contains instrumentation to capture transactions and spans for mes https://www.nuget.org/packages/Elastic.Apm.Azure.Storage[**Elastic.Apm.Azure.Storage**]:: -This packages contains instrumentation to capture spans for interation with Azure Storage with https://www.nuget.org/packages/azure.storage.queues/[Azure.Storage.Queues], https://www.nuget.org/packages/azure.storage.blobs/[Azure.Storage.Blobs] and https://www.nuget.org/packages/azure.storage.files.shares/[Azure.Storage.Files.Shares] packages. +This packages contains instrumentation to capture spans for interaction with Azure Storage with https://www.nuget.org/packages/azure.storage.queues/[Azure.Storage.Queues], https://www.nuget.org/packages/azure.storage.blobs/[Azure.Storage.Blobs] and https://www.nuget.org/packages/azure.storage.files.shares/[Azure.Storage.Files.Shares] packages. [[setup-dotnet-net-core]] diff --git a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs index 2f8675508..b3ad64719 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureBlobStorageDiagnosticListener.cs @@ -10,7 +10,6 @@ using System.Linq; using Elastic.Apm.Api; using Elastic.Apm.DiagnosticListeners; -using Elastic.Apm.Helpers; using Elastic.Apm.Logging; namespace Elastic.Apm.Azure.Storage @@ -134,11 +133,12 @@ private void OnStart(KeyValuePair kv, string action) if (!_processingSegments.TryAdd(activity.Id, span)) { - Logger.Trace()?.Log( - "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", - action, - span.Id, - activity.Id); + Logger.Trace() + ?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + action, + span.Id, + activity.Id); } } @@ -153,9 +153,10 @@ private void OnStop() if (!_processingSegments.TryRemove(activity.Id, out var segment)) { - Logger.Trace()?.Log( - "Could not find segment for activity {ActivityId} in tracked segments", - activity.Id); + Logger.Trace() + ?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; } @@ -174,9 +175,10 @@ private void OnException(KeyValuePair kv) if (!_processingSegments.TryRemove(activity.Id, out var segment)) { - Logger.Trace()?.Log( - "Could not find segment for activity {ActivityId} in tracked segments", - activity.Id); + Logger.Trace() + ?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; } @@ -197,9 +199,9 @@ public BlobUrl(string url) ResourceName = builder.Uri.AbsolutePath.TrimStart('/'); } - public string ResourceName { get; } - public string FullyQualifiedNamespace { get; } + + public string ResourceName { get; } } } } diff --git a/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs index 2da38a86b..276b551e9 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureFileShareStorageDiagnosticListener.cs @@ -115,11 +115,12 @@ private void OnStart(KeyValuePair kv, string action) if (!_processingSegments.TryAdd(activity.Id, span)) { - Logger.Trace()?.Log( - "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", - action, - span.Id, - activity.Id); + Logger.Trace() + ?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + action, + span.Id, + activity.Id); } } @@ -134,9 +135,10 @@ private void OnStop() if (!_processingSegments.TryRemove(activity.Id, out var segment)) { - Logger.Trace()?.Log( - "Could not find segment for activity {ActivityId} in tracked segments", - activity.Id); + Logger.Trace() + ?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; } @@ -155,9 +157,10 @@ private void OnException(KeyValuePair kv) if (!_processingSegments.TryRemove(activity.Id, out var segment)) { - Logger.Trace()?.Log( - "Could not find segment for activity {ActivityId} in tracked segments", - activity.Id); + Logger.Trace() + ?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; } @@ -178,9 +181,9 @@ public FileShareUrl(string url) ResourceName = builder.Uri.AbsolutePath.TrimStart('/'); } - public string ResourceName { get; } - public string FullyQualifiedNamespace { get; } + + public string ResourceName { get; } } } } diff --git a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs index 46e00902c..ca93e2d0c 100644 --- a/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs +++ b/src/Elastic.Apm.Azure.Storage/AzureQueueStorageDiagnosticListener.cs @@ -19,7 +19,6 @@ internal static class AzureQueueStorage { internal const string SpanName = "AzureQueue"; internal const string SubType = "azurequeue"; - } /// @@ -27,11 +26,13 @@ internal static class AzureQueueStorage /// internal class AzureQueueStorageDiagnosticListener : DiagnosticListenerBase { - private readonly ApmAgent _realAgent; private readonly Framework _framework; + private readonly ConcurrentDictionary _processingSegments = new ConcurrentDictionary(); + private readonly ApmAgent _realAgent; + public AzureQueueStorageDiagnosticListener(IApmAgent agent) : base(agent) { _realAgent = agent as ApmAgent; @@ -123,11 +124,12 @@ private void OnSendStart(KeyValuePair kv) if (!_processingSegments.TryAdd(activity.Id, span)) { - Logger.Trace()?.Log( - "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", - "SEND", - span.Id, - activity.Id); + Logger.Trace() + ?.Log( + "Could not add {Action} span {SpanId} for activity {ActivityId} to tracked spans", + "SEND", + span.Id, + activity.Id); } } @@ -159,11 +161,12 @@ private void OnReceiveStart(KeyValuePair kv) if (!_processingSegments.TryAdd(activityId, transaction)) { - Logger.Error()?.Log( - "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", - "RECEIVE", - transaction.Id, - activity.Id); + Logger.Error() + ?.Log( + "Could not add {Action} transaction {TransactionId} for activity {ActivityId} to tracked segments", + "RECEIVE", + transaction.Id, + activity.Id); } } @@ -174,10 +177,11 @@ private bool MatchesIgnoreMessageQueues(string name) var matcher = WildcardMatcher.AnyMatch(_realAgent.ConfigStore.CurrentSnapshot.IgnoreMessageQueues, name); if (matcher != null) { - Logger.Debug()?.Log( - "Not tracing message from {QueueName} because it matched IgnoreMessageQueues pattern {Matcher}", - name, - matcher.GetMatcher()); + Logger.Debug() + ?.Log( + "Not tracing message from {QueueName} because it matched IgnoreMessageQueues pattern {Matcher}", + name, + matcher.GetMatcher()); return true; } } @@ -196,9 +200,10 @@ private void OnStop() if (!_processingSegments.TryRemove(activity.Id, out var segment)) { - Logger.Trace()?.Log( - "Could not find segment for activity {ActivityId} in tracked segments", - activity.Id); + Logger.Trace() + ?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; } @@ -217,9 +222,10 @@ private void OnException(KeyValuePair kv) if (!_processingSegments.TryRemove(activity.Id, out var segment)) { - Logger.Trace()?.Log( - "Could not find segment for activity {ActivityId} in tracked segments", - activity.Id); + Logger.Trace() + ?.Log( + "Could not find segment for activity {ActivityId} in tracked segments", + activity.Id); return; } @@ -246,11 +252,9 @@ public QueueUrl(string url) : null; } - public string QueueName { get; } - public string FullyQualifiedNamespace { get; } + + public string QueueName { get; } } } - - } diff --git a/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs b/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs index 8a9ab6836..ac05f9f43 100644 --- a/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs +++ b/test/Elastic.Apm.Azure.Storage.Tests/BlobContainerScope.cs @@ -6,7 +6,6 @@ using System; using System.Threading.Tasks; using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; namespace Elastic.Apm.Azure.Storage.Tests {