Skip to content
Merged
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
450 changes: 450 additions & 0 deletions WireMock.Net Solution.sln

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions examples-Aspire/AspireApp1.AppHost/AspireApp1.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
<Sdk Name="Aspire.AppHost.Sdk" Version="13.1.0" />

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -18,7 +18,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion examples-Aspire/AspireApp1.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
.AsHttp2Service()
.WithMappingsPath(mappingsPath)
.WithWatchStaticMappings()
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync)
.WithOpenTelemetry(); // Enable OpenTelemetry tracing for Aspire dashboard

//var apiServiceUsedForDocs = builder
// .AddWireMock("apiservice1", WireMockServerArguments.DefaultPort)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
<Sdk Name="Aspire.AppHost.Sdk" Version="13.1.0" />

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -15,7 +15,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.1.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion examples-Aspire/AspireApp1.Tests/AspireApp1.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="8.0.0" />
<PackageReference Include="Aspire.Hosting.Testing" Version="13.1.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.5.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
141 changes: 141 additions & 0 deletions examples/WireMock.Net.OpenTelemetryDemo/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright © WireMock.Net
// OpenTelemetry Tracing Demo for WireMock.Net
// This demo uses the Console Exporter to visualize traces in the terminal.

using OpenTelemetry;
using OpenTelemetry.Trace;
using WireMock.Server;
using WireMock.Settings;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.OpenTelemetry;

Console.WriteLine("=== WireMock.Net OpenTelemetry Tracing Demo ===\n");

// WireMock.Net creates Activity objects using System.Diagnostics.Activity (built into .NET).
// These activities are automatically created when ActivityTracingEnabled is set to true.
//
// To export these traces, you have two options:
//
// Option 1: Configure your own TracerProvider (shown below)
// - Full control over exporters (Console, OTLP, Jaeger, etc.)
// - Add additional instrumentation (HttpClient, database, etc.)
// - Recommended for most applications
//
// Option 2: Use WireMock.Net.OpenTelemetry package
// - Reference the WireMock.Net.OpenTelemetry NuGet package
// - Use services.AddWireMockOpenTelemetry(openTelemetryOptions)
// - Adds WireMock + ASP.NET Core instrumentation and OTLP exporter
// - Good for quick setup with all-in-one configuration

// Option 1: Custom TracerProvider with Console exporter for this demo
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddWireMockInstrumentation(new OpenTelemetryOptions() { ExcludeAdminRequests = true })
.AddHttpClientInstrumentation() // HTTP client traces (for our test requests)
.AddConsoleExporter() // Export traces to console for demo purposes
.Build();

Console.WriteLine("Console Exporter configured to visualize:");
Console.WriteLine(" - WireMock.Net traces (wiremock.* tags)");
Console.WriteLine(" - ASP.NET Core server traces");
Console.WriteLine(" - HTTP client traces\n");

// Start WireMock server with OpenTelemetry enabled (ActivityTracingOptions != null enables tracing)
var server = WireMockServer.Start(new WireMockServerSettings
{
StartAdminInterface = true,
ActivityTracingOptions = new ActivityTracingOptions
{
ExcludeAdminRequests = true
}
});

Console.WriteLine($"WireMock server started at: {string.Join(", ", server.Urls)}\n");

// Configure some mock mappings
server
.Given(Request.Create()
.WithPath("/api/hello")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithBody("Hello from WireMock!"));

server
.Given(Request.Create()
.WithPath("/api/user/*")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBody(@"{""name"": ""John Doe"", ""email"": ""john@example.com""}"));

server
.Given(Request.Create()
.WithPath("/api/error")
.UsingGet())
.RespondWith(Response.Create()
.WithStatusCode(500)
.WithBody("Internal Server Error"));

Console.WriteLine("Mock mappings configured:");
Console.WriteLine(" GET /api/hello -> 200 OK");
Console.WriteLine(" GET /api/user/* -> 200 OK (JSON)");
Console.WriteLine(" GET /api/error -> 500 Error");
Console.WriteLine();

// Make some test requests to generate traces
using var httpClient = server.CreateClient();

Console.WriteLine("Making test requests to generate traces...\n");
Console.WriteLine("─────────────────────────────────────────────────────────────────");

// Request 1: Successful request
Console.WriteLine("\n>>> Request 1: GET /api/hello");
var response1 = await httpClient.GetAsync("/api/hello");
Console.WriteLine($"<<< Response: {(int)response1.StatusCode} {response1.StatusCode}");
Console.WriteLine($" Body: {await response1.Content.ReadAsStringAsync()}");

