Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management. - Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management. - Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support. - Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FidelityMetricsTelemetry.cs
|
||||
// Sprint: SPRINT_3403_0001_0001_fidelity_metrics
|
||||
// Task: FID-3403-008
|
||||
// Description: Prometheus gauges for Bitwise, Semantic, and Policy fidelity metrics
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Telemetry.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Prometheus gauges for fidelity metrics (BF, SF, PF).
|
||||
/// </summary>
|
||||
public sealed class FidelityMetricsTelemetry : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Meter name for fidelity metrics.
|
||||
/// </summary>
|
||||
public const string MeterName = "StellaOps.Fidelity";
|
||||
|
||||
private readonly Meter _meter;
|
||||
private readonly object _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
// Latest fidelity values per (tenant, surface)
|
||||
private readonly Dictionary<string, FidelitySnapshot> _snapshots = new();
|
||||
|
||||
// Observable gauges
|
||||
private readonly ObservableGauge<double> _bitwiseFidelityGauge;
|
||||
private readonly ObservableGauge<double> _semanticFidelityGauge;
|
||||
private readonly ObservableGauge<double> _policyFidelityGauge;
|
||||
private readonly ObservableGauge<int> _totalReplaysGauge;
|
||||
|
||||
// Counters for SLO tracking
|
||||
private readonly Counter<long> _sloBreachCounter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="FidelityMetricsTelemetry"/>.
|
||||
/// </summary>
|
||||
public FidelityMetricsTelemetry(FidelityTelemetryOptions? options = null)
|
||||
{
|
||||
var opts = options ?? new FidelityTelemetryOptions();
|
||||
_meter = new Meter(MeterName, opts.Version);
|
||||
|
||||
_bitwiseFidelityGauge = _meter.CreateObservableGauge(
|
||||
name: "fidelity_bitwise_ratio",
|
||||
observeValue: () => ObserveMetric(s => s.BitwiseFidelity),
|
||||
unit: "{ratio}",
|
||||
description: "Bitwise fidelity ratio (identical_outputs / total_replays).");
|
||||
|
||||
_semanticFidelityGauge = _meter.CreateObservableGauge(
|
||||
name: "fidelity_semantic_ratio",
|
||||
observeValue: () => ObserveMetric(s => s.SemanticFidelity),
|
||||
unit: "{ratio}",
|
||||
description: "Semantic fidelity ratio (semantically equivalent outputs / total).");
|
||||
|
||||
_policyFidelityGauge = _meter.CreateObservableGauge(
|
||||
name: "fidelity_policy_ratio",
|
||||
observeValue: () => ObserveMetric(s => s.PolicyFidelity),
|
||||
unit: "{ratio}",
|
||||
description: "Policy fidelity ratio (matching policy decisions / total).");
|
||||
|
||||
_totalReplaysGauge = _meter.CreateObservableGauge(
|
||||
name: "fidelity_total_replays",
|
||||
observeValue: () => ObserveMetric(s => s.TotalReplays),
|
||||
unit: "{replays}",
|
||||
description: "Total number of replay runs measured.");
|
||||
|
||||
_sloBreachCounter = _meter.CreateCounter<long>(
|
||||
name: "fidelity_slo_breach_total",
|
||||
unit: "{breach}",
|
||||
description: "Total number of fidelity SLO breaches.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a fidelity snapshot for a tenant/surface.
|
||||
/// </summary>
|
||||
public void RecordSnapshot(
|
||||
double bitwiseFidelity,
|
||||
double semanticFidelity,
|
||||
double policyFidelity,
|
||||
int totalReplays,
|
||||
string? tenantId = null,
|
||||
string? surfaceId = null)
|
||||
{
|
||||
var key = BuildKey(tenantId, surfaceId);
|
||||
var snapshot = new FidelitySnapshot
|
||||
{
|
||||
BitwiseFidelity = bitwiseFidelity,
|
||||
SemanticFidelity = semanticFidelity,
|
||||
PolicyFidelity = policyFidelity,
|
||||
TotalReplays = totalReplays,
|
||||
TenantId = tenantId,
|
||||
SurfaceId = surfaceId,
|
||||
RecordedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_snapshots[key] = snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an SLO breach for fidelity metrics.
|
||||
/// </summary>
|
||||
public void RecordSloBreachDirect(
|
||||
FidelityBreachType breachType,
|
||||
double actualValue,
|
||||
double thresholdValue,
|
||||
string? tenantId = null,
|
||||
string? surfaceId = null)
|
||||
{
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "breach_type", breachType.ToString().ToLowerInvariant() },
|
||||
{ "actual_value", actualValue },
|
||||
{ "threshold_value", thresholdValue }
|
||||
};
|
||||
if (!string.IsNullOrEmpty(tenantId)) tags.Add("tenant_id", tenantId);
|
||||
if (!string.IsNullOrEmpty(surfaceId)) tags.Add("surface_id", surfaceId);
|
||||
|
||||
_sloBreachCounter.Add(1, tags);
|
||||
}
|
||||
|
||||
private IEnumerable<Measurement<double>> ObserveMetric(Func<FidelitySnapshot, double> selector)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var snapshot in _snapshots.Values)
|
||||
{
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("tenant_id", snapshot.TenantId ?? ""),
|
||||
new("surface_id", snapshot.SurfaceId ?? "")
|
||||
};
|
||||
yield return new Measurement<double>(selector(snapshot), tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Measurement<int>> ObserveMetric(Func<FidelitySnapshot, int> selector)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var snapshot in _snapshots.Values)
|
||||
{
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("tenant_id", snapshot.TenantId ?? ""),
|
||||
new("surface_id", snapshot.SurfaceId ?? "")
|
||||
};
|
||||
yield return new Measurement<int>(selector(snapshot), tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildKey(string? tenantId, string? surfaceId)
|
||||
{
|
||||
return $"{tenantId ?? ""}|{surfaceId ?? ""}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_meter.Dispose();
|
||||
}
|
||||
|
||||
private sealed record FidelitySnapshot
|
||||
{
|
||||
public required double BitwiseFidelity { get; init; }
|
||||
public required double SemanticFidelity { get; init; }
|
||||
public required double PolicyFidelity { get; init; }
|
||||
public required int TotalReplays { get; init; }
|
||||
public string? TenantId { get; init; }
|
||||
public string? SurfaceId { get; init; }
|
||||
public DateTimeOffset RecordedAt { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for fidelity telemetry.
|
||||
/// </summary>
|
||||
public sealed class FidelityTelemetryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Metric version.
|
||||
/// </summary>
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of fidelity SLO breach.
|
||||
/// </summary>
|
||||
public enum FidelityBreachType
|
||||
{
|
||||
/// <summary>Bitwise fidelity below threshold</summary>
|
||||
Bitwise,
|
||||
|
||||
/// <summary>Semantic fidelity below threshold</summary>
|
||||
Semantic,
|
||||
|
||||
/// <summary>Policy fidelity below threshold</summary>
|
||||
Policy
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FidelitySloAlertingService.cs
|
||||
// Sprint: SPRINT_3403_0001_0001_fidelity_metrics
|
||||
// Task: FID-3403-009
|
||||
// Description: SLO alerting for fidelity thresholds
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Telemetry.Core;
|
||||
|
||||
/// <summary>
|
||||
/// SLO alerting service for fidelity metrics.
|
||||
/// Checks fidelity scores against thresholds and records breaches.
|
||||
/// </summary>
|
||||
public sealed class FidelitySloAlertingService
|
||||
{
|
||||
private readonly FidelityMetricsTelemetry _telemetry;
|
||||
private readonly FidelitySloOptions _options;
|
||||
private readonly ILogger<FidelitySloAlertingService> _logger;
|
||||
|
||||
public FidelitySloAlertingService(
|
||||
FidelityMetricsTelemetry telemetry,
|
||||
IOptions<FidelitySloOptions> options,
|
||||
ILogger<FidelitySloAlertingService> logger)
|
||||
{
|
||||
_telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry));
|
||||
_options = options?.Value ?? new FidelitySloOptions();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate fidelity metrics against SLO thresholds.
|
||||
/// </summary>
|
||||
public FidelitySloResult Evaluate(
|
||||
double bitwiseFidelity,
|
||||
double semanticFidelity,
|
||||
double policyFidelity,
|
||||
int totalReplays,
|
||||
string? tenantId = null,
|
||||
string? surfaceId = null)
|
||||
{
|
||||
var breaches = new List<FidelitySloBreachInfo>();
|
||||
|
||||
// Record the snapshot
|
||||
_telemetry.RecordSnapshot(
|
||||
bitwiseFidelity,
|
||||
semanticFidelity,
|
||||
policyFidelity,
|
||||
totalReplays,
|
||||
tenantId,
|
||||
surfaceId);
|
||||
|
||||
// Check bitwise fidelity
|
||||
if (bitwiseFidelity < _options.BitwiseFidelityThreshold)
|
||||
{
|
||||
var breach = new FidelitySloBreachInfo
|
||||
{
|
||||
BreachType = FidelityBreachType.Bitwise,
|
||||
ActualValue = bitwiseFidelity,
|
||||
ThresholdValue = _options.BitwiseFidelityThreshold,
|
||||
Severity = GetSeverity(bitwiseFidelity, _options.BitwiseFidelityThreshold, _options.BitwiseFidelityCritical)
|
||||
};
|
||||
breaches.Add(breach);
|
||||
|
||||
_telemetry.RecordSloBreachDirect(
|
||||
FidelityBreachType.Bitwise,
|
||||
bitwiseFidelity,
|
||||
_options.BitwiseFidelityThreshold,
|
||||
tenantId,
|
||||
surfaceId);
|
||||
|
||||
_logger.LogWarning(
|
||||
"Bitwise fidelity SLO breach: {Actual:P2} < {Threshold:P2} (tenant={Tenant})",
|
||||
bitwiseFidelity, _options.BitwiseFidelityThreshold, tenantId ?? "global");
|
||||
}
|
||||
|
||||
// Check semantic fidelity
|
||||
if (semanticFidelity < _options.SemanticFidelityThreshold)
|
||||
{
|
||||
var breach = new FidelitySloBreachInfo
|
||||
{
|
||||
BreachType = FidelityBreachType.Semantic,
|
||||
ActualValue = semanticFidelity,
|
||||
ThresholdValue = _options.SemanticFidelityThreshold,
|
||||
Severity = GetSeverity(semanticFidelity, _options.SemanticFidelityThreshold, _options.SemanticFidelityCritical)
|
||||
};
|
||||
breaches.Add(breach);
|
||||
|
||||
_telemetry.RecordSloBreachDirect(
|
||||
FidelityBreachType.Semantic,
|
||||
semanticFidelity,
|
||||
_options.SemanticFidelityThreshold,
|
||||
tenantId,
|
||||
surfaceId);
|
||||
|
||||
_logger.LogWarning(
|
||||
"Semantic fidelity SLO breach: {Actual:P2} < {Threshold:P2} (tenant={Tenant})",
|
||||
semanticFidelity, _options.SemanticFidelityThreshold, tenantId ?? "global");
|
||||
}
|
||||
|
||||
// Check policy fidelity
|
||||
if (policyFidelity < _options.PolicyFidelityThreshold)
|
||||
{
|
||||
var breach = new FidelitySloBreachInfo
|
||||
{
|
||||
BreachType = FidelityBreachType.Policy,
|
||||
ActualValue = policyFidelity,
|
||||
ThresholdValue = _options.PolicyFidelityThreshold,
|
||||
Severity = GetSeverity(policyFidelity, _options.PolicyFidelityThreshold, _options.PolicyFidelityCritical)
|
||||
};
|
||||
breaches.Add(breach);
|
||||
|
||||
_telemetry.RecordSloBreachDirect(
|
||||
FidelityBreachType.Policy,
|
||||
policyFidelity,
|
||||
_options.PolicyFidelityThreshold,
|
||||
tenantId,
|
||||
surfaceId);
|
||||
|
||||
_logger.LogError(
|
||||
"Policy fidelity SLO breach: {Actual:P2} < {Threshold:P2} (tenant={Tenant})",
|
||||
policyFidelity, _options.PolicyFidelityThreshold, tenantId ?? "global");
|
||||
}
|
||||
|
||||
return new FidelitySloResult
|
||||
{
|
||||
Passed = breaches.Count == 0,
|
||||
Breaches = breaches,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static FidelityBreachSeverity GetSeverity(double actual, double warning, double critical)
|
||||
{
|
||||
if (actual < critical) return FidelityBreachSeverity.Critical;
|
||||
if (actual < warning) return FidelityBreachSeverity.Warning;
|
||||
return FidelityBreachSeverity.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for fidelity SLO thresholds.
|
||||
/// </summary>
|
||||
public sealed class FidelitySloOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Bitwise fidelity warning threshold.
|
||||
/// </summary>
|
||||
public double BitwiseFidelityThreshold { get; init; } = 0.98;
|
||||
|
||||
/// <summary>
|
||||
/// Bitwise fidelity critical threshold.
|
||||
/// </summary>
|
||||
public double BitwiseFidelityCritical { get; init; } = 0.90;
|
||||
|
||||
/// <summary>
|
||||
/// Semantic fidelity warning threshold.
|
||||
/// </summary>
|
||||
public double SemanticFidelityThreshold { get; init; } = 0.99;
|
||||
|
||||
/// <summary>
|
||||
/// Semantic fidelity critical threshold.
|
||||
/// </summary>
|
||||
public double SemanticFidelityCritical { get; init; } = 0.95;
|
||||
|
||||
/// <summary>
|
||||
/// Policy fidelity warning threshold.
|
||||
/// </summary>
|
||||
public double PolicyFidelityThreshold { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Policy fidelity critical threshold.
|
||||
/// </summary>
|
||||
public double PolicyFidelityCritical { get; init; } = 0.99;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of fidelity SLO evaluation.
|
||||
/// </summary>
|
||||
public sealed record FidelitySloResult
|
||||
{
|
||||
public required bool Passed { get; init; }
|
||||
public required IReadOnlyList<FidelitySloBreachInfo> Breaches { get; init; }
|
||||
public DateTimeOffset EvaluatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a specific fidelity SLO breach.
|
||||
/// </summary>
|
||||
public sealed record FidelitySloBreachInfo
|
||||
{
|
||||
public required FidelityBreachType BreachType { get; init; }
|
||||
public required double ActualValue { get; init; }
|
||||
public required double ThresholdValue { get; init; }
|
||||
public FidelityBreachSeverity Severity { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity level for fidelity breaches.
|
||||
/// </summary>
|
||||
public enum FidelityBreachSeverity
|
||||
{
|
||||
None,
|
||||
Warning,
|
||||
Critical
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TtePercentileExporter.cs
|
||||
// Sprint: SPRINT_3406_0001_0001_metrics_tables
|
||||
// Task: METRICS-3406-010
|
||||
// Description: Exports TTE percentiles to Prometheus via OpenTelemetry
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Telemetry.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Exports Time-to-Evidence (TTE) percentiles to Prometheus.
|
||||
/// Provides p50, p90, p99 latency metrics for each TTE phase.
|
||||
/// </summary>
|
||||
public sealed class TtePercentileExporter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Meter name for TTE percentile metrics.
|
||||
/// </summary>
|
||||
public const string MeterName = "StellaOps.TimeToEvidence.Percentiles";
|
||||
|
||||
private readonly Meter _meter;
|
||||
private readonly object _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
// Rolling window data per phase (tenant, surface)
|
||||
private readonly Dictionary<string, LatencyWindow> _windows = new();
|
||||
private readonly int _windowSizeSeconds;
|
||||
private readonly int _maxSamplesPerWindow;
|
||||
|
||||
// Observable gauges for percentiles
|
||||
private readonly ObservableGauge<double> _p50Gauge;
|
||||
private readonly ObservableGauge<double> _p90Gauge;
|
||||
private readonly ObservableGauge<double> _p99Gauge;
|
||||
private readonly ObservableGauge<double> _maxGauge;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="TtePercentileExporter"/>.
|
||||
/// </summary>
|
||||
public TtePercentileExporter(TtePercentileOptions? options = null)
|
||||
{
|
||||
var opts = options ?? new TtePercentileOptions();
|
||||
_windowSizeSeconds = opts.WindowSizeSeconds;
|
||||
_maxSamplesPerWindow = opts.MaxSamplesPerWindow;
|
||||
|
||||
_meter = new Meter(MeterName, opts.Version);
|
||||
|
||||
_p50Gauge = _meter.CreateObservableGauge(
|
||||
name: "tte_latency_p50_seconds",
|
||||
observeValue: () => ObservePercentile(0.50),
|
||||
unit: "s",
|
||||
description: "50th percentile (median) TTE latency in seconds.");
|
||||
|
||||
_p90Gauge = _meter.CreateObservableGauge(
|
||||
name: "tte_latency_p90_seconds",
|
||||
observeValue: () => ObservePercentile(0.90),
|
||||
unit: "s",
|
||||
description: "90th percentile TTE latency in seconds.");
|
||||
|
||||
_p99Gauge = _meter.CreateObservableGauge(
|
||||
name: "tte_latency_p99_seconds",
|
||||
observeValue: () => ObservePercentile(0.99),
|
||||
unit: "s",
|
||||
description: "99th percentile TTE latency in seconds.");
|
||||
|
||||
_maxGauge = _meter.CreateObservableGauge(
|
||||
name: "tte_latency_max_seconds",
|
||||
observeValue: () => ObservePercentile(1.0),
|
||||
unit: "s",
|
||||
description: "Maximum TTE latency in seconds.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a latency sample for a TTE phase.
|
||||
/// </summary>
|
||||
public void RecordLatency(TtePhase phase, double latencySeconds, string? tenantId = null, string? surface = null)
|
||||
{
|
||||
var key = BuildKey(phase, tenantId, surface);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_windows.TryGetValue(key, out var window))
|
||||
{
|
||||
window = new LatencyWindow(_windowSizeSeconds, _maxSamplesPerWindow);
|
||||
_windows[key] = window;
|
||||
}
|
||||
window.Add(latencySeconds, DateTimeOffset.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific percentile for a phase.
|
||||
/// </summary>
|
||||
public double? GetPercentile(TtePhase phase, double percentile, string? tenantId = null, string? surface = null)
|
||||
{
|
||||
var key = BuildKey(phase, tenantId, surface);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_windows.TryGetValue(key, out var window))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return window.GetPercentile(percentile);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Measurement<double>> ObservePercentile(double percentile)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var (key, window) in _windows)
|
||||
{
|
||||
var value = window.GetPercentile(percentile);
|
||||
if (value.HasValue)
|
||||
{
|
||||
var (phase, tenantId, surface) = ParseKey(key);
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("phase", phase),
|
||||
new("tenant_id", tenantId ?? ""),
|
||||
new("surface", surface ?? "")
|
||||
};
|
||||
yield return new Measurement<double>(value.Value, tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildKey(TtePhase phase, string? tenantId, string? surface)
|
||||
{
|
||||
return $"{phase}|{tenantId ?? ""}|{surface ?? ""}";
|
||||
}
|
||||
|
||||
private static (string phase, string? tenantId, string? surface) ParseKey(string key)
|
||||
{
|
||||
var parts = key.Split('|');
|
||||
return (
|
||||
parts[0],
|
||||
string.IsNullOrEmpty(parts[1]) ? null : parts[1],
|
||||
string.IsNullOrEmpty(parts[2]) ? null : parts[2]
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_meter.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolling window for latency samples.
|
||||
/// </summary>
|
||||
private sealed class LatencyWindow
|
||||
{
|
||||
private readonly int _windowSizeSeconds;
|
||||
private readonly int _maxSamples;
|
||||
private readonly List<(double Latency, DateTimeOffset Timestamp)> _samples = new();
|
||||
|
||||
public LatencyWindow(int windowSizeSeconds, int maxSamples)
|
||||
{
|
||||
_windowSizeSeconds = windowSizeSeconds;
|
||||
_maxSamples = maxSamples;
|
||||
}
|
||||
|
||||
public void Add(double latency, DateTimeOffset timestamp)
|
||||
{
|
||||
// Evict old samples
|
||||
var cutoff = timestamp.AddSeconds(-_windowSizeSeconds);
|
||||
_samples.RemoveAll(s => s.Timestamp < cutoff);
|
||||
|
||||
// Add new sample
|
||||
if (_samples.Count < _maxSamples)
|
||||
{
|
||||
_samples.Add((latency, timestamp));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reservoir sampling for large windows
|
||||
var index = Random.Shared.Next(_samples.Count + 1);
|
||||
if (index < _samples.Count)
|
||||
{
|
||||
_samples[index] = (latency, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double? GetPercentile(double percentile)
|
||||
{
|
||||
if (_samples.Count == 0) return null;
|
||||
|
||||
var sorted = _samples.Select(s => s.Latency).OrderBy(x => x).ToList();
|
||||
var index = (int)Math.Ceiling(percentile * sorted.Count) - 1;
|
||||
index = Math.Max(0, Math.Min(sorted.Count - 1, index));
|
||||
return sorted[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for TTE percentile exporter.
|
||||
/// </summary>
|
||||
public sealed class TtePercentileOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Metric version.
|
||||
/// </summary>
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Rolling window size in seconds for percentile calculation.
|
||||
/// </summary>
|
||||
public int WindowSizeSeconds { get; init; } = 300; // 5 minutes
|
||||
|
||||
/// <summary>
|
||||
/// Maximum samples to keep per window.
|
||||
/// </summary>
|
||||
public int MaxSamplesPerWindow { get; init; } = 1000;
|
||||
}
|
||||
Reference in New Issue
Block a user