Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

**DRAFT** feat: Azure Service Bus Instrumentation #2880

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions .github/workflows/all_solutions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ jobs:
matrix:
namespace:
[
AzureServiceBus,
CosmosDB,
Couchbase,
Elasticsearch,
Expand Down
9 changes: 9 additions & 0 deletions FullAgent.sln
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Home", "src\Agent\NewRelic\
{338AD83A-ED68-438A-8FB1-E93A3AE87EA8} = {338AD83A-ED68-438A-8FB1-E93A3AE87EA8}
{37262C22-6A3A-4AD7-AB78-3853D2B2931D} = {37262C22-6A3A-4AD7-AB78-3853D2B2931D}
{3D69B4C9-FD16-461F-95AF-6FCA6EAA914E} = {3D69B4C9-FD16-461F-95AF-6FCA6EAA914E}
{4078E594-E738-48F7-A7ED-B208ADD04900} = {4078E594-E738-48F7-A7ED-B208ADD04900}
{44434B8F-EE14-49B0-855D-6EA0B48048BF} = {44434B8F-EE14-49B0-855D-6EA0B48048BF}
{4F5D77F3-B41A-44A7-AF10-2D5462CE0162} = {4F5D77F3-B41A-44A7-AF10-2D5462CE0162}
{570429FD-C785-4673-82DF-643D06B6DC53} = {570429FD-C785-4673-82DF-643D06B6DC53}
{5BBEEC11-B753-4631-BCDD-43BE73B5CCAC} = {5BBEEC11-B753-4631-BCDD-43BE73B5CCAC}
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661} = {5D74E5C5-9BA3-423B-86F7-14C2D1A14661}
{614988AD-7C73-4E71-8F95-520D5F485984} = {614988AD-7C73-4E71-8F95-520D5F485984}
{64C7F267-5185-4AB7-9EB5-0183D8BB0171} = {64C7F267-5185-4AB7-9EB5-0183D8BB0171}
{686AE051-BC8C-4AEC-803D-237AEB807CA9} = {686AE051-BC8C-4AEC-803D-237AEB807CA9}
Expand Down Expand Up @@ -221,6 +223,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicApiChangeTests", "tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memcached", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\Memcached\Memcached.csproj", "{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureServiceBus", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\AzureServiceBus\AzureServiceBus.csproj", "{4078E594-E738-48F7-A7ED-B208ADD04900}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -467,6 +471,10 @@ Global
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Release|Any CPU.Build.0 = Release|Any CPU
{4078E594-E738-48F7-A7ED-B208ADD04900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4078E594-E738-48F7-A7ED-B208ADD04900}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4078E594-E738-48F7-A7ED-B208ADD04900}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4078E594-E738-48F7-A7ED-B208ADD04900}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -538,6 +546,7 @@ Global
{338AD83A-ED68-438A-8FB1-E93A3AE87EA8} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2} = {E5B988C0-5D19-407E-8210-71FFB90C579A}
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
{4078E594-E738-48F7-A7ED-B208ADD04900} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D8B98070-6B8E-403C-A07F-A3F2E4A3A3D0}
Expand Down
2 changes: 2 additions & 0 deletions build/ArtifactBuilder/CoreAgentComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.dll",
};

var wrapperXmls = new[]
Expand Down Expand Up @@ -88,6 +89,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.Instrumentation.xml",
};

ExtensionXsd = $@"{SourceHomeBuilderPath}\extensions\extension.xsd";
Expand Down
2 changes: 2 additions & 0 deletions build/ArtifactBuilder/FrameworkAgentComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.dll",
};

var wrapperXmls = new[]
Expand Down Expand Up @@ -109,6 +110,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.Instrumentation.xml",
};

ExtensionXsd = $@"{SourceHomeBuilderPath}\extensions\extension.xsd";
Expand Down
12 changes: 12 additions & 0 deletions src/Agent/MsiInstaller/Installer/Product.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="MemcachedWrapperComponent" Guid="{2FF15179-BBEB-460C-A145-10F20C0CAD07}">
<File Id="MemcachednWrapperFile" Name="NewRelic.Providers.Wrapper.Memcached.dll" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.Memcached.dll" />
</Component>
<Component Id="AzureServiceBusWrapperComponent" Guid="{5B625630-B816-4E9D-BE30-9403D8C1682A}">
<File Id="AzureServiceBusWrapperFile" Name="NewRelic.Providers.Wrapper.AzureServiceBus.dll" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.dll" />
</Component>
</ComponentGroup>

<ComponentGroup Id="CoreNewRelic.Agent.Extensions" Directory="CoreProgramFilesExtensionsFolder">
Expand Down Expand Up @@ -481,6 +484,9 @@ SPDX-License-Identifier: Apache-2.0
</Component>
<Component Id="CoreMemcachedWrapperComponent" Guid="{1D7D04A1-24D5-4716-B7CB-EACB21D66D7D}">
<File Id="CoreMemcachedWrapperFile" Name="NewRelic.Providers.Wrapper.Memcached.dll" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.Memcached.dll" />
</Component>
<Component Id="CoreAzureServiceBusWrapperComponent" Guid="{E2F1AE1C-9EE8-410D-932B-D6B9C25FE405}">
<File Id="CoreAzureServiceBusWrapperFile" Name="NewRelic.Providers.Wrapper.AzureServiceBus.dll" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.dll" />
</Component>
</ComponentGroup>

