Add call graph fixtures for various languages and scenarios
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
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
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

- Introduced `all-edge-reasons.json` to test edge resolution reasons in .NET.
- Added `all-visibility-levels.json` to validate method visibility levels in .NET.
- Created `dotnet-aspnetcore-minimal.json` for a minimal ASP.NET Core application.
- Included `go-gin-api.json` for a Go Gin API application structure.
- Added `java-spring-boot.json` for the Spring PetClinic application in Java.
- Introduced `legacy-no-schema.json` for legacy application structure without schema.
- Created `node-express-api.json` for an Express.js API application structure.
This commit is contained in:
master
2025-12-16 10:44:24 +02:00
parent 4391f35d8a
commit 5a480a3c2a
223 changed files with 19367 additions and 727 deletions

View File

@@ -0,0 +1,169 @@
using System.Diagnostics.Metrics;
using StellaOps.Telemetry.Core;
namespace StellaOps.Telemetry.Core.Tests;
public sealed class TimeToFirstSignalMetricsTests : IDisposable
{
private readonly MeterListener _listener;
private readonly List<RecordedMeasurement> _measurements = [];
public TimeToFirstSignalMetricsTests()
{
_listener = new MeterListener();
_listener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name == TimeToFirstSignalMetrics.MeterName)
{
listener.EnableMeasurementEvents(instrument);
}
};
_listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
{
_measurements.Add(new RecordedMeasurement(instrument.Name, measurement, tags.ToArray()));
});
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
_measurements.Add(new RecordedMeasurement(instrument.Name, measurement, tags.ToArray()));
});
_listener.Start();
}
public void Dispose() => _listener.Dispose();
[Fact]
public void RecordSignalRendered_WithValidData_RecordsHistogram()
{
using var metrics = new TimeToFirstSignalMetrics();
metrics.RecordSignalRendered(
latencySeconds: 1.234,
surface: "ui",
cacheHit: true,
signalSource: "snapshot",
kind: TtfsSignalKind.Started,
phase: TtfsPhase.Analyze,
tenantId: "tenant-1");
Assert.Contains(_measurements, m =>
m.Name == "ttfs_latency_seconds" && m.Value is double v && Math.Abs(v - 1.234) < 0.000_001);
Assert.Contains(_measurements, m => m.Name == "ttfs_signal_total" && m.Value is long v && v == 1);
Assert.Contains(_measurements, m => m.Name == "ttfs_cache_hit_total" && m.Value is long v && v == 1);
}
[Fact]
public void RecordSignalRendered_ExceedsSlo_IncrementsBreachCounter()
{
var options = new TimeToFirstSignalOptions
{
Ui = new TimeToFirstSignalSurfaceOptions { WarmPathP95Seconds = 0.1 },
};
using var metrics = new TimeToFirstSignalMetrics(options);
metrics.RecordSignalRendered(
latencySeconds: 0.2,
surface: "ui",
cacheHit: true,
signalSource: "snapshot",
kind: TtfsSignalKind.Started,
phase: TtfsPhase.Resolve,
tenantId: "tenant-1");
Assert.Contains(_measurements, m => m.Name == "ttfs_slo_breach_total" && m.Value is long v && v == 1);
}
[Fact]
public void RecordCacheHit_IncrementsCounter()
{
using var metrics = new TimeToFirstSignalMetrics();
metrics.RecordCacheLookup(
latencySeconds: 0.05,
surface: "cli",
cacheHit: true,
signalSource: "snapshot",
kind: TtfsSignalKind.Phase,
phase: TtfsPhase.Restore,
tenantId: "tenant-1");
Assert.Contains(_measurements, m => m.Name == "ttfs_cache_hit_total" && m.Value is long v && v == 1);
}
[Fact]
public void RecordCacheMiss_IncrementsCounter()
{
using var metrics = new TimeToFirstSignalMetrics();
metrics.RecordCacheLookup(
latencySeconds: 0.05,
surface: "cli",
cacheHit: false,
signalSource: "cold_start",
kind: TtfsSignalKind.Started,
phase: TtfsPhase.Fetch,
tenantId: "tenant-1");
Assert.Contains(_measurements, m => m.Name == "ttfs_cache_miss_total" && m.Value is long v && v == 1);
}
[Fact]
public void MeasureSignal_Scope_RecordsLatencyOnDispose()
{
using var metrics = new TimeToFirstSignalMetrics();
using (var scope = metrics.MeasureSignal(
surface: "ui",
cacheHit: true,
signalSource: "snapshot",
kind: TtfsSignalKind.Started,
phase: TtfsPhase.Resolve))
{
scope.Complete();
}
Assert.Contains(_measurements, m => m.Name == "ttfs_latency_seconds" && m.Value is double v && v >= 0);
}
[Fact]
public void MeasureSignal_Scope_RecordsFailureOnException()
{
using var metrics = new TimeToFirstSignalMetrics();
Assert.Throws<InvalidOperationException>((Action)(() =>
{
using (metrics.MeasureSignal(
surface: "ui",
cacheHit: false,
signalSource: "cold_start",
kind: TtfsSignalKind.Unavailable,
phase: TtfsPhase.Unknown))
{
throw new InvalidOperationException("boom");
}
}));
Assert.Contains(_measurements, m => m.Name == "ttfs_error_total" && m.Value is long v && v == 1 && m.HasTag("error_type", "exception"));
}
[Fact]
public void Options_DefaultValues_MatchAdvisory()
{
var options = new TimeToFirstSignalOptions();
Assert.Equal(2.0, options.Ui.SloP50Seconds);
Assert.Equal(5.0, options.Ui.SloP95Seconds);
Assert.Equal(0.7, options.Ui.WarmPathP50Seconds);
Assert.Equal(2.5, options.Ui.WarmPathP95Seconds);
Assert.Equal(4.0, options.Ui.ColdPathP95Seconds);
Assert.Equal(150, options.FrontendBudgetMs);
Assert.Equal(250, options.EdgeApiBudgetMs);
Assert.Equal(1500, options.CoreServicesBudgetMs);
}
private sealed record RecordedMeasurement(string Name, object Value, IReadOnlyList<KeyValuePair<string, object?>> Tags)
{
public bool HasTag(string key, string expectedValue) =>
Tags.Any(t => t.Key == key && string.Equals(t.Value?.ToString(), expectedValue, StringComparison.Ordinal));
}
}

