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
+
+
+