Skip to content

feat: add activity on connection #1734

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions projects/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
<PackageVersion Include="EasyNetQ.Management.Client" Version="3.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Nullable" Version="1.3.1" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.11.2" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.9.0" />
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.4" />
<!--
Note: do NOT upgrade the System.IO.Pipelines dependency unless necessary
See https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1481#pullrequestreview-1847905299
Expand All @@ -33,7 +34,6 @@
* https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1481#pullrequestreview-1847905299
* https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1594
-->
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.Threading.Channels" Version="8.0.0" />
<PackageVersion Include="System.Net.Http.Json" Version="8.0.1" />
Expand All @@ -44,4 +44,4 @@
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<GlobalPackageReference Include="MinVer" Version="6.0.0" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

<ItemGroup>
<PackageReference Include="OpenTelemetry.Api" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework) == 'netstandard2.0'">
Expand Down
8 changes: 2 additions & 6 deletions projects/RabbitMQ.Client/Impl/Channel.BasicPublish.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ await MaybeEnforceFlowControlAsync(cancellationToken)

var cmd = new BasicPublish(exchange, routingKey, mandatory, default);

using Activity? sendActivity = RabbitMQActivitySource.PublisherHasListeners
? RabbitMQActivitySource.BasicPublish(routingKey, exchange, body.Length)
: default;
using Activity? sendActivity = RabbitMQActivitySource.BasicPublish(routingKey, exchange, body.Length);

ulong publishSequenceNumber = 0;
if (publisherConfirmationInfo is not null)
Expand Down Expand Up @@ -115,9 +113,7 @@ await MaybeEnforceFlowControlAsync(cancellationToken)

var cmd = new BasicPublishMemory(exchange.Bytes, routingKey.Bytes, mandatory, default);

using Activity? sendActivity = RabbitMQActivitySource.PublisherHasListeners
? RabbitMQActivitySource.BasicPublish(routingKey.Value, exchange.Value, body.Length)
: default;
using Activity? sendActivity = RabbitMQActivitySource.BasicPublish(routingKey.Value, exchange.Value, body.Length);

ulong publishSequenceNumber = 0;
if (publisherConfirmationInfo is not null)
Expand Down
7 changes: 4 additions & 3 deletions projects/RabbitMQ.Client/Impl/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,10 @@ internal void TakeOver(Connection other)
internal async ValueTask<IConnection> OpenAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

