mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-04-26 02:08:29 +02:00
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:
@@ -0,0 +1,201 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
#if ACTIVITY_TRACING_SUPPORTED
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using WireMock.Logging;
|
||||
|
||||
namespace WireMock.Owin.ActivityTracing;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an ActivitySource for WireMock.Net distributed tracing.
|
||||
/// </summary>
|
||||
public static class WireMockActivitySource
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the ActivitySource used by WireMock.Net.
|
||||
/// </summary>
|
||||
internal const string SourceName = "WireMock.Net";
|
||||
|
||||
/// <summary>
|
||||
/// The ActivitySource instance used for creating tracing activities.
|
||||
/// </summary>
|
||||
public static readonly ActivitySource Source = new(SourceName, GetVersion());
|
||||
|
||||
private static string GetVersion()
|
||||
{
|
||||
return typeof(WireMockActivitySource).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new activity for a WireMock request.
|
||||
/// </summary>
|
||||
/// <param name="requestMethod">The HTTP method of the request.</param>
|
||||
/// <param name="requestPath">The path of the request.</param>
|
||||
/// <returns>The started activity, or null if tracing is not enabled.</returns>
|
||||
internal static Activity? StartRequestActivity(string requestMethod, string requestPath)
|
||||
{
|
||||
if (!Source.HasListeners())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var activity = Source.StartActivity(
|
||||
$"WireMock {requestMethod} {requestPath}",
|
||||
ActivityKind.Server
|
||||
);
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an activity with request information.
|
||||
/// </summary>
|
||||
internal static void EnrichWithRequest(Activity? activity, IRequestMessage request, ActivityTracingOptions? options = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(WireMockSemanticConventions.HttpMethod, request.Method);
|
||||
activity.SetTag(WireMockSemanticConventions.HttpUrl, request.Url);
|
||||
activity.SetTag(WireMockSemanticConventions.HttpPath, request.Path);
|
||||
activity.SetTag(WireMockSemanticConventions.HttpHost, request.Host);
|
||||
|
||||
if (request.ClientIP != null)
|
||||
{
|
||||
activity.SetTag(WireMockSemanticConventions.ClientAddress, request.ClientIP);
|
||||
}
|
||||
|
||||
// Record request body if enabled
|
||||
if (options?.RecordRequestBody == true && request.Body != null)
|
||||
{
|
||||
activity.SetTag(WireMockSemanticConventions.RequestBody, request.Body);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an activity with response information.
|
||||
/// </summary>
|
||||
internal static void EnrichWithResponse(Activity? activity, IResponseMessage? response, ActivityTracingOptions? options = null)
|
||||
{
|
||||
if (activity == null || response == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// StatusCode can be int, HttpStatusCode, or string
|
||||
var statusCode = response.StatusCode;
|
||||
int? statusCodeInt = statusCode switch
|
||||
{
|
||||
int i => i,
|
||||
System.Net.HttpStatusCode hsc => (int)hsc,
|
||||
string s when int.TryParse(s, out var parsed) => parsed,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (statusCodeInt.HasValue)
|
||||
{
|
||||
activity.SetTag(WireMockSemanticConventions.HttpStatusCode, statusCodeInt.Value);
|
||||
activity.SetTag("otel.status_description", $"HTTP {statusCodeInt.Value}");
|
||||
|
||||
// Set status based on HTTP status code (using standard otel.status_code tag)
|
||||
if (statusCodeInt.Value >= 400)
|
||||
{
|
||||
activity.SetTag("otel.status_code", "ERROR");
|
||||
}
|
||||
else
|
||||
{
|
||||
activity.SetTag("otel.status_code", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
// Record response body if enabled
|
||||
if (options?.RecordResponseBody == true && response.BodyData?.BodyAsString != null)
|
||||
{
|
||||
activity.SetTag(WireMockSemanticConventions.ResponseBody, response.BodyData.BodyAsString);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an activity with mapping match information.
|
||||
/// </summary>
|
||||
internal static void EnrichWithMappingMatch(
|
||||
Activity? activity,
|
||||
Guid? mappingGuid,
|
||||
string? mappingTitle,
|
||||
bool isPerfectMatch,
|
||||
double? matchScore)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activity.SetTag(WireMockSemanticConventions.MappingMatched, isPerfectMatch);
|
||||
|
||||
if (mappingGuid.HasValue)
|
||||
{
|
||||
activity.SetTag(WireMockSemanticConventions.MappingGuid, mappingGuid.Value.ToString());
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(mappingTitle))
|
||||
{
|
||||
activity.SetTag(WireMockSemanticConventions.MappingTitle, mappingTitle);
|
||||
}
|
||||
|
||||
if (matchScore.HasValue)
|
||||
{
|
||||
activity.SetTag(WireMockSemanticConventions.MatchScore, matchScore.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an activity with log entry information (includes response and mapping match info).
|
||||
/// </summary>
|
||||
internal static void EnrichWithLogEntry(Activity? activity, ILogEntry logEntry, ActivityTracingOptions? options = null)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Enrich with response
|
||||
EnrichWithResponse(activity, logEntry.ResponseMessage, options);
|
||||
|
||||
// Enrich with mapping match (if enabled)
|
||||
if (options?.RecordMatchDetails != false)
|
||||
{
|
||||
EnrichWithMappingMatch(
|
||||
activity,
|
||||
logEntry.MappingGuid,
|
||||
logEntry.MappingTitle,
|
||||
logEntry.RequestMatchResult?.IsPerfectMatch ?? false,
|
||||
logEntry.RequestMatchResult?.TotalScore);
|
||||
}
|
||||
|
||||
// Set request GUID
|
||||
activity.SetTag(WireMockSemanticConventions.RequestGuid, logEntry.Guid.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an exception on the activity.
|
||||
/// </summary>
|
||||
internal static void RecordException(Activity? activity, Exception exception)
|
||||
{
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use standard OpenTelemetry exception semantic conventions
|
||||
activity.SetTag("otel.status_code", "ERROR");
|
||||
activity.SetTag("otel.status_description", exception.Message);
|
||||
activity.SetTag("exception.type", exception.GetType().FullName);
|
||||
activity.SetTag("exception.message", exception.Message);
|
||||
activity.SetTag("exception.stacktrace", exception.ToString());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user