await Task.Delay(500); // Small delay to let trace export complete

// Request 2: Another successful request with path parameter
Console.WriteLine("\n>>> Request 2: GET /api/user/123");
var response2 = await httpClient.GetAsync("/api/user/123");
Console.WriteLine($"<<< Response: {(int)response2.StatusCode} {response2.StatusCode}");
Console.WriteLine($" Body: {await response2.Content.ReadAsStringAsync()}");

await Task.Delay(500);

// Request 3: Error response
Console.WriteLine("\n>>> Request 3: GET /api/error");
var response3 = await httpClient.GetAsync("/api/error");
Console.WriteLine($"<<< Response: {(int)response3.StatusCode} {response3.StatusCode}");
Console.WriteLine($" Body: {await response3.Content.ReadAsStringAsync()}");

await Task.Delay(500);

// Request 4: No matching mapping (404)
Console.WriteLine("\n>>> Request 4: GET /api/notfound");
var response4 = await httpClient.GetAsync("/api/notfound");
Console.WriteLine($"<<< Response: {(int)response4.StatusCode} {response4.StatusCode}");

await Task.Delay(500);

// Request 5: Admin API request (should be excluded from tracing)
Console.WriteLine("\n>>> Request 5: GET /__admin/health");
var response5 = await httpClient.GetAsync("/__admin/health");
Console.WriteLine($"<<< Admin Health Status: {response5.StatusCode}");

Console.WriteLine("\n─────────────────────────────────────────────────────────────────");
Console.WriteLine("\nTraces above show OpenTelemetry activities from WireMock.Net!");
Console.WriteLine("Look for 'Activity.TraceId', 'Activity.SpanId', and custom tags like:");
Console.WriteLine(" - http.request.method");
Console.WriteLine(" - url.path");
Console.WriteLine(" - http.response.status_code");
Console.WriteLine(" - wiremock.mapping.matched");
Console.WriteLine(" - wiremock.mapping.guid");
Console.WriteLine();

// Cleanup
server.Stop();
Console.WriteLine("WireMock server stopped. Demo complete!");
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.OpenTelemetry\WireMock.Net.OpenTelemetry.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/WireMock.Net.Aspire/WireMock.Net.Aspire.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="9.2.0" />
<PackageReference Include="Aspire.Hosting" Version="13.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
30 changes: 30 additions & 0 deletions src/WireMock.Net.Aspire/WireMockServerArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@
/// </summary>
public Dictionary<string, string[]> ProtoDefinitions { get; set; } = [];

/// <summary>
/// Gets or sets a value indicating whether OpenTelemetry tracing is enabled.
/// When enabled, WireMock.Net will emit distributed traces for request processing.
/// Default value is <c>false</c>.
/// </summary>
public bool OpenTelemetryEnabled { get; set; }

/// <summary>
/// Gets or sets the OTLP exporter endpoint URL.
/// When set, traces will be exported to this endpoint using the OTLP protocol.
/// Example: "http://localhost:4317" for gRPC or "http://localhost:4318" for HTTP.
/// If not set, the OTLP exporter will use the <c>OTEL_EXPORTER_OTLP_ENDPOINT</c> environment variable,
/// or fall back to the default endpoint (<c>http://localhost:4317</c> for gRPC).
/// </summary>
public string? OpenTelemetryOtlpExporterEndpoint { get; set; }

/// <summary>
/// Add an additional Urls on which WireMock should listen.
/// </summary>
Expand Down Expand Up @@ -138,6 +154,20 @@
Add(args, "--WatchStaticMappingsInSubdirectories", "true");
}

if (OpenTelemetryEnabled)
{
// Enable activity tracing (creates System.Diagnostics.Activity objects)
Add(args, "--ActivityTracingEnabled", "true");

// Enable OpenTelemetry exporter
Add(args, "--OpenTelemetryEnabled", "true");

if (!string.IsNullOrEmpty(OpenTelemetryOtlpExporterEndpoint))
{
Add(args, "--OpenTelemetryOtlpExporterEndpoint", OpenTelemetryOtlpExporterEndpoint);
}
}

if (AdditionalUrls.Count > 0)
{
Add(args, "--Urls", $"http://*:{HttpContainerPort} {string.Join(' ', AdditionalUrls)}");
Expand All @@ -153,7 +183,7 @@
args[argument] = value;
}

