From 20c1d74aa975ece44dbe407834ddbae9805dbe2d Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Wed, 22 May 2024 06:10:39 +0300 Subject: [PATCH] Replace our own instrumentation for MongoDB with MongoDB.Driver.Core.Extensions.DiagnosticSources The less maintenance work around such, the better. Effects: 1. Change listened to ActivitySource from `Tingle.Extensions.MongoDB` to `MongoDB.Driver.Core.Extensions.DiagnosticSources` 2. The tags use for instrumentation are the official supported ones by OpenTelemetry. See [docs](https://opentelemetry.io/docs/specs/semconv/database/database-spans/) --- .../Diagnostics/MongoDbDiagnosticEvents.cs | 145 -------- .../MongoDbContextOptions.cs | 58 +-- src/Tingle.Extensions.MongoDB/README.md | 2 +- .../Tingle.Extensions.MongoDB.csproj | 1 + .../MongoDbDiagnosticEventsTests.cs | 329 ------------------ 5 files changed, 15 insertions(+), 520 deletions(-) delete mode 100644 src/Tingle.Extensions.MongoDB/Diagnostics/MongoDbDiagnosticEvents.cs delete mode 100644 tests/Tingle.Extensions.MongoDB.Tests/MongoDbDiagnosticEventsTests.cs diff --git a/src/Tingle.Extensions.MongoDB/Diagnostics/MongoDbDiagnosticEvents.cs b/src/Tingle.Extensions.MongoDB/Diagnostics/MongoDbDiagnosticEvents.cs deleted file mode 100644 index 7da6149..0000000 --- a/src/Tingle.Extensions.MongoDB/Diagnostics/MongoDbDiagnosticEvents.cs +++ /dev/null @@ -1,145 +0,0 @@ -using MongoDB.Driver.Core.Events; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Net; -using System.Reflection; - -namespace Tingle.Extensions.MongoDB.Diagnostics; - -/// -/// A subscriber to events that writes to a -/// -/// -/// This class is highly borrowed from https://github.com/jbogard/MongoDB.Driver.Core.Extensions.DiagnosticSources -/// -public class MongoDbDiagnosticEvents : IEventSubscriber -{ - internal static readonly AssemblyName AssemblyName = typeof(MongoDbDiagnosticEvents).Assembly.GetName(); - internal static readonly string ActivitySourceName = AssemblyName.Name!; - internal static readonly Version Version = AssemblyName.Version!; - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); - - private const string ActivityName = "MongoDB"; - - private readonly bool captureCommandText = false; // Ideally should be provided via constructor - private readonly Func? shouldStartActivity; - private readonly ReflectionEventSubscriber subscriber; - private readonly ConcurrentDictionary activityMap = new(); - - /// - /// Creates an instance of . - /// - /// indicates if the command text should be captured - /// optional delegate to check if an activity should be started - public MongoDbDiagnosticEvents(bool captureCommandText = false, Func? shouldStartActivity = null) - { - this.captureCommandText = captureCommandText; - this.shouldStartActivity = shouldStartActivity; - - // the reflection-based subscriber accepts any objects, for this case, we take non public ones - subscriber = new ReflectionEventSubscriber(this, bindingFlags: BindingFlags.Instance | BindingFlags.NonPublic); - } - - /// - /// Tries to get an event handler for an event of type TEvent. - /// - /// The type of the event. - /// The handler. - /// true if this subscriber has provided an event handler; otherwise false. - public bool TryGetEventHandler(out Action handler) => subscriber.TryGetEventHandler(out handler); - -#pragma warning disable IDE0051 // Remove unused private members - - private void Handle(CommandStartedEvent @event) - { - if (shouldStartActivity != null && !shouldStartActivity(@event)) - { - return; - } - - var activity = ActivitySource.StartActivity(ActivityName, ActivityKind.Client); - - // if the activity is null, there is no one listening so just return - if (activity == null) return; - - var collectionName = @event.GetCollectionName(); - - // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md - activity.DisplayName = collectionName == null ? $"mongodb.{@event.CommandName}" : $"{collectionName}.{@event.CommandName}"; - - // add tags known by open telemetry - activity.AddTag("db.system", "mongodb"); - activity.AddTag("db.name", @event.DatabaseNamespace?.DatabaseName); - activity.AddTag("db.mongodb.collection", collectionName); - activity.AddTag("db.operation", @event.CommandName); - var endPoint = @event.ConnectionId?.ServerId?.EndPoint; - switch (endPoint) - { - case IPEndPoint ipe: - activity.AddTag("db.user", $"mongodb://{ipe.Address}:{ipe.Port}"); - activity.AddTag("net.peer.ip", ipe.Address.ToString()); - activity.AddTag("net.peer.port", ipe.Port.ToString()); - break; - case DnsEndPoint dnse: - activity.AddTag("db.user", $"mongodb://{dnse.Host}:{dnse.Port}"); - activity.AddTag("net.peer.name", dnse.Host); - activity.AddTag("net.peer.port", dnse.Port.ToString()); - break; - } - - if (activity.IsAllDataRequested && captureCommandText) - { - activity.AddTag("db.statement", @event.Command.ToString()); - } - - activityMap.TryAdd(@event.RequestId, activity); - } - - private void Handle(CommandSucceededEvent @event) - { - if (activityMap.TryRemove(@event.RequestId, out var activity)) - { - WithReplacedActivityCurrent(activity, () => - { - activity.AddTag("otel.status_code", "Ok"); - activity.Stop(); - }); - } - } - - private void Handle(CommandFailedEvent @event) - { - if (activityMap.TryRemove(@event.RequestId, out var activity)) - { - WithReplacedActivityCurrent(activity, () => - { - if (activity.IsAllDataRequested) - { - activity.AddTag("otel.status_code", "Error"); - activity.AddTag("otel.status_description", @event.Failure.Message); - activity.AddTag("error.type", @event.Failure.GetType().FullName); - activity.AddTag("error.msg", @event.Failure.Message); - activity.AddTag("error.stack", @event.Failure.StackTrace); - } - - activity.Stop(); - }); - } - } - - private static void WithReplacedActivityCurrent(Activity activity, Action action) - { - var current = Activity.Current; - try - { - Activity.Current = activity; - action(); - } - finally - { - Activity.Current = current; - } - } - -#pragma warning restore IDE0051 // Remove unused private members -} diff --git a/src/Tingle.Extensions.MongoDB/MongoDbContextOptions.cs b/src/Tingle.Extensions.MongoDB/MongoDbContextOptions.cs index 771a228..26fb71f 100644 --- a/src/Tingle.Extensions.MongoDB/MongoDbContextOptions.cs +++ b/src/Tingle.Extensions.MongoDB/MongoDbContextOptions.cs @@ -1,7 +1,6 @@ using MongoDB.Driver; -using MongoDB.Driver.Core.Events; +using MongoDB.Driver.Core.Extensions.DiagnosticSources; using System.Diagnostics.CodeAnalysis; -using Tingle.Extensions.MongoDB.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; @@ -122,16 +121,9 @@ public virtual MongoDbContextOptionsBuilder UseApplicationServiceProvider(IServi /// Sets the to use when configuring the context. /// /// The to be used - /// - /// Whether the command text should be captured in instrumentation. - /// - /// - /// Delegate for determining if a should be instrumented. - /// + /// The options to use for instrumentation. /// - public virtual MongoDbContextOptionsBuilder UseMongoUrl(MongoUrl url, - bool instrumentCommandText = true, - Func? shouldInstrument = null) + public virtual MongoDbContextOptionsBuilder UseMongoUrl(MongoUrl url, InstrumentationOptions? instrumentationOptions = null) { ArgumentNullException.ThrowIfNull(url); if (string.IsNullOrWhiteSpace(url.DatabaseName)) @@ -146,10 +138,7 @@ public virtual MongoDbContextOptionsBuilder UseMongoUrl(MongoUrl url, { settings.ClusterConfigurator = builder => { - builder.Subscribe( - new MongoDbDiagnosticEvents( - captureCommandText: instrumentCommandText, - shouldStartActivity: shouldInstrument)); + builder.Subscribe(new DiagnosticsActivityEventSubscriber(instrumentationOptions ?? new() { CaptureCommandText = true })); }; }); @@ -165,19 +154,12 @@ public virtual MongoDbContextOptionsBuilder UseMongoUrl(MongoUrl url, /// mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] /// e.g. mongodb://localhost:27017/myDatabase /// - /// - /// Whether the command text should be captured in instrumentation. - /// - /// - /// Delegate for determining if a should be instrumented. - /// + /// The options to use for instrumentation. /// - public virtual MongoDbContextOptionsBuilder UseMongoConnectionString(string connectionString, - bool instrumentCommandText = true, - Func? shouldInstrument = null) + public virtual MongoDbContextOptionsBuilder UseMongoConnectionString(string connectionString, InstrumentationOptions? instrumentationOptions = null) { ArgumentNullException.ThrowIfNull(connectionString); - return UseMongoUrl(new MongoUrl(connectionString), instrumentCommandText, shouldInstrument); + return UseMongoUrl(new MongoUrl(connectionString), instrumentationOptions); } /// @@ -246,17 +228,10 @@ public MongoDbContextOptionsBuilder() : this(new MongoDbContextOptions /// Sets the to use when configuring the context. /// /// The to be used - /// - /// Whether the command text should be captured in instrumentation. - /// - /// - /// Delegate for determining if a should be instrumented. - /// + /// The options to use for instrumentation. /// - public new virtual MongoDbContextOptionsBuilder UseMongoUrl(MongoUrl url, - bool instrumentCommandText = true, - Func? shouldInstrument = null) - => (MongoDbContextOptionsBuilder)base.UseMongoUrl(url, instrumentCommandText, shouldInstrument); + public new virtual MongoDbContextOptionsBuilder UseMongoUrl(MongoUrl url, InstrumentationOptions? instrumentationOptions = null) + => (MongoDbContextOptionsBuilder)base.UseMongoUrl(url, instrumentationOptions); /// /// Sets the to use when configuring the context by @@ -267,17 +242,10 @@ public MongoDbContextOptionsBuilder() : this(new MongoDbContextOptions /// mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] /// e.g. mongodb://localhost:27017/myDatabase /// - /// - /// Whether the command text should be captured in instrumentation. - /// - /// - /// Delegate for determining if a should be instrumented. - /// + /// The options to use for instrumentation. /// - public new virtual MongoDbContextOptionsBuilder UseMongoConnectionString(string connectionString, - bool instrumentCommandText = true, - Func? shouldInstrument = null) - => (MongoDbContextOptionsBuilder)base.UseMongoConnectionString(connectionString, instrumentCommandText, shouldInstrument); + public new virtual MongoDbContextOptionsBuilder UseMongoConnectionString(string connectionString, InstrumentationOptions? instrumentationOptions=null) + => (MongoDbContextOptionsBuilder)base.UseMongoConnectionString(connectionString, instrumentationOptions); /// /// Further configure the existing instance of . diff --git a/src/Tingle.Extensions.MongoDB/README.md b/src/Tingle.Extensions.MongoDB/README.md index eb2a5e8..b393077 100644 --- a/src/Tingle.Extensions.MongoDB/README.md +++ b/src/Tingle.Extensions.MongoDB/README.md @@ -49,7 +49,7 @@ If MongoDB client is configured with a connection string, add the `ConnectionStr ## Diagnostics -Events for Mongo are produced on an `ActivitySource` named `MongoDB`. This is done by registering and instance of `IEventSubscriber` named `MongoDbDiagnosticEvents` to the `ClusterConfigurator`. However, this is done automatically when using `MongoDbContext`. +Events for Mongo are produced on an `ActivitySource` named [`MongoDB.Driver.Core.Extensions.DiagnosticSources`](https://github.com/jbogard/MongoDB.Driver.Core.Extensions.DiagnosticSources), automatically when using `MongoDbContext`. ## HealthChecks diff --git a/src/Tingle.Extensions.MongoDB/Tingle.Extensions.MongoDB.csproj b/src/Tingle.Extensions.MongoDB/Tingle.Extensions.MongoDB.csproj index 172b05f..45298ab 100644 --- a/src/Tingle.Extensions.MongoDB/Tingle.Extensions.MongoDB.csproj +++ b/src/Tingle.Extensions.MongoDB/Tingle.Extensions.MongoDB.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/Tingle.Extensions.MongoDB.Tests/MongoDbDiagnosticEventsTests.cs b/tests/Tingle.Extensions.MongoDB.Tests/MongoDbDiagnosticEventsTests.cs deleted file mode 100644 index 30b89d9..0000000 --- a/tests/Tingle.Extensions.MongoDB.Tests/MongoDbDiagnosticEventsTests.cs +++ /dev/null @@ -1,329 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.Core.Clusters; -using MongoDB.Driver.Core.Connections; -using MongoDB.Driver.Core.Events; -using MongoDB.Driver.Core.Servers; -using System.Diagnostics; -using System.Net; -using Tingle.Extensions.MongoDB.Diagnostics; - -namespace Tingle.Extensions.MongoDB.Tests; - -public class MongoDbDiagnosticEventsTests -{ - static MongoDbDiagnosticEventsTests() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - Activity.ForceDefaultIdFormat = true; - } - - [Fact] - public void NoActivityCreatedWhenNoListenerIsAttached() - { - var startFired = false; - var stopFired = false; - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Nonsense", - ActivityStarted = _ => startFired = true, - ActivityStopped = _ => stopFired = true - }; - - ActivitySource.AddActivityListener(listener); - - var behavior = new MongoDbDiagnosticEvents(); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - startEvent(new CommandStartedEvent()); - stopEvent(new CommandSucceededEvent()); - - Assert.False(startFired); - Assert.False(stopFired); - } - - [Fact] - public void ActivityStartedAndStoppedWhenSampling() - { - var startFired = false; - var stopFired = false; - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Tingle.Extensions.MongoDB", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.PropagationData, - ActivityStarted = _ => startFired = true, - ActivityStopped = _ => stopFired = true - }; - ActivitySource.AddActivityListener(listener); - - var behavior = new MongoDbDiagnosticEvents(); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - startEvent(new CommandStartedEvent()); - stopEvent(new CommandSucceededEvent()); - - Assert.True(startFired); - Assert.True(stopFired); - Assert.Null(Activity.Current); - } - - [Fact] - public void StartsAndLogsSuccessfulActivity() - { - var stopFired = false; - var startFired = false; - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Tingle.Extensions.MongoDB", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.PropagationData, - ActivityStarted = activity => - { - startFired = true; - Assert.NotNull(activity); - Assert.Equal("MongoDB", Activity.Current?.OperationName); - }, - ActivityStopped = activity => - { - stopFired = true; - Assert.NotNull(activity); - Assert.Equal("MongoDB", Activity.Current?.OperationName); - } - }; - ActivitySource.AddActivityListener(listener); - - var behavior = new MongoDbDiagnosticEvents(); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - startEvent(new CommandStartedEvent()); - stopEvent(new CommandSucceededEvent()); - - Assert.True(startFired); - Assert.True(stopFired); - Assert.Null(Activity.Current); - } - - [Fact] - public void StartsAndLogsFailedActivity() - { - var exceptionFired = false; - var startFired = false; - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Tingle.Extensions.MongoDB", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, - ActivityStarted = activity => - { - startFired = true; - Assert.NotNull(activity); - Assert.Equal("MongoDB", Activity.Current?.OperationName); - }, - ActivityStopped = activity => - { - exceptionFired = true; - Assert.NotNull(activity); - Assert.Equal("MongoDB", Activity.Current?.OperationName); - var statusTag = activity.Tags.SingleOrDefault(t => t.Key == "otel.status_code"); - Assert.NotEqual(default, statusTag); - Assert.Equal("Error", statusTag.Value); - } - }; - ActivitySource.AddActivityListener(listener); - - var behavior = new MongoDbDiagnosticEvents(); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - var connectionId = new ConnectionId(new ServerId(new ClusterId(), new DnsEndPoint("localhost", 8000))); - var databaseNamespace = new DatabaseNamespace("test"); - var command = new BsonDocument(new Dictionary - { - {"update", "my_collection"} - }); - startEvent(new CommandStartedEvent("update", command, databaseNamespace, null, 1, connectionId)); - stopEvent(new CommandFailedEvent("update", databaseNamespace, new Exception("Failed"), null, 1, connectionId, TimeSpan.Zero)); - - Assert.True(startFired); - Assert.True(exceptionFired); - Assert.Null(Activity.Current); - } - - [Fact] - public void RecordsAllData() - { - var stopFired = false; - var startFired = false; - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Tingle.Extensions.MongoDB", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, - ActivityStarted = activity => - { - startFired = true; - Assert.NotNull(activity); - }, - ActivityStopped = activity => - { - stopFired = true; - Assert.NotNull(activity); - Assert.Equal("MongoDB", Activity.Current?.OperationName); - var instanceTag = activity.Tags.SingleOrDefault(t => t.Key == "db.name"); - Assert.NotEqual(default, instanceTag); - Assert.Equal("test", instanceTag.Value); - - Assert.Equal("mongodb", activity.Tags.SingleOrDefault(t => t.Key == "db.system").Value); - Assert.Equal("update", activity.Tags.SingleOrDefault(t => t.Key == "db.operation").Value); - Assert.Equal(default, activity.Tags.SingleOrDefault(t => t.Key == "db.statement").Value); - } - }; - ActivitySource.AddActivityListener(listener); - - var behavior = new MongoDbDiagnosticEvents(captureCommandText: false); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - var connectionId = new ConnectionId(new ServerId(new ClusterId(), new DnsEndPoint("localhost", 8000))); - var databaseNamespace = new DatabaseNamespace("test"); - var command = new BsonDocument(new Dictionary - { - {"update", "my_collection"} - }); - startEvent(new CommandStartedEvent("update", command, databaseNamespace, null, 1, connectionId)); - stopEvent(new CommandSucceededEvent("update", command, databaseNamespace, null, 1, connectionId, TimeSpan.Zero)); - - Assert.True(startFired); - Assert.True(stopFired); - } - - [Fact] - public void RecordsCommandTextWhenOptionIsSet() - { - var stopFired = false; - var startFired = false; - - var command = new BsonDocument(new Dictionary - { - {"update", "my_collection"} - }); - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Tingle.Extensions.MongoDB", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, - ActivityStarted = activity => - { - startFired = true; - Assert.NotNull(activity); - }, - ActivityStopped = activity => - { - Assert.NotNull(activity); - Assert.Equal("MongoDB", Activity.Current?.OperationName); - var statementTag = activity.Tags.SingleOrDefault(t => t.Key == "db.statement"); - Assert.NotEqual(default, statementTag); - Assert.Equal(command.ToString(), statementTag.Value); - - stopFired = true; - } - }; - ActivitySource.AddActivityListener(listener); - - var behavior = new MongoDbDiagnosticEvents(captureCommandText: true); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - var connectionId = new ConnectionId(new ServerId(new ClusterId(), new DnsEndPoint("localhost", 8000))); - var databaseNamespace = new DatabaseNamespace("test"); - startEvent(new CommandStartedEvent("update", command, databaseNamespace, null, 1, connectionId)); - stopEvent(new CommandSucceededEvent("update", command, databaseNamespace, null, 1, connectionId, TimeSpan.Zero)); - - Assert.True(startFired); - Assert.True(stopFired); - } - - [Fact] - public void WorksWithParallelActivities() - { - var activities = new List(); - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Tingle.Extensions.MongoDB", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStarted = _ => activities.Add(Activity.Current), - ActivityStopped = _ => activities.Add(Activity.Current) - }; - ActivitySource.AddActivityListener(listener); - - var behavior = new MongoDbDiagnosticEvents(); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - var outerActivity = new Activity("Outer"); - outerActivity.Start(); - - var connectionId = new ConnectionId(new ServerId(new ClusterId(), new DnsEndPoint("localhost", 8000))); - var databaseNamespace = new DatabaseNamespace("test"); - var updateCommand = new BsonDocument(new Dictionary - { - {"update", "my_collection"} - }); - var insertCommand = new BsonDocument(new Dictionary - { - {"insert", "my_collection"} - }); - startEvent(new CommandStartedEvent("update", updateCommand, databaseNamespace, null, 1, connectionId)); - startEvent(new CommandStartedEvent("insert", insertCommand, databaseNamespace, null, 2, connectionId)); - stopEvent(new CommandSucceededEvent("update", updateCommand, databaseNamespace, null, 1, connectionId, TimeSpan.Zero)); - stopEvent(new CommandSucceededEvent("insert", insertCommand, databaseNamespace, null, 2, connectionId, TimeSpan.Zero)); - - outerActivity.Stop(); - - Assert.Equal(4, activities.Count); - Assert.Equal(4, activities.Count(a => a != null && a.OperationName == "MongoDB")); - Assert.Null(Activity.Current); - } - - [Theory] - [InlineData(null, true)] - [InlineData(true, true)] - [InlineData(false, false)] - public void ShouldStartActivityIsRespected(bool? filterResult, bool shouldFireActivity) - { - var activities = new List(); - - using var listener = new ActivityListener - { - ShouldListenTo = source => source.Name == "Tingle.Extensions.MongoDB", - Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.PropagationData, - ActivityStarted = _ => activities.Add(Activity.Current) - }; - ActivitySource.AddActivityListener(listener); - - Func? filter = filterResult == null ? null : x => filterResult.Value; - - var behavior = new MongoDbDiagnosticEvents(shouldStartActivity: filter); - - Assert.True(behavior.TryGetEventHandler(out var startEvent)); - Assert.True(behavior.TryGetEventHandler(out var stopEvent)); - - startEvent(new CommandStartedEvent()); - stopEvent(new CommandSucceededEvent()); - - Assert.Equal(shouldFireActivity ? 1 : 0, activities.Count); - } -}