Expand Down Expand Up @@ -600,6 +606,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="MemcachedInstrumentationComponent" Guid="{065F899F-4942-43C6-9589-538C432E3E4D}">
<File Id="MemcachedInstrumentationFile" Name="NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml" />
</Component>
<Component Id="AzureServiceBusInstrumentationComponent" Guid="{77A2B667-7535-45A6-9CAC-27BFCFEF95B8}">
<File Id="AzureServiceBusInstrumentationFile" Name="NewRelic.Providers.Wrapper.AzureServiceBus.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.Instrumentation.xml" />
</Component>
</ComponentGroup>

<ComponentGroup Id="CoreNewRelic.Agent.Extensions.Instrumentation" Directory="CoreExtensionsFolder">
Expand Down Expand Up @@ -675,6 +684,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="CoreMemcachedInstrumentationComponent" Guid="{5A78488A-837C-4CA5-BD20-4A1ED734C085}">
<File Id="CoreMemcachedInstrumentationFile" Name="NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml"/>
</Component>
<Component Id="CoreAzureServiceBusInstrumentationComponent" Guid="{78AF8B95-BC14-41DA-8046-8F9348BA4769}">
<File Id="CoreAzureServiceBusInstrumentationFile" Name="NewRelic.Providers.Wrapper.AzureServiceBus.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.AzureServiceBus.Instrumentation.xml"/>
</Component>
</ComponentGroup>

