Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
472 lines
14 KiB
C#
472 lines
14 KiB
C#
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace StellaOps.Policy.Engine.Telemetry;
|
|
|
|
/// <summary>
|
|
/// Provides structured timeline events for policy evaluation and decision flows.
|
|
/// Events are emitted as structured logs with correlation to traces.
|
|
/// </summary>
|
|
public sealed class PolicyTimelineEvents
|
|
{
|
|
private readonly ILogger<PolicyTimelineEvents> _logger;
|
|
private readonly TimeProvider _timeProvider;
|
|
|
|
public PolicyTimelineEvents(ILogger<PolicyTimelineEvents> logger, TimeProvider timeProvider)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
|
}
|
|
|
|
#region Evaluation Flow Events
|
|
|
|
/// <summary>
|
|
/// Emits an event when a policy evaluation run starts.
|
|
/// </summary>
|
|
public void EmitRunStarted(string runId, string tenant, string policyId, string policyVersion, string mode)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.RunStarted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
PolicyVersion = policyVersion,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["mode"] = mode,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when a policy evaluation run completes.
|
|
/// </summary>
|
|
public void EmitRunCompleted(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
string outcome,
|
|
double durationSeconds,
|
|
int findingsCount,
|
|
string? determinismHash = null)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.RunCompleted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["outcome"] = outcome,
|
|
["duration_seconds"] = durationSeconds,
|
|
["findings_count"] = findingsCount,
|
|
["determinism_hash"] = determinismHash,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when a batch selection phase starts.
|
|
/// </summary>
|
|
public void EmitSelectionStarted(string runId, string tenant, string policyId, int batchNumber)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.SelectionStarted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["batch_number"] = batchNumber,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when a batch selection phase completes.
|
|
/// </summary>
|
|
public void EmitSelectionCompleted(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
int batchNumber,
|
|
int tupleCount,
|
|
double durationSeconds)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.SelectionCompleted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["batch_number"] = batchNumber,
|
|
["tuple_count"] = tupleCount,
|
|
["duration_seconds"] = durationSeconds,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when batch evaluation starts.
|
|
/// </summary>
|
|
public void EmitEvaluationStarted(string runId, string tenant, string policyId, int batchNumber, int tupleCount)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.EvaluationStarted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["batch_number"] = batchNumber,
|
|
["tuple_count"] = tupleCount,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when batch evaluation completes.
|
|
/// </summary>
|
|
public void EmitEvaluationCompleted(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
int batchNumber,
|
|
int rulesEvaluated,
|
|
int rulesFired,
|
|
double durationSeconds)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.EvaluationCompleted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["batch_number"] = batchNumber,
|
|
["rules_evaluated"] = rulesEvaluated,
|
|
["rules_fired"] = rulesFired,
|
|
["duration_seconds"] = durationSeconds,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Decision Flow Events
|
|
|
|
/// <summary>
|
|
/// Emits an event when a rule matches during evaluation.
|
|
/// </summary>
|
|
public void EmitRuleMatched(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
string ruleId,
|
|
string findingKey,
|
|
string? severity = null)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.RuleMatched,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["rule_id"] = ruleId,
|
|
["finding_key"] = findingKey,
|
|
["severity"] = severity,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when a VEX override is applied.
|
|
/// </summary>
|
|
public void EmitVexOverrideApplied(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
string findingKey,
|
|
string vendor,
|
|
string status,
|
|
string? justification = null)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.VexOverrideApplied,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["finding_key"] = findingKey,
|
|
["vendor"] = vendor,
|
|
["status"] = status,
|
|
["justification"] = justification,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when a final verdict is determined for a finding.
|
|
/// </summary>
|
|
public void EmitVerdictDetermined(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
string findingKey,
|
|
string verdict,
|
|
string severity,
|
|
string? reachabilityState = null,
|
|
IReadOnlyList<string>? contributingRules = null)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.VerdictDetermined,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["finding_key"] = findingKey,
|
|
["verdict"] = verdict,
|
|
["severity"] = severity,
|
|
["reachability_state"] = reachabilityState,
|
|
["contributing_rules"] = contributingRules,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when materialization of findings starts.
|
|
/// </summary>
|
|
public void EmitMaterializationStarted(string runId, string tenant, string policyId, int findingsCount)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.MaterializationStarted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["findings_count"] = findingsCount,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when materialization of findings completes.
|
|
/// </summary>
|
|
public void EmitMaterializationCompleted(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
int findingsWritten,
|
|
int findingsUpdated,
|
|
double durationSeconds)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.MaterializationCompleted,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["findings_written"] = findingsWritten,
|
|
["findings_updated"] = findingsUpdated,
|
|
["duration_seconds"] = durationSeconds,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Events
|
|
|
|
/// <summary>
|
|
/// Emits an event when an error occurs during evaluation.
|
|
/// </summary>
|
|
public void EmitError(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
string errorCode,
|
|
string errorMessage,
|
|
string? phase = null)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.Error,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["error_code"] = errorCode,
|
|
["error_message"] = errorMessage,
|
|
["phase"] = phase,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt, LogLevel.Error);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits an event when a determinism violation is detected.
|
|
/// </summary>
|
|
public void EmitDeterminismViolation(
|
|
string runId,
|
|
string tenant,
|
|
string policyId,
|
|
string violationType,
|
|
string details)
|
|
{
|
|
var evt = new TimelineEvent
|
|
{
|
|
EventType = TimelineEventType.DeterminismViolation,
|
|
Timestamp = _timeProvider.GetUtcNow(),
|
|
RunId = runId,
|
|
Tenant = tenant,
|
|
PolicyId = policyId,
|
|
TraceId = Activity.Current?.TraceId.ToString(),
|
|
SpanId = Activity.Current?.SpanId.ToString(),
|
|
Data = new Dictionary<string, object?>
|
|
{
|
|
["violation_type"] = violationType,
|
|
["details"] = details,
|
|
},
|
|
};
|
|
|
|
LogTimelineEvent(evt, LogLevel.Warning);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void LogTimelineEvent(TimelineEvent evt, LogLevel level = LogLevel.Information)
|
|
{
|
|
_logger.Log(
|
|
level,
|
|
"PolicyTimeline: {EventType} | run={RunId} tenant={Tenant} policy={PolicyId} trace={TraceId} span={SpanId} data={Data}",
|
|
evt.EventType,
|
|
evt.RunId,
|
|
evt.Tenant,
|
|
evt.PolicyId,
|
|
evt.TraceId,
|
|
evt.SpanId,
|
|
JsonSerializer.Serialize(evt.Data, TimelineEventJsonContext.Default.DictionaryStringObject));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Types of timeline events emitted during policy evaluation.
|
|
/// </summary>
|
|
public enum TimelineEventType
|
|
{
|
|
RunStarted,
|
|
RunCompleted,
|
|
SelectionStarted,
|
|
SelectionCompleted,
|
|
EvaluationStarted,
|
|
EvaluationCompleted,
|
|
RuleMatched,
|
|
VexOverrideApplied,
|
|
VerdictDetermined,
|
|
MaterializationStarted,
|
|
MaterializationCompleted,
|
|
Error,
|
|
DeterminismViolation,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a timeline event for policy evaluation flows.
|
|
/// </summary>
|
|
public sealed record TimelineEvent
|
|
{
|
|
public required TimelineEventType EventType { get; init; }
|
|
public required DateTimeOffset Timestamp { get; init; }
|
|
public required string RunId { get; init; }
|
|
public required string Tenant { get; init; }
|
|
public required string PolicyId { get; init; }
|
|
public string? PolicyVersion { get; init; }
|
|
public string? TraceId { get; init; }
|
|
public string? SpanId { get; init; }
|
|
public Dictionary<string, object?>? Data { get; init; }
|
|
}
|
|
|
|
[JsonSerializable(typeof(Dictionary<string, object?>))]
|
|
[JsonSourceGenerationOptions(WriteIndented = false)]
|
|
internal partial class TimelineEventJsonContext : JsonSerializerContext
|
|
{
|
|
}
|