View File

@@ -0,0 +1,163 @@
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
using StellaOps.Telemetry.Core.Triage;
namespace StellaOps.Telemetry.Core.Tests;
public sealed class TtfsIngestionServiceTests : IDisposable
{
private const string TriageMeterName = "StellaOps.Triage";
private readonly MeterListener _listener;
private readonly List<RecordedMeasurement> _measurements = [];
public TtfsIngestionServiceTests()
{
_listener = new MeterListener();
_listener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name == TriageMeterName)
{
listener.EnableMeasurementEvents(instrument);
}
};
_listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
{
_measurements.Add(new RecordedMeasurement(instrument.Name, measurement, tags.ToArray()));
});
_listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
{
_measurements.Add(new RecordedMeasurement(instrument.Name, measurement, tags.ToArray()));
});
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
_measurements.Add(new RecordedMeasurement(instrument.Name, measurement, tags.ToArray()));
});
_listener.Start();
}
public void Dispose() => _listener.Dispose();
[Fact]
public void EvidenceBitset_From_ComputesScoreAndFlags()
{
var bitset = EvidenceBitset.From(reachability: true, callstack: false, provenance: true, vex: true);
Assert.True(bitset.HasReachability);
Assert.False(bitset.HasCallstack);
Assert.True(bitset.HasProvenance);
Assert.True(bitset.HasVex);
Assert.Equal(3, bitset.CompletenessScore);
}
[Fact]
public void IngestEvent_Skeleton_RecordsDurationAndBudgetViolation()
{
using var loggerFactory = LoggerFactory.Create(_ => { });
var service = new TtfsIngestionService(loggerFactory.CreateLogger<TtfsIngestionService>());
service.IngestEvent(new TtfsEvent
{
EventType = TtfsEventType.Skeleton,
AlertId = "alert-1",
DurationMs = 250,
Timestamp = new DateTimeOffset(2025, 12, 15, 0, 0, 0, TimeSpan.Zero),
});
Assert.Contains(_measurements, m =>
m.Name == "stellaops_ttfs_skeleton_seconds" && m.Value is double v && Math.Abs(v - 0.25) < 0.000_001);
Assert.Contains(_measurements, m =>
m.Name == "stellaops_performance_budget_violations_total" &&
m.HasTag("phase", "skeleton"));
}
[Fact]
public void IngestEvent_FirstEvidence_RecordsDurationAndEvidenceType()
{
using var loggerFactory = LoggerFactory.Create(_ => { });
var service = new TtfsIngestionService(loggerFactory.CreateLogger<TtfsIngestionService>());
service.IngestEvent(new TtfsEvent
{
EventType = TtfsEventType.FirstEvidence,
AlertId = "alert-1",
EvidenceType = "reachability",
DurationMs = 600,
Timestamp = new DateTimeOffset(2025, 12, 15, 0, 0, 0, TimeSpan.Zero),
});
Assert.Contains(_measurements, m =>
m.Name == "stellaops_ttfs_first_evidence_seconds" &&
m.Value is double v &&
Math.Abs(v - 0.6) < 0.000_001 &&
m.HasTag("evidence_type", "reachability"));
Assert.Contains(_measurements, m =>
m.Name == "stellaops_performance_budget_violations_total" &&
m.HasTag("phase", "first_evidence"));
}
[Fact]
public void IngestEvent_FullEvidence_RecordsCompletenessAndEvidenceByType()
{
using var loggerFactory = LoggerFactory.Create(_ => { });
var service = new TtfsIngestionService(loggerFactory.CreateLogger<TtfsIngestionService>());
service.IngestEvent(new TtfsEvent
{
EventType = TtfsEventType.FullEvidence,
AlertId = "alert-1",
EvidenceBitset = EvidenceBitset.From(reachability: true, callstack: true, provenance: true, vex: true).Value,
CompletenessScore = 4,
DurationMs = 1400,
Timestamp = new DateTimeOffset(2025, 12, 15, 0, 0, 0, TimeSpan.Zero),
});
Assert.Contains(_measurements, m => m.Name == "stellaops_evidence_completeness_score" && m.Value is int v && v == 4);
var evidenceByType = _measurements
.Where(m => m.Name == "stellaops_evidence_available_total")
.Select(m => m.Tags.FirstOrDefault(t => t.Key == "evidence_type").Value?.ToString())
.Where(v => v is not null)
.ToList();
Assert.Contains("reachability", evidenceByType);
Assert.Contains("callstack", evidenceByType);
Assert.Contains("provenance", evidenceByType);
Assert.Contains("vex", evidenceByType);
}
[Fact]
public void IngestEvent_DecisionRecorded_RecordsDecisionMetricsAndClickBudgetViolation()
{
using var loggerFactory = LoggerFactory.Create(_ => { });
var service = new TtfsIngestionService(loggerFactory.CreateLogger<TtfsIngestionService>());
service.IngestEvent(new TtfsEvent
{
EventType = TtfsEventType.DecisionRecorded,
AlertId = "alert-1",
DecisionStatus = "accepted",
ClickCount = 7,
DurationMs = 2500,
Timestamp = new DateTimeOffset(2025, 12, 15, 0, 0, 0, TimeSpan.Zero),
});
Assert.Contains(_measurements, m => m.Name == "stellaops_clicks_to_closure" && m.Value is int v && v == 7);
Assert.Contains(_measurements, m =>
m.Name == "stellaops_triage_decision_duration_seconds" && m.Value is double v && Math.Abs(v - 2.5) < 0.000_001);
Assert.Contains(_measurements, m => m.Name == "stellaops_triage_decisions_total" && m.Value is long v && v == 1);
Assert.Contains(_measurements, m =>
m.Name == "stellaops_performance_budget_violations_total" &&
m.HasTag("phase", "clicks_to_closure"));
}
private sealed record RecordedMeasurement(string Name, object Value, IReadOnlyList<KeyValuePair<string, object?>> Tags)
{
public bool HasTag(string key, string expectedValue) =>
Tags.Any(t => t.Key == key && string.Equals(t.Value?.ToString(), expectedValue, StringComparison.Ordinal));
}
}