using Activity? connectionActivity = RabbitMQActivitySource.OpenConnection(_frameHandler);
try
{
RabbitMqClientEventSource.Log.ConnectionOpened();

cancellationToken.ThrowIfCancellationRequested();

// Note: this must happen *after* the frame handler is started
Expand All @@ -250,8 +249,10 @@ await _channel0.ConnectionOpenAsync(_config.VirtualHost, cancellationToken)

return this;
}
catch
catch (Exception ex)
{
connectionActivity?.SetStatus(ActivityStatusCode.Error);
connectionActivity?.AddException(ex);
try
{
var ea = new ShutdownEventArgs(ShutdownInitiator.Library, Constants.InternalError, "FailedOpen");
Expand Down
42 changes: 17 additions & 25 deletions projects/RabbitMQ.Client/Impl/RabbitMQActivitySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,20 @@ public static class RabbitMQActivitySource
private static readonly ActivitySource s_subscriberSource =
new ActivitySource(SubscriberSourceName, AssemblyVersion);

private static readonly ActivitySource s_connectionSource =
new ActivitySource(ConnectionSourceName, AssemblyVersion);

public const string PublisherSourceName = "RabbitMQ.Client.Publisher";
public const string SubscriberSourceName = "RabbitMQ.Client.Subscriber";
public const string ConnectionSourceName = "RabbitMQ.Client.Connection";

public static Action<Activity, IDictionary<string, object?>> ContextInjector { get; set; } = DefaultContextInjector;
public static Action<Activity, IDictionary<string, object?>> ContextInjector { get; set; } =
DefaultContextInjector;

public static Func<IReadOnlyBasicProperties, ActivityContext> ContextExtractor { get; set; } =
DefaultContextExtractor;

public static bool UseRoutingKeyAsOperationName { get; set; } = true;
internal static bool PublisherHasListeners => s_publisherSource.HasListeners();

internal static readonly IEnumerable<KeyValuePair<string, object?>> CreationTags = new[]
{
Expand All @@ -61,14 +65,18 @@ public static class RabbitMQActivitySource
new KeyValuePair<string, object?>(ProtocolVersion, "0.9.1")
};

internal static Activity? OpenConnection(IFrameHandler frameHandler)
{
Activity? connectionActivity =
s_connectionSource.StartRabbitMQActivity("connection attempt", ActivityKind.Client);
connectionActivity?
.SetNetworkTags(frameHandler);
return connectionActivity;
}

internal static Activity? BasicPublish(string routingKey, string exchange, int bodySize,
ActivityContext linkedContext = default)
{
if (!s_publisherSource.HasListeners())
{
return null;
}

Activity? activity = linkedContext == default
? s_publisherSource.StartRabbitMQActivity(
UseRoutingKeyAsOperationName ? $"{MessagingOperationNameBasicPublish} {routingKey}" : MessagingOperationNameBasicPublish,
Expand All @@ -82,16 +90,10 @@ public static class RabbitMQActivitySource
}

return activity;

}

internal static Activity? BasicGetEmpty(string queue)
{
if (!s_subscriberSource.HasListeners())
{
return null;
}

Activity? activity = s_subscriberSource.StartRabbitMQActivity(
UseRoutingKeyAsOperationName ? $"{MessagingOperationNameBasicGetEmpty} {queue}" : MessagingOperationNameBasicGetEmpty,
ActivityKind.Consumer);
Expand All @@ -109,11 +111,6 @@ public static class RabbitMQActivitySource
internal static Activity? BasicGet(string routingKey, string exchange, ulong deliveryTag,
IReadOnlyBasicProperties readOnlyBasicProperties, int bodySize)
{
if (!s_subscriberSource.HasListeners())
{
return null;
}

// Extract the PropagationContext of the upstream parent from the message headers.
Activity? activity = s_subscriberSource.StartLinkedRabbitMQActivity(
UseRoutingKeyAsOperationName ? $"{MessagingOperationNameBasicGet} {routingKey}" : MessagingOperationNameBasicGet, ActivityKind.Consumer,
Expand All @@ -130,11 +127,6 @@ public static class RabbitMQActivitySource
internal static Activity? Deliver(string routingKey, string exchange, ulong deliveryTag,
IReadOnlyBasicProperties basicProperties, int bodySize)
{
if (!s_subscriberSource.HasListeners())
{
return null;
}

// Extract the PropagationContext of the upstream parent from the message headers.
Activity? activity = s_subscriberSource.StartLinkedRabbitMQActivity(
UseRoutingKeyAsOperationName ? $"{MessagingOperationNameBasicDeliver} {routingKey}" : MessagingOperationNameBasicDeliver,
Expand Down Expand Up @@ -197,15 +189,15 @@ private static void PopulateMessagingTags(string operationType, string operation

internal static void PopulateMessageEnvelopeSize(Activity? activity, int size)
{
if (activity != null && activity.IsAllDataRequested && PublisherHasListeners)
if (activity?.IsAllDataRequested ?? false)
{
activity.SetTag(MessagingEnvelopeSize, size);
}
}

internal static void SetNetworkTags(this Activity? activity, IFrameHandler frameHandler)
{
if (PublisherHasListeners && activity != null && activity.IsAllDataRequested)
if (activity?.IsAllDataRequested ?? false)
{
switch (frameHandler.RemoteEndPoint.AddressFamily)
{
Expand Down
1 change: 1 addition & 0 deletions projects/RabbitMQ.Client/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const RabbitMQ.Client.RabbitMQActivitySource.ConnectionSourceName = "RabbitMQ.Client.Connection" -> string!
2 changes: 1 addition & 1 deletion projects/RabbitMQ.Client/RabbitMQ.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<PackageReference Include="System.IO.Pipelines" />
<PackageReference Include="System.Threading.RateLimiting" />
<PackageReference Include="Nullable" PrivateAssets="all" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework) == 'netstandard2.0'">
Expand All @@ -76,7 +77,6 @@
* https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1481#pullrequestreview-1847905299
* https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1594
-->
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<PackageReference Include="System.Memory" />
<PackageReference Include="System.Threading.Channels" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
Expand Down
187 changes: 187 additions & 0 deletions projects/Test/Common/ActivityRecorder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// This source code is dual-licensed under the Apache License, version
// 2.0, and the Mozilla Public License, version 2.0.
//
// The APL v2.0:
//
//---------------------------------------------------------------------------
// Copyright (c) 2007-2025 Broadcom. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//---------------------------------------------------------------------------
//
// The MPL v2.0:
//
//---------------------------------------------------------------------------
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2007-2025 Broadcom. All Rights Reserved.
//---------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using Xunit;

namespace Test
{
public class ActivityRecorder : IDisposable
{
private string _activitySourceName;
private string _activityName;

private readonly ActivityListener _listener;
private List<Activity> _finishedActivities = new();

private int _started;
private int _stopped;

public int Started => _started;
public int Stopped => _stopped;

public Predicate<Activity> Filter { get; set; } = _ => true;
public bool VerifyParent { get; set; } = true;
public Activity ExpectedParent { get; set; }

public Activity LastStartedActivity { get; private set; }
public Activity LastFinishedActivity { get; private set; }
public IEnumerable<Activity> FinishedActivities => _finishedActivities;

public ActivityRecorder(string activitySourceName, string activityName)
{
_activitySourceName = activitySourceName;
_activityName = activityName;
_listener = new ActivityListener
{
ShouldListenTo = (activitySource) => activitySource.Name == _activitySourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
ActivityStarted = (activity) =>
{
if (activity.OperationName == _activityName && Filter(activity))
{
if (VerifyParent)
{
Assert.Same(ExpectedParent, activity.Parent);
}

Interlocked.Increment(ref _started);

LastStartedActivity = activity;
}
},
ActivityStopped = (activity) =>
{
if (activity.OperationName == _activityName && Filter(activity))
{
if (VerifyParent)
{
Assert.Same(ExpectedParent, activity.Parent);
}

Interlocked.Increment(ref _stopped);

lock (_finishedActivities)
{
LastFinishedActivity = activity;
_finishedActivities.Add(activity);
}
}
}
};

ActivitySource.AddActivityListener(_listener);
}

public void Dispose() => _listener.Dispose();

public void VerifyActivityRecorded(int times)
{
Assert.Equal(times, Started);
Assert.Equal(times, Stopped);
}

public Activity VerifyActivityRecordedOnce()
{
VerifyActivityRecorded(1);
return LastFinishedActivity;
}
}

public static class ActivityAssert
{
public static KeyValuePair<string, object> HasTag(this Activity activity, string name)
{
KeyValuePair<string, object> tag = activity.TagObjects.SingleOrDefault(t => t.Key == name);
if (tag.Key is null)
{
Assert.Fail($"The Activity tags should contain {name}.");
}

return tag;
}

public static void HasTag<T>(this Activity activity, string name, T expectedValue)
{
KeyValuePair<string, object> tag = HasTag(activity, name);
Assert.Equal(expectedValue, (T)tag.Value);
}

public static void HasRecordedException(this Activity activity, Exception exception)
{
var exceptionEvent = activity.Events.First();
Assert.Equal("exception", activity.Events.First().Name);
Assert.Equal(exception.GetType().ToString(),
exceptionEvent.Tags.SingleOrDefault(t => t.Key == "exception.type").Value);
}

public static void IsInError(this Activity activity)
{
Assert.Equal(ActivityStatusCode.Error, activity.Status);
}

public static void HasNoTag(this Activity activity, string name)
{
bool contains = activity.TagObjects.Any(t => t.Key == name);
Assert.False(contains, $"The Activity tags should not contain {name}.");
}

public static void FinishedInOrder(this Activity first, Activity second)
{
Assert.True(first.StartTimeUtc + first.Duration < second.StartTimeUtc + second.Duration,
$"{first.OperationName} should stop before {second.OperationName}");
}

public static string CamelToSnake(string camel)
{
if (string.IsNullOrEmpty(camel)) return camel;
StringBuilder bld = new();
bld.Append(char.ToLower(camel[0]));
for (int i = 1; i < camel.Length; i++)
{
char c = camel[i];
if (char.IsUpper(c))
{
bld.Append('_');
}

bld.Append(char.ToLower(c));
}

return bld.ToString();
}
}
}
Loading
Loading