using System.Diagnostics; using OpenTelemetry; namespace GB5Shared.Telemetry; /// /// Tags every span with gb5.call.origin so Zipkin/Jaeger can filter human-triggered /// requests separately from system-triggered work — independent of which Host process or /// business module produced the span (see and the /// gb5.module tag set in BaseEndpoint for the module-filtering half of this). /// /// Interactive — a real API caller (FastEndpoints request) started the trace. /// Automated — a Quartz job, hosted background service, Dapr pub/sub /// subscription, outbox dispatch, action handler, or workflow-engine action started the /// trace, with no user waiting on it. Automated spans additionally get /// gb5.automation.type naming the exact mechanism (see ) /// so "Automated" traces can be filtered down to one specific trigger. /// /// Runs once per span in — zero changes needed in any BLL, DAL, or /// endpoint file, the same zero-touch approach as . /// Child spans inherit their root's classification so an entire trace stays one colour; spans /// with no live in-process parent (e.g. a Dapr-subscribed handler picking up a message /// published by a different Host) fall back to their own identity, /// which is exactly the boundary where cross-process inheritance would have broken anyway. /// public sealed class GB5CallOriginProcessor : BaseProcessor { public const string OriginTag = "gb5.call.origin"; public const string Interactive = "Interactive"; public const string Automated = "Automated"; /// Set only on Automated spans — names the exact trigger mechanism. public const string AutomationTypeTag = "gb5.automation.type"; private static readonly Dictionary AutomationTypeBySource = new(StringComparer.Ordinal) { [GB5ActivitySources.Scheduler.Name] = "ScheduledJob", [GB5ActivitySources.Background.Name] = "BackgroundService", [GB5ActivitySources.DaprSubscribe.Name] = "EventSubscription", [GB5ActivitySources.Outbox.Name] = "OutboxDispatch", [GB5ActivitySources.ActionProcessor.Name] = "ActionHandler", [GB5ActivitySources.WorkflowAction.Name] = "WorkflowEngine", }; public override void OnStart(Activity activity) { // Inherit from the live parent so an entire trace stays one classification. if (activity.Parent?.GetTagItem(OriginTag) is string inheritedOrigin) { activity.SetTag(OriginTag, inheritedOrigin); if (activity.Parent?.GetTagItem(AutomationTypeTag) is string inheritedType) activity.SetTag(AutomationTypeTag, inheritedType); return; } if (AutomationTypeBySource.TryGetValue(activity.Source.Name, out var automationType)) { activity.SetTag(OriginTag, Automated); activity.SetTag(AutomationTypeTag, automationType); } else { activity.SetTag(OriginTag, Interactive); } } }