View File

@@ -109,6 +109,30 @@ public static class TelemetryServiceCollectionExtensions
return services;
}
/// <summary>
/// Registers Time-to-First-Signal (TTFS) metrics for measuring first-signal latency across UI/CLI/CI surfaces.
/// </summary>
/// <param name="services">Service collection to mutate.</param>
/// <param name="configureOptions">Optional options configuration including per-surface SLO targets.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddTimeToFirstSignalMetrics(
this IServiceCollection services,
Action<TimeToFirstSignalOptions>? configureOptions = null)
{
ArgumentNullException.ThrowIfNull(services);
services.AddOptions<TimeToFirstSignalOptions>()
.Configure(options => configureOptions?.Invoke(options));
services.TryAddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptions<TimeToFirstSignalOptions>>().Value;
return new TimeToFirstSignalMetrics(options);
});
return services;
}
/// <summary>
/// Registers incident mode services for toggling enhanced telemetry during incidents.
/// </summary>

View File

@@ -0,0 +1,360 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
namespace StellaOps.Telemetry.Core;
/// <summary>
/// Time-to-First-Signal (TTFS) metrics for measuring the speed and reliability
/// of the first meaningful signal across UI, CLI, and CI surfaces.
/// </summary>
public sealed class TimeToFirstSignalMetrics : IDisposable
{
/// <summary>
/// Default meter name for TTFS metrics.
/// </summary>
public const string MeterName = "StellaOps.TimeToFirstSignal";
private readonly Meter _meter;
private readonly TimeToFirstSignalOptions _options;
private bool _disposed;
private readonly Histogram<double> _ttfsLatencyHistogram;
private readonly Histogram<double> _ttfsCacheLatencyHistogram;
private readonly Histogram<double> _ttfsColdLatencyHistogram;
private readonly Counter<long> _signalTotalCounter;
private readonly Counter<long> _cacheHitCounter;
private readonly Counter<long> _cacheMissCounter;
private readonly Counter<long> _sloBreachCounter;
private readonly Counter<long> _errorCounter;
/// <summary>
/// Initializes a new instance of <see cref="TimeToFirstSignalMetrics"/>.
/// </summary>
public TimeToFirstSignalMetrics(TimeToFirstSignalOptions? options = null)
{
_options = options ?? new TimeToFirstSignalOptions();
_meter = new Meter(MeterName, _options.Version);
_ttfsLatencyHistogram = _meter.CreateHistogram<double>(
name: "ttfs_latency_seconds",
unit: "s",
description: "Time-to-first-signal latency in seconds.");
_ttfsCacheLatencyHistogram = _meter.CreateHistogram<double>(
name: "ttfs_cache_latency_seconds",
unit: "s",
description: "Time-to-first-signal cache lookup latency in seconds.");
_ttfsColdLatencyHistogram = _meter.CreateHistogram<double>(
name: "ttfs_cold_latency_seconds",
unit: "s",
description: "Time-to-first-signal cold-path computation latency in seconds.");
_signalTotalCounter = _meter.CreateCounter<long>(
name: "ttfs_signal_total",
description: "Total TTFS signals by surface and kind.");
_cacheHitCounter = _meter.CreateCounter<long>(
name: "ttfs_cache_hit_total",
description: "Total TTFS cache hits.");
_cacheMissCounter = _meter.CreateCounter<long>(
name: "ttfs_cache_miss_total",
description: "Total TTFS cache misses.");
_sloBreachCounter = _meter.CreateCounter<long>(
name: "ttfs_slo_breach_total",
description: "Total TTFS SLO breaches.");
_errorCounter = _meter.CreateCounter<long>(
name: "ttfs_error_total",
description: "Total TTFS errors by type.");
}
/// <summary>
/// Records a signal rendered event with TTFS latency.
/// </summary>
public void RecordSignalRendered(
double latencySeconds,
string surface,
bool cacheHit,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
string? tenantId = null)
{
var tags = CreateSignalTags(surface, cacheHit, signalSource, kind, phase, tenantId);
_ttfsLatencyHistogram.Record(latencySeconds, tags);
_signalTotalCounter.Add(1, tags);
if (cacheHit)
{
_cacheHitCounter.Add(1, tags);
}
else
{
_cacheMissCounter.Add(1, tags);
}
var sloTarget = GetSloTargetSeconds(surface, cacheHit);
if (latencySeconds > sloTarget)
{
_sloBreachCounter.Add(1, tags);
}
}
/// <summary>
/// Records a cache lookup latency and updates cache hit/miss counters.
/// </summary>
public void RecordCacheLookup(
double latencySeconds,
string surface,
bool cacheHit,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
string? tenantId = null)
{
var tags = CreateSignalTags(surface, cacheHit, signalSource, kind, phase, tenantId);
_ttfsCacheLatencyHistogram.Record(latencySeconds, tags);
if (cacheHit)
{
_cacheHitCounter.Add(1, tags);
}
else
{
_cacheMissCounter.Add(1, tags);
}
}
/// <summary>
/// Records cold-path computation latency.
/// </summary>
public void RecordColdPathComputation(
double latencySeconds,
string surface,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
string? tenantId = null)
{
var tags = CreateSignalTags(surface, cacheHit: false, signalSource, kind, phase, tenantId);
_ttfsColdLatencyHistogram.Record(latencySeconds, tags);
}
/// <summary>
/// Records an error event and increments error counters.
/// </summary>
public void RecordError(
string errorType,
string surface,
bool cacheHit,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
string? tenantId = null,
string? errorCode = null)
{
var tags = CreateSignalTags(surface, cacheHit, signalSource, kind, phase, tenantId);
tags.Add("error_type", (errorType ?? string.Empty).Trim());
if (!string.IsNullOrWhiteSpace(errorCode))
{
tags.Add("error_code", errorCode.Trim());
}
_errorCounter.Add(1, tags);
}
/// <summary>
/// Records an SLO breach directly.
/// </summary>
public void RecordSloBreachDirect(
string surface,
bool cacheHit,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
double actualSeconds,
double targetSeconds,
string? tenantId = null)
{
var tags = CreateSignalTags(surface, cacheHit, signalSource, kind, phase, tenantId);
tags.Add("actual_seconds", actualSeconds);
tags.Add("target_seconds", targetSeconds);
_sloBreachCounter.Add(1, tags);
}
/// <summary>
/// Starts a measurement scope for a TTFS signal.
/// </summary>
public TtfsSignalScope MeasureSignal(
string surface,
bool cacheHit,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
string? tenantId = null)
{
return new TtfsSignalScope(this, surface, cacheHit, signalSource, kind, phase, tenantId);
}
private TagList CreateSignalTags(
string surface,
bool cacheHit,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
string? tenantId)
{
var tags = new TagList
{
{ "surface", (surface ?? string.Empty).Trim().ToLowerInvariant() },
{ "cache_hit", cacheHit },
{ "kind", kind.ToString().ToLowerInvariant() },
{ "phase", phase.ToString().ToLowerInvariant() },
};
if (!string.IsNullOrWhiteSpace(signalSource))
{
tags.Add("signal_source", signalSource.Trim().ToLowerInvariant());
}
if (!string.IsNullOrWhiteSpace(tenantId))
{
tags.Add("tenant_id", tenantId.Trim());
}
return tags;
}
private double GetSloTargetSeconds(string surface, bool cacheHit)
{
var surfaceOptions = _options.GetSurfaceOptions(surface);
return cacheHit ? surfaceOptions.WarmPathP95Seconds : surfaceOptions.ColdPathP95Seconds;
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_meter.Dispose();
}
/// <summary>
/// Measurement scope for TTFS signals.
/// </summary>
public sealed class TtfsSignalScope : IDisposable
{
private readonly TimeToFirstSignalMetrics _metrics;
private readonly string _surface;
private readonly bool _cacheHit;
private readonly string? _signalSource;
private readonly TtfsSignalKind _kind;
private readonly TtfsPhase _phase;
private readonly string? _tenantId;
private readonly Stopwatch _stopwatch;
private bool _completed;
private string? _errorType;
private string? _errorCode;
internal TtfsSignalScope(
TimeToFirstSignalMetrics metrics,
string surface,
bool cacheHit,
string? signalSource,
TtfsSignalKind kind,
TtfsPhase phase,
string? tenantId)
{
_metrics = metrics;
_surface = surface;
_cacheHit = cacheHit;
_signalSource = signalSource;
_kind = kind;
_phase = phase;
_tenantId = tenantId;
_stopwatch = Stopwatch.StartNew();
}
/// <summary>
/// Marks the signal as successfully rendered.
/// </summary>
public void Complete()
{
_completed = true;
}
/// <summary>
/// Marks the signal as failed with an optional error type and error code.
/// </summary>
public void Fail(string? errorType = null, string? errorCode = null)
{
_errorType = errorType;
_errorCode = errorCode;
_completed = false;
}
/// <inheritdoc/>
public void Dispose()
{
_stopwatch.Stop();
if (_completed)
{
_metrics.RecordSignalRendered(
_stopwatch.Elapsed.TotalSeconds,
_surface,
_cacheHit,
_signalSource,
_kind,
_phase,
_tenantId);
return;
}
_metrics.RecordError(
errorType: string.IsNullOrWhiteSpace(_errorType) ? "exception" : _errorType,
surface: _surface,
cacheHit: _cacheHit,
signalSource: _signalSource,
kind: _kind,
phase: _phase,
tenantId: _tenantId,
errorCode: _errorCode);
}
}
}
/// <summary>
/// TTFS phases for sub-step classification.
/// </summary>
public enum TtfsPhase
{
Resolve,
Fetch,
Restore,
Analyze,
Policy,
Report,
Unknown,
}
/// <summary>
/// TTFS signal kind describing the first signal category.
/// </summary>
public enum TtfsSignalKind
{
Queued,
Started,
Phase,
Blocked,
Failed,
Succeeded,
Canceled,
Unavailable,
}

