using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; namespace GB5Shared.Telemetry; /// /// Lightweight static helper for adding explicit, structured instrumentation to BLL, DAL, /// and any other application code — without constructor injection or interface dependencies. /// /// All methods are no-ops when there is no active span, so they are safe to call /// unconditionally without null checks or feature flags. /// /// /// /// /// // Add a named step event with data visible in Zipkin / Jaeger: /// GB5Trace.Step("validate-invoice", new { invoiceId = inv.Id, amount = inv.Total }); /// /// // Open a child span for a heavy processing section: /// using var section = GB5Trace.BeginSection("qualification-pipeline"); /// /// // Tag the active span with business context: /// GB5Trace.Tag("gb5.invoice.id", invoiceId); /// /// // Record a non-fatal, recoverable error without throwing: /// GB5Trace.RecordError(ex, context: "fallback-to-local-cache"); /// /// public static class GB5Trace { private static readonly JsonSerializerOptions _jsonOpts = new() { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; /// /// Adds a named event (annotation) to the active span with optional structured data. /// In Zipkin each step appears as a timeline annotation; in Jaeger as a structured log entry. /// /// /// Short, lowercase-hyphenated name for this step, e.g. "validate-payment", "enrich-address". /// /// /// Optional payload. Top-level properties of a plain object are flattened into individual /// span event tags for easy searching (e.g. step.invoiceId, step.amount). /// public static void Step(string stepName, object? data = null) { var activity = Activity.Current; if (activity is null) return; var tags = new ActivityTagsCollection(); if (data is not null) { try { var json = JsonSerializer.Serialize(data, _jsonOpts); using var doc = JsonDocument.Parse(json); if (doc.RootElement.ValueKind == JsonValueKind.Object) { foreach (var prop in doc.RootElement.EnumerateObject()) { var val = prop.Value.ToString(); if (!string.IsNullOrEmpty(val)) tags[$"step.{prop.Name}"] = val.Length > 500 ? val[..500] + "…" : val; } } else { tags["step.data"] = json.Length > 500 ? json[..500] + "…" : json; } } catch { tags["step.data"] = data.ToString() ?? string.Empty; } } activity.AddEvent(new ActivityEvent(stepName, tags: tags)); } /// /// Opens a named child span for a BLL or DAL processing section. /// Dispose the returned (or use using var) to close the span. /// Returns null when there is no active trace — disposal of null is safe. /// Child spans appear nested under the current endpoint or subscriber span in Zipkin / Jaeger. /// /// /// Descriptive section name, e.g. "qualification-pipeline", "bulk-persist", "approval-dispatch". /// public static Activity? BeginSection(string sectionName) => GB5ActivitySources.Processing.StartActivity(sectionName, ActivityKind.Internal); /// /// Sets a tag on the active span. No-op when there is no active trace. /// Prefer structured tags (e.g. "gb5.invoice.id") over ad-hoc strings. /// public static void Tag(string key, object? value) => Activity.Current?.SetTag(key, value); /// /// Truncates a value (e.g. a published/received message payload) to a tag-friendly /// length so a full request/response body can be set directly as a span tag without /// bloating the trace — the raw, untruncated value always still lives in the DB row /// this span reads or writes (TJOBQUEUE.PAYLOAD, TJOBEXECUTION.SUCCESSMESSAGE, etc). /// public static string Preview(string? value, int maxLength = 2000) { if (string.IsNullOrEmpty(value)) return string.Empty; return value.Length <= maxLength ? value : $"{value[..maxLength]}...[truncated, {value.Length} chars total]"; } /// /// Adds an error event to the active span without marking it as failed. /// Use this for non-fatal / recoverable errors you want visible in the trace timeline. /// For fatal errors, let the exception propagate — the middleware captures it automatically. /// /// The exception to record. /// Optional human-readable context, e.g. "fallback-to-local-cache". public static void RecordError(Exception ex, string? context = null) { var activity = Activity.Current; if (activity is null) return; var tags = new ActivityTagsCollection { ["exception.type"] = ex.GetType().FullName ?? ex.GetType().Name, ["exception.message"] = ex.Message, }; if (!string.IsNullOrEmpty(context)) tags["exception.context"] = context; if (ex.StackTrace is { Length: > 0 } st) tags["exception.stacktrace"] = st.Length > 1000 ? st[..1000] + "…" : st; if (ex.InnerException is { } inner) { tags["exception.inner.type"] = inner.GetType().Name; tags["exception.inner.message"] = inner.Message; } activity.AddEvent(new ActivityEvent("error", tags: tags)); } /// /// Marks the active span as failed with a message and optional exception. /// Use this when a BLL method wants to explicitly signal failure on the span /// without throwing (e.g. returning a result object that indicates failure). /// public static void MarkFailed(string reason, Exception? ex = null) { var activity = Activity.Current; if (activity is null) return; activity.SetStatus(ActivityStatusCode.Error, reason); activity.SetTag("error", true); activity.SetTag("gb5.error.reason", reason); if (ex is not null) RecordError(ex, context: reason); } }