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);
}
}