View File

@@ -0,0 +1,81 @@
namespace StellaOps.Telemetry.Core;
/// <summary>
/// Options for Time-to-First-Signal (TTFS) metrics including per-surface SLO targets.
/// </summary>
public sealed class TimeToFirstSignalOptions
{
/// <summary>
/// Version string for the meter.
/// </summary>
public string Version { get; set; } = "1.0.0";
/// <summary>
/// UI surface SLO targets and budgets.
/// </summary>
public TimeToFirstSignalSurfaceOptions Ui { get; set; } = new();
/// <summary>
/// CLI surface SLO targets and budgets.
/// </summary>
public TimeToFirstSignalSurfaceOptions Cli { get; set; } = new();
/// <summary>
/// CI surface SLO targets and budgets.
/// </summary>
public TimeToFirstSignalSurfaceOptions Ci { get; set; } = new();
/// <summary>
/// Frontend budget in milliseconds.
/// </summary>
public double FrontendBudgetMs { get; set; } = 150;
/// <summary>
/// Edge API budget in milliseconds.
/// </summary>
public double EdgeApiBudgetMs { get; set; } = 250;
/// <summary>
/// Core services budget in milliseconds.
/// </summary>
public double CoreServicesBudgetMs { get; set; } = 1500;
internal TimeToFirstSignalSurfaceOptions GetSurfaceOptions(string? surface) =>
surface?.Trim().ToLowerInvariant() switch
{
"cli" => Cli,
"ci" => Ci,
_ => Ui,
};
}
/// <summary>
/// Per-surface SLO targets for TTFS.
/// </summary>
public sealed class TimeToFirstSignalSurfaceOptions
{
/// <summary>
/// Primary SLO P50 target in seconds. Default: 2.0 seconds.
/// </summary>
public double SloP50Seconds { get; set; } = 2.0;
/// <summary>
/// Primary SLO P95 target in seconds. Default: 5.0 seconds.
/// </summary>
public double SloP95Seconds { get; set; } = 5.0;
/// <summary>
/// Warm path P50 target in seconds. Default: 0.7 seconds.
/// </summary>
public double WarmPathP50Seconds { get; set; } = 0.7;
/// <summary>
/// Warm path P95 target in seconds. Default: 2.5 seconds.
/// </summary>
public double WarmPathP95Seconds { get; set; } = 2.5;
/// <summary>
/// Cold path P95 target in seconds. Default: 4.0 seconds.
/// </summary>
public double ColdPathP95Seconds { get; set; } = 4.0;
}

View File

@@ -5,4 +5,5 @@ This file mirrors sprint work for the Telemetry Core module.
| Task ID | Sprint | Status | Notes |
| --- | --- | --- | --- |
| `DET-3401-005` | `docs/implplan/SPRINT_3401_0001_0001_determinism_scoring_foundations.md` | DONE (2025-12-14) | Added `ProofCoverageMetrics` (`System.Diagnostics.Metrics`) in `src/Telemetry/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core/ProofCoverageMetrics.cs` and tests in `src/Telemetry/StellaOps.Telemetry.Core/StellaOps.Telemetry.Core.Tests/ProofCoverageMetricsTests.cs`. |
| `TTFS-0338-001` | `docs/implplan/SPRINT_0338_0001_0001_ttfs_foundation.md` | DONE (2025-12-15) | Added `TimeToFirstSignalMetrics`/`TimeToFirstSignalOptions`, DI extension `AddTimeToFirstSignalMetrics`, and unit tests `TimeToFirstSignalMetricsTests`. |