From 862cf5f1794c95cf0f3e6b62713b4ae9bc807c25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:56:05 +0000 Subject: [PATCH 1/2] Initial plan From fa2b7e5933f9f3ee5c7e46448caac605188086ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:06:39 +0000 Subject: [PATCH 2/2] Add OpenTelemetry instrumentation extension library for Papst.EventStore Co-authored-by: MPapst <16494676+MPapst@users.noreply.github.com> --- Papst.EventStore.Contracts.slnx | 2 + Papst.EventStore.slnx | 2 + .../EventStoreActivitySource.cs | 20 ++ .../EventStoreOpenTelemetryExtensions.cs | 51 +++++ .../InstrumentedEventStore.cs | 86 ++++++++ ...strumentedEventStoreTransactionAppender.cs | 52 +++++ .../InstrumentedEventStream.cs | 149 +++++++++++++ .../Papst.EventStore.OpenTelemetry.csproj | 28 +++ .../EventStoreOpenTelemetryTests.cs | 196 ++++++++++++++++++ ...apst.EventStore.OpenTelemetry.Tests.csproj | 38 ++++ 10 files changed, 624 insertions(+) create mode 100644 src/Papst.EventStore.OpenTelemetry/EventStoreActivitySource.cs create mode 100644 src/Papst.EventStore.OpenTelemetry/EventStoreOpenTelemetryExtensions.cs create mode 100644 src/Papst.EventStore.OpenTelemetry/InstrumentedEventStore.cs create mode 100644 src/Papst.EventStore.OpenTelemetry/InstrumentedEventStoreTransactionAppender.cs create mode 100644 src/Papst.EventStore.OpenTelemetry/InstrumentedEventStream.cs create mode 100644 src/Papst.EventStore.OpenTelemetry/Papst.EventStore.OpenTelemetry.csproj create mode 100644 tests/Papst.EventStore.OpenTelemetry.Tests/EventStoreOpenTelemetryTests.cs create mode 100644 tests/Papst.EventStore.OpenTelemetry.Tests/Papst.EventStore.OpenTelemetry.Tests.csproj diff --git a/Papst.EventStore.Contracts.slnx b/Papst.EventStore.Contracts.slnx index a82a92c..73e477a 100644 --- a/Papst.EventStore.Contracts.slnx +++ b/Papst.EventStore.Contracts.slnx @@ -8,10 +8,12 @@ + + diff --git a/Papst.EventStore.slnx b/Papst.EventStore.slnx index 0c05f62..e95c45b 100644 --- a/Papst.EventStore.slnx +++ b/Papst.EventStore.slnx @@ -29,6 +29,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/src/Papst.EventStore.OpenTelemetry/EventStoreActivitySource.cs b/src/Papst.EventStore.OpenTelemetry/EventStoreActivitySource.cs new file mode 100644 index 0000000..c63bc75 --- /dev/null +++ b/src/Papst.EventStore.OpenTelemetry/EventStoreActivitySource.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using System.Reflection; + +namespace Papst.EventStore.OpenTelemetry; + +/// +/// Provides the for Papst.EventStore OpenTelemetry instrumentation. +/// +public static class EventStoreActivitySource +{ + /// + /// The name of the ActivitySource used for all EventStore activities. + /// Use this name when configuring OpenTelemetry to listen to EventStore traces. + /// + public const string SourceName = "Papst.EventStore"; + + internal static readonly ActivitySource Source = new( + SourceName, + Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0"); +} diff --git a/src/Papst.EventStore.OpenTelemetry/EventStoreOpenTelemetryExtensions.cs b/src/Papst.EventStore.OpenTelemetry/EventStoreOpenTelemetryExtensions.cs new file mode 100644 index 0000000..9d8bb2e --- /dev/null +++ b/src/Papst.EventStore.OpenTelemetry/EventStoreOpenTelemetryExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace Papst.EventStore.OpenTelemetry; + +/// +/// Extension methods for adding OpenTelemetry instrumentation to Papst.EventStore. +/// +public static class EventStoreOpenTelemetryExtensions +{ + /// + /// Wraps the registered with an instrumented decorator that produces + /// OpenTelemetry-compatible instances for all EventStore operations. + /// + /// Call this method after registering the EventStore implementation (e.g. after AddInMemoryEventStore()). + /// To collect these traces configure your OpenTelemetry SDK to listen to the + /// activity source. + /// + /// + /// The service collection. + /// The same for chaining. + /// + /// Thrown when no has been registered before calling this method. + /// + public static IServiceCollection AddEventStoreInstrumentation(this IServiceCollection services) + { + ServiceDescriptor? descriptor = services.LastOrDefault(d => d.ServiceType == typeof(IEventStore)); + if (descriptor == null) + { + throw new InvalidOperationException( + $"No {nameof(IEventStore)} registration found. Register an EventStore implementation before calling {nameof(AddEventStoreInstrumentation)}."); + } + + services.Remove(descriptor); + + ServiceDescriptor decorated = ServiceDescriptor.Describe( + typeof(IEventStore), + sp => + { + IEventStore inner = (IEventStore)(descriptor.ImplementationInstance + ?? descriptor.ImplementationFactory?.Invoke(sp) + ?? ActivatorUtilities.CreateInstance(sp, descriptor.ImplementationType!)); + return new InstrumentedEventStore(inner); + }, + descriptor.Lifetime); + + services.Add(decorated); + return services; + } +} diff --git a/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStore.cs b/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStore.cs new file mode 100644 index 0000000..230e1ba --- /dev/null +++ b/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStore.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Papst.EventStore.OpenTelemetry; + +/// +/// A decorator for that instruments all operations with OpenTelemetry activities. +/// +internal sealed class InstrumentedEventStore : IEventStore +{ + private readonly IEventStore _inner; + + public InstrumentedEventStore(IEventStore inner) + { + _inner = inner; + } + + /// + public async Task GetAsync(Guid streamId, CancellationToken cancellationToken = default) + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStore.Get"); + activity?.SetTag("event_store.stream_id", streamId.ToString()); + + try + { + IEventStream stream = await _inner.GetAsync(streamId, cancellationToken).ConfigureAwait(false); + activity?.SetTag("event_store.stream_version", stream.Version.ToString()); + return new InstrumentedEventStream(stream); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + throw; + } + } + + /// + public async Task CreateAsync(Guid streamId, string targetTypeName, + CancellationToken cancellationToken = default) + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStore.Create"); + activity?.SetTag("event_store.stream_id", streamId.ToString()); + activity?.SetTag("event_store.target_type", targetTypeName); + + try + { + IEventStream stream = await _inner.CreateAsync(streamId, targetTypeName, cancellationToken) + .ConfigureAwait(false); + return new InstrumentedEventStream(stream); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + throw; + } + } + + /// + public async Task CreateAsync(Guid streamId, string targetTypeName, string? tenantId, + string? userId, string? username, string? comment, + Dictionary? additionalMetaData, CancellationToken cancellationToken = default) + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStore.Create"); + activity?.SetTag("event_store.stream_id", streamId.ToString()); + activity?.SetTag("event_store.target_type", targetTypeName); + if (tenantId != null) + { + activity?.SetTag("event_store.tenant_id", tenantId); + } + + try + { + IEventStream stream = await _inner.CreateAsync(streamId, targetTypeName, tenantId, userId, username, + comment, additionalMetaData, cancellationToken).ConfigureAwait(false); + return new InstrumentedEventStream(stream); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + throw; + } + } +} diff --git a/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStoreTransactionAppender.cs b/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStoreTransactionAppender.cs new file mode 100644 index 0000000..4068449 --- /dev/null +++ b/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStoreTransactionAppender.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Papst.EventStore.Documents; + +namespace Papst.EventStore.OpenTelemetry; + +/// +/// A decorator for that instruments all operations with OpenTelemetry activities. +/// +internal sealed class InstrumentedEventStoreTransactionAppender : IEventStoreTransactionAppender +{ + private readonly IEventStoreTransactionAppender _inner; + private readonly Guid _streamId; + + public InstrumentedEventStoreTransactionAppender(IEventStoreTransactionAppender inner, Guid streamId) + { + _inner = inner; + _streamId = streamId; + } + + /// + public IEventStoreTransactionAppender Add(Guid id, TEvent evt, EventStreamMetaData? metaData = null, + CancellationToken cancellationToken = default) where TEvent : notnull + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStore.TransactionAppender.Add"); + activity?.SetTag("event_store.stream_id", _streamId.ToString()); + activity?.SetTag("event_store.event_id", id.ToString()); + activity?.SetTag("event_store.event_type", typeof(TEvent).FullName ?? typeof(TEvent).Name); + + _inner.Add(id, evt, metaData, cancellationToken); + return this; + } + + /// + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStore.TransactionAppender.Commit"); + activity?.SetTag("event_store.stream_id", _streamId.ToString()); + + try + { + await _inner.CommitAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + throw; + } + } +} diff --git a/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStream.cs b/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStream.cs new file mode 100644 index 0000000..88642e8 --- /dev/null +++ b/src/Papst.EventStore.OpenTelemetry/InstrumentedEventStream.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Papst.EventStore.Documents; + +namespace Papst.EventStore.OpenTelemetry; + +/// +/// A decorator for that instruments all operations with OpenTelemetry activities. +/// +internal sealed class InstrumentedEventStream : IEventStream +{ + private readonly IEventStream _inner; + + public InstrumentedEventStream(IEventStream inner) + { + _inner = inner; + } + + /// + public Guid StreamId => _inner.StreamId; + + /// + public ulong Version => _inner.Version; + + /// + public DateTimeOffset Created => _inner.Created; + + /// + public ulong? LatestSnapshotVersion => _inner.LatestSnapshotVersion; + + /// + public EventStreamMetaData MetaData => _inner.MetaData; + + /// + public Task GetLatestSnapshot(CancellationToken cancellationToken = default) + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStream.GetLatestSnapshot"); + activity?.SetTag("event_store.stream_id", StreamId.ToString()); + return _inner.GetLatestSnapshot(cancellationToken); + } + + /// + public async Task AppendAsync(Guid id, TEvent evt, EventStreamMetaData? metaData = null, + CancellationToken cancellationToken = default) where TEvent : notnull + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStream.Append"); + activity?.SetTag("event_store.stream_id", StreamId.ToString()); + activity?.SetTag("event_store.event_id", id.ToString()); + activity?.SetTag("event_store.event_type", typeof(TEvent).FullName ?? typeof(TEvent).Name); + + try + { + await _inner.AppendAsync(id, evt, metaData, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + throw; + } + } + + /// + public async Task AppendSnapshotAsync(Guid id, TEntity entity, EventStreamMetaData? metaData = null, + CancellationToken cancellationToken = default) where TEntity : notnull + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStream.AppendSnapshot"); + activity?.SetTag("event_store.stream_id", StreamId.ToString()); + activity?.SetTag("event_store.event_id", id.ToString()); + activity?.SetTag("event_store.entity_type", typeof(TEntity).FullName ?? typeof(TEntity).Name); + + try + { + await _inner.AppendSnapshotAsync(id, entity, metaData, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + throw; + } + } + + /// + public async Task CreateTransactionalBatchAsync() + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStream.CreateTransactionalBatch"); + activity?.SetTag("event_store.stream_id", StreamId.ToString()); + + try + { + IEventStoreTransactionAppender appender = await _inner.CreateTransactionalBatchAsync().ConfigureAwait(false); + return new InstrumentedEventStoreTransactionAppender(appender, StreamId); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + throw; + } + } + + /// + public IAsyncEnumerable ListAsync(ulong startVersion = 0, + CancellationToken cancellationToken = default) + => ListAsyncCore(_inner.ListAsync(startVersion, cancellationToken), "EventStream.List", cancellationToken); + + /// + public IAsyncEnumerable ListAsync(ulong startVersion, ulong endVersion, + CancellationToken cancellationToken = default) + => ListAsyncCore(_inner.ListAsync(startVersion, endVersion, cancellationToken), "EventStream.List", cancellationToken); + + /// + public IAsyncEnumerable ListDescendingAsync(ulong endVersion, ulong startVersion, + CancellationToken cancellationToken = default) + => ListAsyncCore(_inner.ListDescendingAsync(endVersion, startVersion, cancellationToken), "EventStream.ListDescending", cancellationToken); + + /// + public IAsyncEnumerable ListDescendingAsync(ulong endVersion, + CancellationToken cancellationToken = default) + => ListAsyncCore(_inner.ListDescendingAsync(endVersion, cancellationToken), "EventStream.ListDescending", cancellationToken); + + /// + public Task UpdateStreamMetaData(EventStreamMetaData metaData, CancellationToken cancellationToken = default) + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity("EventStream.UpdateMetaData"); + activity?.SetTag("event_store.stream_id", StreamId.ToString()); + return _inner.UpdateStreamMetaData(metaData, cancellationToken); + } + + private async IAsyncEnumerable ListAsyncCore( + IAsyncEnumerable source, + string activityName, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + using Activity? activity = EventStoreActivitySource.Source.StartActivity(activityName); + activity?.SetTag("event_store.stream_id", StreamId.ToString()); + + ulong count = 0; + await foreach (EventStreamDocument doc in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + count++; + yield return doc; + } + + activity?.SetTag("event_store.document_count", count.ToString()); + } +} diff --git a/src/Papst.EventStore.OpenTelemetry/Papst.EventStore.OpenTelemetry.csproj b/src/Papst.EventStore.OpenTelemetry/Papst.EventStore.OpenTelemetry.csproj new file mode 100644 index 0000000..e195430 --- /dev/null +++ b/src/Papst.EventStore.OpenTelemetry/Papst.EventStore.OpenTelemetry.csproj @@ -0,0 +1,28 @@ + + + net10.0 + disable + enable + MIT + OpenTelemetry instrumentation for Papst.EventStore - adds Activity-based tracing to all EventStore operations + Marco Papst + Papst.EventStore.OpenTelemetry + Papst.EventStore.OpenTelemetry + eventsourcing;eventstore;opentelemetry;tracing;observability + https://github.com/PapstIO/Papst.EventStore + true + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/Papst.EventStore.OpenTelemetry.Tests/EventStoreOpenTelemetryTests.cs b/tests/Papst.EventStore.OpenTelemetry.Tests/EventStoreOpenTelemetryTests.cs new file mode 100644 index 0000000..b7669da --- /dev/null +++ b/tests/Papst.EventStore.OpenTelemetry.Tests/EventStoreOpenTelemetryTests.cs @@ -0,0 +1,196 @@ +using System.Diagnostics; +using AutoFixture.Xunit2; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Papst.EventStore; +using Papst.EventStore.Aggregation.EventRegistration; +using Papst.EventStore.EventRegistration; +using Papst.EventStore.InMemory; +using Papst.EventStore.OpenTelemetry; +using Xunit; + +namespace Papst.EventStore.OpenTelemetry.Tests; + +[EventName("OtelTestEvent")] +public record OtelTestEvent +{ + public string Value { get; init; } = Guid.NewGuid().ToString(); +} + +public class EventStoreOpenTelemetryTests +{ + private static IServiceProvider BuildServiceProvider() + { + EventDescriptionEventRegistration registration = new(); + registration.AddEvent(new EventAttributeDescriptor(nameof(OtelTestEvent), true)); + + return new ServiceCollection() + .AddInMemoryEventStore() + .AddEventStoreInstrumentation() + .AddEventRegistrationTypeProvider() + .AddSingleton(registration) + .AddLogging() + .BuildServiceProvider(); + } + + [Fact] + public void AddEventStoreInstrumentation_ShouldWrapEventStore() + { + // arrange & act + var sp = BuildServiceProvider(); + + // assert - the resolved IEventStore should NOT be the raw InMemory implementation + var store = sp.GetRequiredService(); + store.Should().NotBeOfType(); + store.GetType().Name.Should().Be("InstrumentedEventStore"); + } + + [Fact] + public void AddEventStoreInstrumentation_WithNoEventStore_ShouldThrow() + { + // arrange + var services = new ServiceCollection(); + + // act & assert + var act = () => services.AddEventStoreInstrumentation(); + act.Should().Throw(); + } + + [Theory, AutoData] + public async Task GetAsync_ShouldProduceActivity(Guid streamId) + { + // arrange + var sp = BuildServiceProvider(); + var store = sp.GetRequiredService(); + await store.CreateAsync(streamId, "TestTarget", CancellationToken.None); + + var activities = new List(); + using var listener = new ActivityListener + { + ShouldListenTo = src => src.Name == EventStoreActivitySource.SourceName, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + // act + await store.GetAsync(streamId, CancellationToken.None); + + // assert + activities.Should().ContainSingle(a => a.OperationName == "EventStore.Get"); + var activity = activities.Single(a => a.OperationName == "EventStore.Get"); + activity.GetTagItem("event_store.stream_id").Should().Be(streamId.ToString()); + } + + [Theory, AutoData] + public async Task CreateAsync_ShouldProduceActivity(Guid streamId) + { + // arrange + var sp = BuildServiceProvider(); + var store = sp.GetRequiredService(); + + var activities = new List(); + using var listener = new ActivityListener + { + ShouldListenTo = src => src.Name == EventStoreActivitySource.SourceName, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + // act + await store.CreateAsync(streamId, "TestTarget", CancellationToken.None); + + // assert + activities.Should().ContainSingle(a => a.OperationName == "EventStore.Create"); + var activity = activities.Single(a => a.OperationName == "EventStore.Create"); + activity.GetTagItem("event_store.stream_id").Should().Be(streamId.ToString()); + activity.GetTagItem("event_store.target_type").Should().Be("TestTarget"); + } + + [Theory, AutoData] + public async Task AppendAsync_ShouldProduceActivity(Guid streamId, Guid eventId) + { + // arrange + var sp = BuildServiceProvider(); + var store = sp.GetRequiredService(); + var stream = await store.CreateAsync(streamId, "TestTarget", CancellationToken.None); + + var activities = new List(); + using var listener = new ActivityListener + { + ShouldListenTo = src => src.Name == EventStoreActivitySource.SourceName, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + // act + await stream.AppendAsync(eventId, new OtelTestEvent(), cancellationToken: CancellationToken.None); + + // assert + activities.Should().ContainSingle(a => a.OperationName == "EventStream.Append"); + var activity = activities.Single(a => a.OperationName == "EventStream.Append"); + activity.GetTagItem("event_store.stream_id").Should().Be(streamId.ToString()); + activity.GetTagItem("event_store.event_id").Should().Be(eventId.ToString()); + } + + [Theory, AutoData] + public async Task ListAsync_ShouldProduceActivity(Guid streamId) + { + // arrange + var sp = BuildServiceProvider(); + var store = sp.GetRequiredService(); + var stream = await store.CreateAsync(streamId, "TestTarget", CancellationToken.None); + await stream.AppendAsync(Guid.NewGuid(), new OtelTestEvent(), cancellationToken: CancellationToken.None); + + var activities = new List(); + using var listener = new ActivityListener + { + ShouldListenTo = src => src.Name == EventStoreActivitySource.SourceName, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + // act + var docs = await stream.ListAsync(0, CancellationToken.None).ToListAsync(CancellationToken.None); + + // assert + docs.Should().HaveCount(1); + activities.Should().ContainSingle(a => a.OperationName == "EventStream.List"); + var activity = activities.Single(a => a.OperationName == "EventStream.List"); + activity.GetTagItem("event_store.stream_id").Should().Be(streamId.ToString()); + activity.GetTagItem("event_store.document_count").Should().Be("1"); + } + + [Theory, AutoData] + public async Task CommitAsync_ShouldProduceActivity(Guid streamId) + { + // arrange + var sp = BuildServiceProvider(); + var store = sp.GetRequiredService(); + var stream = await store.CreateAsync(streamId, "TestTarget", CancellationToken.None); + // Append an event first so the transactional batch can calculate version from a non-empty list + await stream.AppendAsync(Guid.NewGuid(), new OtelTestEvent(), cancellationToken: CancellationToken.None); + var batch = await stream.CreateTransactionalBatchAsync(); + batch.Add(Guid.NewGuid(), new OtelTestEvent()); + + var activities = new List(); + using var listener = new ActivityListener + { + ShouldListenTo = src => src.Name == EventStoreActivitySource.SourceName, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activities.Add + }; + ActivitySource.AddActivityListener(listener); + + // act + await batch.CommitAsync(CancellationToken.None); + + // assert + activities.Should().ContainSingle(a => a.OperationName == "EventStore.TransactionAppender.Commit"); + var activity = activities.Single(a => a.OperationName == "EventStore.TransactionAppender.Commit"); + activity.GetTagItem("event_store.stream_id").Should().Be(streamId.ToString()); + } +} diff --git a/tests/Papst.EventStore.OpenTelemetry.Tests/Papst.EventStore.OpenTelemetry.Tests.csproj b/tests/Papst.EventStore.OpenTelemetry.Tests/Papst.EventStore.OpenTelemetry.Tests.csproj new file mode 100644 index 0000000..403a448 --- /dev/null +++ b/tests/Papst.EventStore.OpenTelemetry.Tests/Papst.EventStore.OpenTelemetry.Tests.csproj @@ -0,0 +1,38 @@ + + + net10.0 + enable + enable + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +