Add OTEL tracing support for Wiremock + automatic OTEL for Aspire integration (#1418)

* Update aspire to 13.1 (examples + code)

Allows usage of aspire CLI which is very useful for dev in codespaces (for my next PR).

* Add OTEL support

* Initial PR feedback

* PR feedback

* PR feedback

* PR feedback

* Cleanup.

* Cleanup

* Fix

* Fix

* Rename stuff around to be more accurate

* PR feedback

* Update WireMock.Net.OpenTelemetry.csproj

Update <Authors>

* PR feedback parser

* PR feedback package versions

* Status code feedback.

* Update preprocessor directives to to Activity Tracing instead of OpenTelemetry. Is more descriptive.

* Add tests

* Improve tests

---------

Co-authored-by: Stef Heyenrath <Stef.Heyenrath@gmail.com>
This commit is contained in:
Petr Houška
2026-01-18 17:22:36 +01:00
committed by GitHub
parent abe996671e
commit 4525c61847
38 changed files with 2057 additions and 9 deletions

View File

@@ -0,0 +1,195 @@
// Copyright © WireMock.Net
#if NET6_0_OR_GREATER
using System;
using System.Diagnostics;
using System.Linq;
using FluentAssertions;
using Moq;
using WireMock.Logging;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.Owin.ActivityTracing;
using WireMock.Util;
using Xunit;
namespace WireMock.Net.Tests.Owin.ActivityTracing;
public class WireMockActivitySourceTests
{
[Fact]
public void EnrichWithRequest_ShouldSetRequestTagsAndBody_WhenEnabled()
{
// Arrange
using var activity = new Activity("test").Start();
var request = new RequestMessage(
new UrlDetails("http://localhost/api/orders"),
"POST",
"127.0.0.1",
new BodyData { BodyAsString = "payload" });
var options = new ActivityTracingOptions
{
RecordRequestBody = true
};
// Act
WireMockActivitySource.EnrichWithRequest(activity, request, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.HttpMethod).Should().Be("POST");
activity.GetTagItem(WireMockSemanticConventions.HttpUrl).Should().Be("http://localhost/api/orders");
activity.GetTagItem(WireMockSemanticConventions.HttpPath).Should().Be("/api/orders");
activity.GetTagItem(WireMockSemanticConventions.HttpHost).Should().Be("localhost");
activity.GetTagItem(WireMockSemanticConventions.ClientAddress).Should().Be("127.0.0.1");
activity.GetTagItem(WireMockSemanticConventions.RequestBody).Should().Be("payload");
}
[Fact]
public void EnrichWithResponse_ShouldSetStatusAndBody_WhenEnabled()
{
// Arrange
using var activity = new Activity("test").Start();
var response = new ResponseMessage
{
StatusCode = 200,
BodyData = new BodyData { BodyAsString = "ok" }
};
var options = new ActivityTracingOptions
{
RecordResponseBody = true
};
// Act
WireMockActivitySource.EnrichWithResponse(activity, response, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.HttpStatusCode).Should().Be(200);
activity.GetTagItem("otel.status_code").Should().Be("OK");
activity.GetTagItem(WireMockSemanticConventions.ResponseBody).Should().Be("ok");
}
[Fact]
public void EnrichWithResponse_ShouldSetErrorStatus_ForNonSuccess()
{
// Arrange
using var activity = new Activity("test").Start();
var response = new ResponseMessage
{
StatusCode = 500
};
// Act
WireMockActivitySource.EnrichWithResponse(activity, response, new ActivityTracingOptions());
// Assert
activity.GetTagItem(WireMockSemanticConventions.HttpStatusCode).Should().Be(500);
activity.GetTagItem("otel.status_code").Should().Be("ERROR");
}
[Fact]
public void EnrichWithRequest_ShouldNotRecordBody_WhenDisabled()
{
// Arrange
using var activity = new Activity("test").Start();
var request = new RequestMessage(
new UrlDetails("http://localhost/api/orders"),
"POST",
"127.0.0.1",
new BodyData { BodyAsString = "payload" });
var options = new ActivityTracingOptions
{
RecordRequestBody = false
};
// Act
WireMockActivitySource.EnrichWithRequest(activity, request, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.RequestBody).Should().BeNull();
}
[Fact]
public void EnrichWithResponse_ShouldNotRecordBody_WhenDisabled()
{
// Arrange
using var activity = new Activity("test").Start();
var response = new ResponseMessage
{
StatusCode = 200,
BodyData = new BodyData { BodyAsString = "ok" }
};
var options = new ActivityTracingOptions
{
RecordResponseBody = false
};
// Act
WireMockActivitySource.EnrichWithResponse(activity, response, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.ResponseBody).Should().BeNull();
}
[Fact]
public void EnrichWithLogEntry_ShouldSkipMatchDetails_WhenDisabled()
{
// Arrange
using var activity = new Activity("test").Start();
var request = new RequestMessage(
new UrlDetails("http://localhost/api/orders"),
"GET",
"127.0.0.1");
var response = new ResponseMessage { StatusCode = 200 };
var matchResult = new Mock<IRequestMatchResult>();
matchResult.SetupGet(r => r.IsPerfectMatch).Returns(true);
matchResult.SetupGet(r => r.TotalScore).Returns(1.0);
var logEntry = new LogEntry
{
Guid = Guid.NewGuid(),
RequestMessage = request,
ResponseMessage = response,
RequestMatchResult = matchResult.Object,
MappingGuid = Guid.NewGuid(),
MappingTitle = "test-mapping"
};
var options = new ActivityTracingOptions
{
RecordMatchDetails = false
};
// Act
WireMockActivitySource.EnrichWithLogEntry(activity, logEntry, options);
// Assert
activity.GetTagItem(WireMockSemanticConventions.RequestGuid).Should().Be(logEntry.Guid.ToString());
activity.Tags.Should().NotContain(tag => tag.Key == WireMockSemanticConventions.MappingGuid);
activity.Tags.Should().NotContain(tag => tag.Key == WireMockSemanticConventions.MappingTitle);
activity.Tags.Should().NotContain(tag => tag.Key == WireMockSemanticConventions.MatchScore);
}
[Fact]
public void RecordException_ShouldSetExceptionTags()
{
// Arrange
using var activity = new Activity("test").Start();
var exception = new InvalidOperationException("boom");
// Act
WireMockActivitySource.RecordException(activity, exception);
// Assert
activity.GetTagItem("otel.status_code").Should().Be("ERROR");
activity.GetTagItem("otel.status_description").Should().Be("boom");
activity.GetTagItem("exception.type").Should().Be(typeof(InvalidOperationException).FullName);
activity.GetTagItem("exception.message").Should().Be("boom");
activity.GetTagItem("exception.stacktrace").Should().NotBeNull();
}
}
#endif

View File

@@ -13,6 +13,9 @@ using WireMock.Util;
using WireMock.Logging;
using WireMock.Matchers;
using System.Collections.Generic;
#if NET6_0_OR_GREATER
using System.Diagnostics;
#endif
using WireMock.Admin.Mappings;
using WireMock.Admin.Requests;
using WireMock.Settings;
@@ -21,6 +24,9 @@ using WireMock.Handlers;
using WireMock.Matchers.Request;
using WireMock.ResponseBuilders;
using WireMock.RequestBuilders;
#if NET6_0_OR_GREATER
using WireMock.Owin.ActivityTracing;
#endif
#if NET452
using Microsoft.Owin;
using IContext = Microsoft.Owin.IOwinContext;
@@ -289,4 +295,90 @@ public class WireMockMiddlewareTests
_mappings.Should().HaveCount(1);
}
#if NET6_0_OR_GREATER
[Fact]
public async Task WireMockMiddleware_Invoke_AdminPath_WithExcludeAdminRequests_ShouldNotStartActivity()
{
// Arrange
var request = new RequestMessage(new UrlDetails("http://localhost/__admin/health"), "GET", "::1");
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);
_optionsMock.SetupGet(o => o.ActivityTracingOptions).Returns(new WireMock.Owin.ActivityTracing.ActivityTracingOptions
{
ExcludeAdminRequests = true
});
var activityStarted = false;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == WireMockActivitySource.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = _ => activityStarted = true
};
ActivitySource.AddActivityListener(listener);
// Act
await _sut.Invoke(_contextMock.Object).ConfigureAwait(false);
// Assert
activityStarted.Should().BeFalse();
}
[Fact]
public async Task WireMockMiddleware_Invoke_NonAdminPath_WithTracingEnabled_ShouldStartActivity()
{
// Arrange
var request = new RequestMessage(new UrlDetails("http://localhost/api/orders"), "GET", "::1");
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);
_optionsMock.SetupGet(o => o.ActivityTracingOptions).Returns(new WireMock.Owin.ActivityTracing.ActivityTracingOptions
{
ExcludeAdminRequests = true
});
var activityStarted = false;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == WireMockActivitySource.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = _ => activityStarted = true
};
ActivitySource.AddActivityListener(listener);
// Act
await _sut.Invoke(_contextMock.Object).ConfigureAwait(false);
// Assert
activityStarted.Should().BeTrue();
}
[Fact]
public async Task WireMockMiddleware_Invoke_NonAdminPath_WithoutTracingOptions_ShouldNotStartActivity()
{
// Arrange
var request = new RequestMessage(new UrlDetails("http://localhost/api/orders"), "GET", "::1");
_requestMapperMock.Setup(m => m.MapAsync(It.IsAny<IRequest>(), It.IsAny<IWireMockMiddlewareOptions>())).ReturnsAsync(request);
_optionsMock.SetupGet(o => o.ActivityTracingOptions).Returns((WireMock.Owin.ActivityTracing.ActivityTracingOptions?)null);
var activityStarted = false;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == WireMockActivitySource.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = _ => activityStarted = true
};
ActivitySource.AddActivityListener(listener);
// Act
await _sut.Invoke(_contextMock.Object).ConfigureAwait(false);
// Assert
activityStarted.Should().BeFalse();
}
#endif
}