Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ProofGenerationMetrics.cs
|
||||
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
|
||||
// Tasks: DET-GAP-21, DET-GAP-22, DET-GAP-23, DET-GAP-24
|
||||
// Description: Metrics for proof generation rate, size, replay success, and dedup ratio
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Telemetry.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Metrics for proof generation tracking.
|
||||
/// Measures generation rate, sizes, replay success, and deduplication effectiveness.
|
||||
/// </summary>
|
||||
public sealed class ProofGenerationMetrics : IDisposable
|
||||
{
|
||||
public const string MeterName = "StellaOps.ProofGeneration";
|
||||
|
||||
private readonly Meter _meter;
|
||||
|
||||
// Rate metrics (DET-GAP-21)
|
||||
private readonly Counter<long> _proofsGenerated;
|
||||
private readonly Histogram<double> _proofGenerationDuration;
|
||||
|
||||
// Size metrics (DET-GAP-22)
|
||||
private readonly Histogram<double> _proofSizeBytes;
|
||||
private readonly ConcurrentDictionary<(string TenantId, string ProofType), long> _medianSizes = new();
|
||||
|
||||
// Replay metrics (DET-GAP-23)
|
||||
private readonly Counter<long> _replayAttempts;
|
||||
private readonly Counter<long> _replaySuccesses;
|
||||
private readonly Counter<long> _replayFailures;
|
||||
private readonly ConcurrentDictionary<(string TenantId, string FailureReason), long> _replayFailureCounts = new();
|
||||
|
||||
// Dedup metrics (DET-GAP-24)
|
||||
private readonly Counter<long> _totalProofsRequested;
|
||||
private readonly Counter<long> _uniqueProofsGenerated;
|
||||
private readonly Counter<long> _dedupHits;
|
||||
private readonly ConcurrentDictionary<string, double> _dedupRatios = new();
|
||||
|
||||
// Observable gauges for ratios
|
||||
private readonly ObservableGauge<double> _replaySuccessRate;
|
||||
private readonly ObservableGauge<double> _dedupRatio;
|
||||
|
||||
public ProofGenerationMetrics(string version = "1.0.0")
|
||||
{
|
||||
_meter = new Meter(MeterName, version);
|
||||
|
||||
// === DET-GAP-21: Proof generation rate ===
|
||||
_proofsGenerated = _meter.CreateCounter<long>(
|
||||
name: "stellaops_proofs_generated_total",
|
||||
unit: "{proof}",
|
||||
description: "Total number of proofs generated.");
|
||||
|
||||
_proofGenerationDuration = _meter.CreateHistogram<double>(
|
||||
name: "stellaops_proof_generation_duration_seconds",
|
||||
unit: "s",
|
||||
description: "Time to generate a proof.",
|
||||
advice: new InstrumentAdvice<double>
|
||||
{
|
||||
HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
||||
});
|
||||
|
||||
// === DET-GAP-22: Proof size metrics ===
|
||||
_proofSizeBytes = _meter.CreateHistogram<double>(
|
||||
name: "stellaops_proof_size_bytes",
|
||||
unit: "By",
|
||||
description: "Size of generated proofs in bytes.",
|
||||
advice: new InstrumentAdvice<double>
|
||||
{
|
||||
HistogramBucketBoundaries = [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072]
|
||||
});
|
||||
|
||||
// === DET-GAP-23: Replay success metrics ===
|
||||
_replayAttempts = _meter.CreateCounter<long>(
|
||||
name: "stellaops_replay_attempts_total",
|
||||
unit: "{attempt}",
|
||||
description: "Total replay attempts.");
|
||||
|
||||
_replaySuccesses = _meter.CreateCounter<long>(
|
||||
name: "stellaops_replay_successes_total",
|
||||
unit: "{attempt}",
|
||||
description: "Successful replay attempts.");
|
||||
|
||||
_replayFailures = _meter.CreateCounter<long>(
|
||||
name: "stellaops_replay_failures_total",
|
||||
unit: "{attempt}",
|
||||
description: "Failed replay attempts.");
|
||||
|
||||
_replaySuccessRate = _meter.CreateObservableGauge(
|
||||
name: "stellaops_replay_success_rate",
|
||||
observeValue: ObserveReplaySuccessRate,
|
||||
unit: "1",
|
||||
description: "Ratio of successful replays to total attempts.");
|
||||
|
||||
// === DET-GAP-24: Dedup metrics ===
|
||||
_totalProofsRequested = _meter.CreateCounter<long>(
|
||||
name: "stellaops_proofs_requested_total",
|
||||
unit: "{proof}",
|
||||
description: "Total proof generation requests (before dedup).");
|
||||
|
||||
_uniqueProofsGenerated = _meter.CreateCounter<long>(
|
||||
name: "stellaops_proofs_unique_total",
|
||||
unit: "{proof}",
|
||||
description: "Unique proofs generated (after dedup).");
|
||||
|
||||
_dedupHits = _meter.CreateCounter<long>(
|
||||
name: "stellaops_proof_dedup_hits_total",
|
||||
unit: "{hit}",
|
||||
description: "Number of proof requests served from cache.");
|
||||
|
||||
_dedupRatio = _meter.CreateObservableGauge(
|
||||
name: "stellaops_proof_dedup_ratio",
|
||||
observeValue: ObserveDedupRatio,
|
||||
unit: "1",
|
||||
description: "Ratio of unique proofs to total requests (lower = better dedup).");
|
||||
}
|
||||
|
||||
// === DET-GAP-21: Proof generation rate ===
|
||||
|
||||
/// <summary>
|
||||
/// Records a proof generation event.
|
||||
/// </summary>
|
||||
public void RecordProofGenerated(
|
||||
string tenantId,
|
||||
string proofType,
|
||||
TimeSpan duration,
|
||||
long sizeBytes)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "tenant_id", NormalizeLabel(tenantId) },
|
||||
{ "proof_type", NormalizeLabel(proofType) }
|
||||
};
|
||||
|
||||
_proofsGenerated.Add(1, tags);
|
||||
_proofGenerationDuration.Record(duration.TotalSeconds, tags);
|
||||
_proofSizeBytes.Record(sizeBytes, tags);
|
||||
|
||||
// Track for median calculation
|
||||
var key = (NormalizeLabel(tenantId), NormalizeLabel(proofType));
|
||||
_medianSizes.AddOrUpdate(key, sizeBytes, (_, existing) => (existing + sizeBytes) / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a proof generation with timing scope.
|
||||
/// </summary>
|
||||
public ProofGenerationScope StartGeneration(string tenantId, string proofType)
|
||||
{
|
||||
return new ProofGenerationScope(this, tenantId, proofType);
|
||||
}
|
||||
|
||||
// === DET-GAP-23: Replay metrics ===
|
||||
|
||||
/// <summary>
|
||||
/// Records a replay attempt result.
|
||||
/// </summary>
|
||||
public void RecordReplayResult(
|
||||
string tenantId,
|
||||
bool success,
|
||||
string? failureReason = null)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "tenant_id", NormalizeLabel(tenantId) }
|
||||
};
|
||||
|
||||
_replayAttempts.Add(1, tags);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_replaySuccesses.Add(1, tags);
|
||||
}
|
||||
else
|
||||
{
|
||||
_replayFailures.Add(1, tags);
|
||||
|
||||
if (!string.IsNullOrEmpty(failureReason))
|
||||
{
|
||||
var failureTags = new TagList
|
||||
{
|
||||
{ "tenant_id", NormalizeLabel(tenantId) },
|
||||
{ "reason", NormalizeLabel(failureReason) }
|
||||
};
|
||||
_replayFailures.Add(1, failureTags);
|
||||
|
||||
// Track failure reasons for analysis
|
||||
var key = (NormalizeLabel(tenantId), NormalizeLabel(failureReason));
|
||||
_replayFailureCounts.AddOrUpdate(key, 1, (_, count) => count + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === DET-GAP-24: Dedup metrics ===
|
||||
|
||||
/// <summary>
|
||||
/// Records a proof request, indicating whether it was a cache hit.
|
||||
/// </summary>
|
||||
public void RecordProofRequest(
|
||||
string tenantId,
|
||||
bool wasDeduplicated)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "tenant_id", NormalizeLabel(tenantId) }
|
||||
};
|
||||
|
||||
_totalProofsRequested.Add(1, tags);
|
||||
|
||||
if (wasDeduplicated)
|
||||
{
|
||||
_dedupHits.Add(1, tags);
|
||||
}
|
||||
else
|
||||
{
|
||||
_uniqueProofsGenerated.Add(1, tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the dedup ratio for a tenant.
|
||||
/// </summary>
|
||||
public void UpdateDedupRatio(string tenantId, long uniqueProofs, long totalRequests)
|
||||
{
|
||||
if (totalRequests > 0)
|
||||
{
|
||||
var ratio = (double)uniqueProofs / totalRequests;
|
||||
_dedupRatios[NormalizeLabel(tenantId)] = ratio;
|
||||
}
|
||||
}
|
||||
|
||||
private double ObserveReplaySuccessRate()
|
||||
{
|
||||
// This is a simplified global rate; in production you'd want per-tenant
|
||||
// This would be better implemented with a more sophisticated approach
|
||||
return 1.0; // Placeholder - actual implementation would track running totals
|
||||
}
|
||||
|
||||
private double ObserveDedupRatio()
|
||||
{
|
||||
if (_dedupRatios.IsEmpty)
|
||||
{
|
||||
return 1.0; // No dedup yet
|
||||
}
|
||||
|
||||
// Return average across all tenants
|
||||
return _dedupRatios.Values.Average();
|
||||
}
|
||||
|
||||
private static string NormalizeLabel(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "unknown" : value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_meter.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scope for timing proof generation.
|
||||
/// </summary>
|
||||
public sealed class ProofGenerationScope : IDisposable
|
||||
{
|
||||
private readonly ProofGenerationMetrics _metrics;
|
||||
private readonly string _tenantId;
|
||||
private readonly string _proofType;
|
||||
private readonly System.Diagnostics.Stopwatch _stopwatch;
|
||||
private long _sizeBytes;
|
||||
private bool _completed;
|
||||
|
||||
internal ProofGenerationScope(ProofGenerationMetrics metrics, string tenantId, string proofType)
|
||||
{
|
||||
_metrics = metrics;
|
||||
_tenantId = tenantId;
|
||||
_proofType = proofType;
|
||||
_stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the size of the generated proof.
|
||||
/// </summary>
|
||||
public void SetSize(long sizeBytes)
|
||||
{
|
||||
_sizeBytes = sizeBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the generation as complete.
|
||||
/// </summary>
|
||||
public void Complete()
|
||||
{
|
||||
if (_completed) return;
|
||||
_completed = true;
|
||||
_stopwatch.Stop();
|
||||
_metrics.RecordProofGenerated(_tenantId, _proofType, _stopwatch.Elapsed, _sizeBytes);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proof types for metrics categorization.
|
||||
/// </summary>
|
||||
public static class ProofTypes
|
||||
{
|
||||
public const string Witness = "witness";
|
||||
public const string Subgraph = "subgraph";
|
||||
public const string Spine = "spine";
|
||||
public const string VexVerdict = "vex_verdict";
|
||||
public const string DeltaVerdict = "delta_verdict";
|
||||
public const string ReachabilityAttestation = "reachability_attestation";
|
||||
public const string BundleManifest = "bundle_manifest";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Common replay failure reasons.
|
||||
/// </summary>
|
||||
public static class ReplayFailureReasons
|
||||
{
|
||||
public const string FeedSnapshotDrift = "feed_snapshot_drift";
|
||||
public const string PolicyMismatch = "policy_mismatch";
|
||||
public const string ToolchainDrift = "toolchain_drift";
|
||||
public const string MissingEvidence = "missing_evidence";
|
||||
public const string HashMismatch = "hash_mismatch";
|
||||
public const string SchemaVersionMismatch = "schema_version_mismatch";
|
||||
public const string Timeout = "timeout";
|
||||
public const string Unknown = "unknown";
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UnknownsBurndownMetrics.cs
|
||||
// Sprint: SPRINT_20251226_007_BE_determinism_gaps
|
||||
// Task: DET-GAP-25
|
||||
// Description: Tracks "unknowns" burn-down (count reduction per scan)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Telemetry.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Metrics for tracking unknowns burn-down across scans.
|
||||
/// Measures how unknown vulnerabilities are reduced over time.
|
||||
/// </summary>
|
||||
public sealed class UnknownsBurndownMetrics : IDisposable
|
||||
{
|
||||
public const string MeterName = "StellaOps.UnknownsBurndown";
|
||||
|
||||
private readonly Meter _meter;
|
||||
|
||||
// Current state tracking
|
||||
private readonly ConcurrentDictionary<(string TenantId, string SurfaceId), UnknownsState> _currentState = new();
|
||||
|
||||
// Counters for total unknowns
|
||||
private readonly Counter<long> _totalUnknownsEncountered;
|
||||
private readonly Counter<long> _unknownsResolved;
|
||||
private readonly Counter<long> _unknownsEscalated;
|
||||
|
||||
// Observable gauges for current state
|
||||
private readonly ObservableGauge<long> _currentUnknownsCount;
|
||||
private readonly ObservableGauge<double> _burndownRate;
|
||||
private readonly ObservableGauge<double> _unknownsBudgetUtilization;
|
||||
|
||||
// Histogram for reduction per scan
|
||||
private readonly Histogram<long> _unknownsReductionPerScan;
|
||||
private readonly Histogram<double> _burndownVelocity;
|
||||
|
||||
public UnknownsBurndownMetrics(string version = "1.0.0")
|
||||
{
|
||||
_meter = new Meter(MeterName, version);
|
||||
|
||||
// Total counters
|
||||
_totalUnknownsEncountered = _meter.CreateCounter<long>(
|
||||
name: "stellaops_unknowns_encountered_total",
|
||||
unit: "{unknown}",
|
||||
description: "Total unknown findings encountered.");
|
||||
|
||||
_unknownsResolved = _meter.CreateCounter<long>(
|
||||
name: "stellaops_unknowns_resolved_total",
|
||||
unit: "{unknown}",
|
||||
description: "Total unknowns resolved (reclassified to known state).");
|
||||
|
||||
_unknownsEscalated = _meter.CreateCounter<long>(
|
||||
name: "stellaops_unknowns_escalated_total",
|
||||
unit: "{unknown}",
|
||||
description: "Total unknowns escalated (exceeded budget).");
|
||||
|
||||
// Current state gauges
|
||||
_currentUnknownsCount = _meter.CreateObservableGauge(
|
||||
name: "stellaops_unknowns_current",
|
||||
observeValues: ObserveCurrentUnknowns,
|
||||
unit: "{unknown}",
|
||||
description: "Current number of unknowns by tenant and surface.");
|
||||
|
||||
_burndownRate = _meter.CreateObservableGauge(
|
||||
name: "stellaops_unknowns_burndown_rate",
|
||||
observeValues: ObserveBurndownRate,
|
||||
unit: "1",
|
||||
description: "Rate of unknowns reduction (0-1, higher = faster burndown).");
|
||||
|
||||
_unknownsBudgetUtilization = _meter.CreateObservableGauge(
|
||||
name: "stellaops_unknowns_budget_utilization",
|
||||
observeValues: ObserveBudgetUtilization,
|
||||
unit: "1",
|
||||
description: "Ratio of current unknowns to budget limit (>1 = over budget).");
|
||||
|
||||
// Histograms
|
||||
_unknownsReductionPerScan = _meter.CreateHistogram<long>(
|
||||
name: "stellaops_unknowns_reduction_per_scan",
|
||||
unit: "{unknown}",
|
||||
description: "Number of unknowns reduced per scan.",
|
||||
advice: new InstrumentAdvice<long>
|
||||
{
|
||||
HistogramBucketBoundaries = [0, 1, 5, 10, 25, 50, 100, 250, 500, 1000]
|
||||
});
|
||||
|
||||
_burndownVelocity = _meter.CreateHistogram<double>(
|
||||
name: "stellaops_unknowns_burndown_velocity",
|
||||
unit: "{unknown}/d",
|
||||
description: "Daily burn-down velocity (unknowns resolved per day).",
|
||||
advice: new InstrumentAdvice<double>
|
||||
{
|
||||
HistogramBucketBoundaries = [0.1, 0.5, 1, 2, 5, 10, 20, 50, 100]
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records unknowns state after a scan.
|
||||
/// </summary>
|
||||
public void RecordScanUnknowns(
|
||||
string tenantId,
|
||||
string surfaceId,
|
||||
int totalUnknowns,
|
||||
int newUnknowns,
|
||||
int resolvedSinceLastScan,
|
||||
int budgetLimit)
|
||||
{
|
||||
var key = (NormalizeLabel(tenantId), NormalizeLabel(surfaceId));
|
||||
var tags = CreateTags(tenantId, surfaceId);
|
||||
|
||||
// Update totals
|
||||
_totalUnknownsEncountered.Add(newUnknowns, tags);
|
||||
_unknownsResolved.Add(resolvedSinceLastScan, tags);
|
||||
|
||||
// Check for budget breach
|
||||
if (totalUnknowns > budgetLimit)
|
||||
{
|
||||
var overBudget = totalUnknowns - budgetLimit;
|
||||
_unknownsEscalated.Add(overBudget, tags);
|
||||
}
|
||||
|
||||
// Calculate reduction
|
||||
var previousState = _currentState.GetValueOrDefault(key);
|
||||
var reduction = previousState is null
|
||||
? 0
|
||||
: Math.Max(0, previousState.TotalUnknowns - totalUnknowns);
|
||||
|
||||
_unknownsReductionPerScan.Record(reduction, tags);
|
||||
|
||||
// Calculate velocity if we have previous data
|
||||
if (previousState is not null && previousState.Timestamp != default)
|
||||
{
|
||||
var daysSinceLastScan = (DateTimeOffset.UtcNow - previousState.Timestamp).TotalDays;
|
||||
if (daysSinceLastScan > 0)
|
||||
{
|
||||
var velocity = reduction / daysSinceLastScan;
|
||||
_burndownVelocity.Record(velocity, tags);
|
||||
}
|
||||
}
|
||||
|
||||
// Update current state
|
||||
_currentState[key] = new UnknownsState
|
||||
{
|
||||
TenantId = tenantId,
|
||||
SurfaceId = surfaceId,
|
||||
TotalUnknowns = totalUnknowns,
|
||||
BudgetLimit = budgetLimit,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
PreviousTotalUnknowns = previousState?.TotalUnknowns ?? totalUnknowns,
|
||||
ResolvedThisPeriod = resolvedSinceLastScan
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records resolution of specific unknowns.
|
||||
/// </summary>
|
||||
public void RecordUnknownsResolved(
|
||||
string tenantId,
|
||||
string surfaceId,
|
||||
int count,
|
||||
string resolutionReason)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "tenant_id", NormalizeLabel(tenantId) },
|
||||
{ "surface_id", NormalizeLabel(surfaceId) },
|
||||
{ "resolution_reason", NormalizeLabel(resolutionReason) }
|
||||
};
|
||||
|
||||
_unknownsResolved.Add(count, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current unknowns state for a surface.
|
||||
/// </summary>
|
||||
public UnknownsState? GetCurrentState(string tenantId, string surfaceId)
|
||||
{
|
||||
var key = (NormalizeLabel(tenantId), NormalizeLabel(surfaceId));
|
||||
return _currentState.GetValueOrDefault(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the burn-down projection.
|
||||
/// </summary>
|
||||
public BurndownProjection? CalculateProjection(string tenantId, string surfaceId)
|
||||
{
|
||||
var state = GetCurrentState(tenantId, surfaceId);
|
||||
if (state is null || state.ResolvedThisPeriod <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Estimate days to reach zero based on current velocity
|
||||
var dailyVelocity = state.ResolvedThisPeriod; // Simplified - assumes one scan per day
|
||||
var daysToZero = state.TotalUnknowns / (double)dailyVelocity;
|
||||
|
||||
return new BurndownProjection
|
||||
{
|
||||
CurrentUnknowns = state.TotalUnknowns,
|
||||
DailyBurnRate = dailyVelocity,
|
||||
EstimatedDaysToZero = (int)Math.Ceiling(daysToZero),
|
||||
ProjectedZeroDate = DateTimeOffset.UtcNow.AddDays(daysToZero),
|
||||
BudgetLimit = state.BudgetLimit,
|
||||
IsOverBudget = state.TotalUnknowns > state.BudgetLimit
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<Measurement<long>> ObserveCurrentUnknowns()
|
||||
{
|
||||
foreach (var kvp in _currentState)
|
||||
{
|
||||
var tags = CreateTags(kvp.Key.TenantId, kvp.Key.SurfaceId);
|
||||
yield return new Measurement<long>(kvp.Value.TotalUnknowns, tags);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Measurement<double>> ObserveBurndownRate()
|
||||
{
|
||||
foreach (var kvp in _currentState)
|
||||
{
|
||||
var state = kvp.Value;
|
||||
if (state.PreviousTotalUnknowns > 0)
|
||||
{
|
||||
var rate = 1.0 - ((double)state.TotalUnknowns / state.PreviousTotalUnknowns);
|
||||
var tags = CreateTags(kvp.Key.TenantId, kvp.Key.SurfaceId);
|
||||
yield return new Measurement<double>(Math.Max(0, rate), tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Measurement<double>> ObserveBudgetUtilization()
|
||||
{
|
||||
foreach (var kvp in _currentState)
|
||||
{
|
||||
var state = kvp.Value;
|
||||
if (state.BudgetLimit > 0)
|
||||
{
|
||||
var utilization = (double)state.TotalUnknowns / state.BudgetLimit;
|
||||
var tags = CreateTags(kvp.Key.TenantId, kvp.Key.SurfaceId);
|
||||
yield return new Measurement<double>(utilization, tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TagList CreateTags(string tenantId, string surfaceId)
|
||||
{
|
||||
return new TagList
|
||||
{
|
||||
{ "tenant_id", NormalizeLabel(tenantId) },
|
||||
{ "surface_id", NormalizeLabel(surfaceId) }
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeLabel(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "unknown" : value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_meter.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current state of unknowns for a surface.
|
||||
/// </summary>
|
||||
public sealed record UnknownsState
|
||||
{
|
||||
public required string TenantId { get; init; }
|
||||
public required string SurfaceId { get; init; }
|
||||
public required int TotalUnknowns { get; init; }
|
||||
public required int BudgetLimit { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public int PreviousTotalUnknowns { get; init; }
|
||||
public int ResolvedThisPeriod { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projection for unknowns burn-down.
|
||||
/// </summary>
|
||||
public sealed record BurndownProjection
|
||||
{
|
||||
public required int CurrentUnknowns { get; init; }
|
||||
public required int DailyBurnRate { get; init; }
|
||||
public required int EstimatedDaysToZero { get; init; }
|
||||
public required DateTimeOffset ProjectedZeroDate { get; init; }
|
||||
public required int BudgetLimit { get; init; }
|
||||
public required bool IsOverBudget { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reasons for unknowns resolution.
|
||||
/// </summary>
|
||||
public static class UnknownsResolutionReasons
|
||||
{
|
||||
public const string VexUpdated = "vex_updated";
|
||||
public const string ReachabilityAnalyzed = "reachability_analyzed";
|
||||
public const string RuntimeObserved = "runtime_observed";
|
||||
public const string ManualTriage = "manual_triage";
|
||||
public const string PolicyException = "policy_exception";
|
||||
public const string FalsePositive = "false_positive";
|
||||
public const string AdvisoryUpdated = "advisory_updated";
|
||||
public const string PackageUpgraded = "package_upgraded";
|
||||
public const string ComponentRemoved = "component_removed";
|
||||
}
|
||||
Reference in New Issue
Block a user