<!-- Extensions XSD-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public interface ITransaction
/// <param name="messagingSystemName"></param>
/// <param name="cloudAccountId"></param>
/// <param name="cloudRegion"></param>
/// <param name="serverAddress"></param>
/// <param name="serverPort"></param>
/// <param name="routingKey"></param>
/// <exception cref="ArgumentNullException"></exception>
/// <returns>an opaque object that will be needed when you want to end the segment.</returns>
ISegment StartMessageBrokerSegment(MethodCall methodCall, MessageBrokerDestinationType destinationType,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
<AssemblyName>NewRelic.Providers.Wrapper.AzureServiceBus</AssemblyName>
<RootNamespace>NewRelic.Providers.Wrapper.AzureServiceBus</RootNamespace>
<Description>Azure Service Bus Wrapper Provider for New Relic .NET Agent</Description>
</PropertyGroup>

<ItemGroup>
<Content Include="Instrumentation.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\NewRelic.Agent.Extensions\NewRelic.Agent.Extensions.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;

namespace NewRelic.Providers.Wrapper.AzureServiceBus;

public class AzureServiceBusReceiveWrapper : AzureServiceBusWrapperBase
{
private static readonly ConcurrentDictionary<Type, Func<object, object>> _getResultFromGenericTask = new();

public override CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo)
{
var canWrap = instrumentedMethodInfo.RequestedWrapperName.Equals(nameof(AzureServiceBusReceiveWrapper));
return new CanWrapResponse(canWrap);
}

public override AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
{
dynamic serviceBusReceiver = instrumentedMethodCall.MethodCall.InvocationTarget;
string queueName = serviceBusReceiver.EntityPath; // some-queue-name
string fqns = serviceBusReceiver.FullyQualifiedNamespace; // some-service-bus-entity.servicebus.windows.net

MessageBrokerAction action =
instrumentedMethodCall.MethodCall.Method.MethodName switch
{
"ReceiveMessagesAsync" => MessageBrokerAction.Consume,
"ReceiveDeferredMessagesAsync" => MessageBrokerAction.Consume,
"PeekMessagesInternalAsync" => MessageBrokerAction.Peek,
"AbandonMessageAsync" => MessageBrokerAction.Purge, // TODO is this correct ??? Abandon sends the message back to the queue for re-delivery
"CompleteMessageAsync" => MessageBrokerAction.Consume,
"DeadLetterInternalAsync" => MessageBrokerAction.Purge, // TODO is this correct ???
"DeferMessageAsync" => MessageBrokerAction.Consume, // TODO is this correct or should we extend MessageBrokerAction with more values???
"RenewMessageLockAsync" => MessageBrokerAction.Consume, // TODO is this correct or should we extend MessageBrokerAction with more values???
_ => throw new ArgumentOutOfRangeException(nameof(action), $"Unexpected instrumented method call: {instrumentedMethodCall.MethodCall.Method.MethodName}")
};

// start a message broker segment
var segment = transaction.StartMessageBrokerSegment(
instrumentedMethodCall.MethodCall,
MessageBrokerDestinationType.Queue,
action,
BrokerVendorName,
queueName,
serverAddress: fqns );

return instrumentedMethodCall.IsAsync
?
// return an async delegate
Delegates.GetAsyncDelegateFor<Task>(
agent,
segment,
false,
HandleResponse,
TaskContinuationOptions.ExecuteSynchronously)
: Delegates.GetDelegateFor<object>(
onFailure: transaction.NoticeError,
onComplete: segment.End,
onSuccess: ExtractDTHeadersIfAvailable);

void HandleResponse(Task responseTask)
{
try
{
if (responseTask.IsFaulted)
{
transaction.NoticeError(responseTask.Exception);
return;
}

var resultObj = GetTaskResultFromObject(responseTask);
ExtractDTHeadersIfAvailable(resultObj);
}
finally
{
segment.End();
}
}



void ExtractDTHeadersIfAvailable(object resultObj)
{
if (resultObj != null)
{
switch (instrumentedMethodCall.MethodCall.Method.MethodName)
{
case "ReceiveMessagesAsync":
case "ReceiveDeferredMessagesAsync":
case "PeekMessagesInternalAsync":
// the response contains a list of messages.
// get the first message from the response and extract DT headers
dynamic messages = resultObj;
if (messages.Count > 0)
{
var msg = messages[0];
if (msg.ApplicationProperties is ReadOnlyDictionary<string, object> applicationProperties)
{
transaction.AcceptDistributedTraceHeaders(applicationProperties, ProcessHeaders, TransportType.Queue);
}
}
break;
}
}
IEnumerable<string> ProcessHeaders(ReadOnlyDictionary<string, object> applicationProperties, string key)
{
var headerValues = new List<string>();
foreach (var item in applicationProperties)
{
if (item.Key.Equals(key, StringComparison.OrdinalIgnoreCase))
{
headerValues.Add(item.Value as string);
}
}

return headerValues;
}
}
}

private static object GetTaskResultFromObject(object taskObj)
{
var task = taskObj as Task;
if (task == null)
{
return null;
}
if (task.IsFaulted)
{
return null;
}

var getResponse = _getResultFromGenericTask.GetOrAdd(task.GetType(), t => VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(t, "Result"));
return getResponse(task);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.Providers.Wrapper;

namespace NewRelic.Providers.Wrapper.AzureServiceBus;

public class AzureServiceBusSendWrapper : AzureServiceBusWrapperBase
{
public override CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo)
{
var canWrap = instrumentedMethodInfo.RequestedWrapperName.Equals(nameof(AzureServiceBusSendWrapper));
return new CanWrapResponse(canWrap);
}

public override AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
{
dynamic serviceBusReceiver = instrumentedMethodCall.MethodCall.InvocationTarget;
string queueName = serviceBusReceiver.EntityPath; // some-queue-name
string fqns = serviceBusReceiver.FullyQualifiedNamespace; // some-service-bus-entity.servicebus.windows.net

// determine message broker action based on method name
MessageBrokerAction action =
instrumentedMethodCall.MethodCall.Method.MethodName switch
{
"SendMessagesAsync" => MessageBrokerAction.Produce,
"ScheduleMessagesAsync" => MessageBrokerAction.Produce,
"CancelScheduledMessagesAsync" => MessageBrokerAction.Purge, // TODO is this correct ???
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems correct based on the description of the method.

_ => throw new ArgumentOutOfRangeException(nameof(action), $"Unexpected instrumented method call: {instrumentedMethodCall.MethodCall.Method.MethodName}")
};

// start a message broker segment
var segment = transaction.StartMessageBrokerSegment(
instrumentedMethodCall.MethodCall,
MessageBrokerDestinationType.Queue,
action,
BrokerVendorName,
queueName,
serverAddress: fqns);

if (action == MessageBrokerAction.Produce)
{
dynamic messages = instrumentedMethodCall.MethodCall.MethodArguments[0];

// iterate all messages that are being sent,
// insert DT headers into each message
foreach (var message in messages)
{
if (message.ApplicationProperties is IDictionary<string, object> applicationProperties)
transaction.InsertDistributedTraceHeaders(applicationProperties, ProcessHeaders);
}

void ProcessHeaders(IDictionary<string, object> applicationProperties, string key, string value)
{
applicationProperties.Add(key, value);
}
}

return instrumentedMethodCall.IsAsync ? Delegates.GetAsyncDelegateFor<Task>(agent, segment) : Delegates.GetDelegateFor(segment);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.Providers.Wrapper;

namespace NewRelic.Providers.Wrapper.AzureServiceBus
{
public abstract class AzureServiceBusWrapperBase : IWrapper
{
protected const string BrokerVendorName = "AzureServiceBus";

public bool IsTransactionRequired => true; // only instrument service bus methods if we're already in a transaction
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems different compared with other MQ that we instrument. Shouldn't the receive generate a transaction?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a topic for pairing on Tuesday. But this implementation is consistent with our MSMQ instrumentation. Unlike RabbitMQ, there's not an "eventing" layer that we could instrument that would wrap processing time on the client application.

Copy link
Member Author

@tippmar-nr tippmar-nr Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we could potentially instrument ServiceBusProcessor which does implement an event-based receive model where client code is called when a message is received. I'll take a quick look to see if it's worth the effort or not.


public abstract CanWrapResponse CanWrap(InstrumentedMethodInfo instrumentedMethodInfo);

public abstract AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent,ITransaction transaction);

}
}
Loading
Loading