private static void Add(IDictionary<string, string> args, string argument, Func<string> action)

Check warning on line 186 in src/WireMock.Net.Aspire/WireMockServerArguments.cs

View workflow job for this annotation

GitHub Actions / Run Tests on Linux

Remove the unused private method 'Add'. (https://rules.sonarsource.com/csharp/RSPEC-1144)
{
args[argument] = action();
}
Expand Down
34 changes: 34 additions & 0 deletions src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
});

// Always add the lifecycle hook to support dynamic mappings and proto definitions
resourceBuilder.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();

Check warning on line 138 in src/WireMock.Net.Aspire/WireMockServerBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / Run Tests on Linux

'LifecycleHookServiceCollectionExtensions.TryAddLifecycleHook<T>(IServiceCollection)' is obsolete: 'Use EventingSubscriberServiceCollectionExtensions.TryAddEventingSubscriber instead.'

return resourceBuilder;
}
Expand Down Expand Up @@ -287,6 +287,40 @@
return wiremock;
}

/// <summary>
/// Configures OpenTelemetry distributed tracing for the WireMock.Net server.
/// This enables automatic trace export to the Aspire dashboard.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
/// <remarks>
/// When enabled, WireMock.Net will emit distributed traces for each request processed,
/// including information about:
/// <list type="bullet">
/// <item>HTTP method, URL, and status code</item>
/// <item>Mapping match results and scores</item>
/// <item>Request processing duration</item>
/// </list>
/// The traces will automatically appear in the Aspire dashboard.
/// </remarks>
public static IResourceBuilder<WireMockServerResource> WithOpenTelemetry(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock);

// Enable OpenTelemetry in WireMock server arguments
wiremock.Resource.Arguments.OpenTelemetryEnabled = true;

// Use Aspire's standard WithOtlpExporter to configure OTEL environment variables for the container
// This sets OTEL_EXPORTER_OTLP_ENDPOINT which the OTLP exporter reads automatically
var containerBuilder = wiremock as IResourceBuilder<ContainerResource>;
if (containerBuilder != null)
{
containerBuilder.WithOtlpExporter();
}

return wiremock;
}

private static Task<ExecuteCommandResult> OnRunOpenInspectorCommandAsync(IResourceBuilder<WireMockServerResource> builder)
{
WireMockInspector.Inspect(builder.Resource.GetEndpoint().Url);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright © WireMock.Net

#if ACTIVITY_TRACING_SUPPORTED

namespace WireMock.Owin.ActivityTracing;

/// <summary>
/// Options for controlling activity tracing in WireMock.Net middleware.
/// These options control the creation of System.Diagnostics.Activity objects
/// but do not require any OpenTelemetry exporter dependencies.
/// </summary>
public class ActivityTracingOptions
{
/// <summary>
/// Gets or sets a value indicating whether to exclude admin interface requests from tracing.
/// Default is <c>true</c>.
/// </summary>
public bool ExcludeAdminRequests { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether to record request body in trace attributes.
/// Default is <c>false</c> due to potential PII concerns.
/// </summary>
public bool RecordRequestBody { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to record response body in trace attributes.
/// Default is <c>false</c> due to potential PII concerns.
/// </summary>
public bool RecordResponseBody { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to record mapping match details in trace attributes.
/// Default is <c>true</c>.
/// </summary>
public bool RecordMatchDetails { get; set; } = true;
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright © WireMock.Net

#if !ACTIVITY_TRACING_SUPPORTED
using System;
#endif
using WireMock.Settings;

namespace WireMock.Owin.ActivityTracing;

/// <summary>
/// Validator for Activity Tracing configuration.
/// </summary>
internal static class ActivityTracingValidator
{
/// <summary>
/// Validates that Activity Tracing is supported on the current framework.
/// Throws an exception if ActivityTracingOptions is configured on an unsupported framework.
/// </summary>
/// <param name="settings">The WireMock server settings to validate.</param>
/// <exception cref="System.InvalidOperationException">
/// Thrown when ActivityTracingOptions is configured but the current framework does not support System.Diagnostics.Activity.
/// </exception>
public static void ValidateActivityApiPresence(WireMockServerSettings settings)
{
#if !ACTIVITY_TRACING_SUPPORTED
if (settings.ActivityTracingOptions is not null)
{
throw new InvalidOperationException(
"Activity Tracing is not supported on this target framework. " +
"It requires .NET 5.0 or higher which includes System.Diagnostics.Activity support.");
}
#endif
}
}
Loading
Loading