// Copyright © WireMock.Net #if ACTIVITY_TRACING_SUPPORTED using System; using System.Diagnostics; using WireMock.Logging; namespace WireMock.Owin.ActivityTracing; /// /// Provides an ActivitySource for WireMock.Net distributed tracing. /// public static class WireMockActivitySource { /// /// The name of the ActivitySource used by WireMock.Net. /// internal const string SourceName = "WireMock.Net"; /// /// The ActivitySource instance used for creating tracing activities. /// public static readonly ActivitySource Source = new(SourceName, GetVersion()); private static string GetVersion() { return typeof(WireMockActivitySource).Assembly.GetName().Version?.ToString() ?? "1.0.0"; } /// /// Starts a new activity for a WireMock request. /// /// The HTTP method of the request. /// The path of the request. /// The started activity, or null if tracing is not enabled. internal static Activity? StartRequestActivity(string requestMethod, string requestPath) { if (!Source.HasListeners()) { return null; } var activity = Source.StartActivity( $"WireMock {requestMethod} {requestPath}", ActivityKind.Server ); return activity; } /// /// Enriches an activity with request information. /// 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); } } /// /// Enriches an activity with response information. /// 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); } } /// /// Enriches an activity with mapping match information. /// 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); } } /// /// Enriches an activity with log entry information (includes response and mapping match info). /// 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()); } /// /// Records an exception on the activity. /// 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