Files
git.stella-ops.org/src/Signals/StellaOps.Signals/CompatibilityApiV1Endpoints.cs
2026-03-10 01:37:24 +02:00

216 lines
7.6 KiB
C#

using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Signals.Hosting;
using StellaOps.Signals.Options;
using StellaOps.Signals.Routing;
namespace StellaOps.Signals;
internal static class CompatibilityApiV1Endpoints
{
private static readonly CompatibilitySignalRecord[] Signals =
[
new(
"sig-001",
"ci_build",
"gitea",
"completed",
new Dictionary<string, object?>
{
["host"] = "build-agent-01",
["runtime"] = "ebpf",
["probeStatus"] = "healthy",
["latencyMs"] = 41
},
"corr-001",
"sha256:001",
new[] { "update-runtime-health" },
"2026-03-09T08:10:00Z",
"2026-03-09T08:10:02Z",
null),
new(
"sig-002",
"ci_deploy",
"internal",
"processing",
new Dictionary<string, object?>
{
["host"] = "deploy-stage-02",
["runtime"] = "etw",
["probeStatus"] = "degraded",
["latencyMs"] = 84
},
"corr-002",
"sha256:002",
new[] { "refresh-rollout-state" },
"2026-03-09T08:12:00Z",
null,
null),
new(
"sig-003",
"registry_push",
"harbor",
"failed",
new Dictionary<string, object?>
{
["host"] = "registry-sync-01",
["runtime"] = "dyld",
["probeStatus"] = "failed",
["latencyMs"] = 132
},
"corr-003",
"sha256:003",
new[] { "retry-mirror" },
"2026-03-09T08:13:00Z",
"2026-03-09T08:13:05Z",
"Registry callback timed out."),
new(
"sig-004",
"scan_complete",
"internal",
"completed",
new Dictionary<string, object?>
{
["host"] = "scanner-03",
["runtime"] = "ebpf",
["probeStatus"] = "healthy",
["latencyMs"] = 58
},
"corr-004",
"sha256:004",
new[] { "refresh-risk-snapshot" },
"2026-03-09T08:16:00Z",
"2026-03-09T08:16:01Z",
null),
new(
"sig-005",
"policy_eval",
"internal",
"received",
new Dictionary<string, object?>
{
["host"] = "policy-runner-01",
["runtime"] = "unknown",
["probeStatus"] = "degraded",
["latencyMs"] = 73
},
"corr-005",
"sha256:005",
new[] { "await-policy-evaluation" },
"2026-03-09T08:18:00Z",
null,
null)
];
public static IEndpointRouteBuilder MapSignalsCompatibilityEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/signals").RequireTenant();
group.MapGet("", (
HttpContext context,
SignalsOptions options,
SignalsSealedModeMonitor sealedModeMonitor,
string? type,
string? status,
string? provider,
int? limit,
string? cursor) =>
{
if (!Program.TryAuthorizeAny(context, [SignalsPolicies.Read, StellaOpsScopes.OrchRead], options.Authority.AllowAnonymousFallback, out var authFailure))
{
return authFailure ?? Results.Unauthorized();
}
if (!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return sealedFailure ?? Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
var filtered = ApplyFilters(type, status, provider);
var offset = ParseCursor(cursor);
var pageSize = Math.Clamp(limit ?? 50, 1, 200);
var items = filtered.Skip(offset).Take(pageSize).ToArray();
var nextCursor = offset + pageSize < filtered.Length ? (offset + pageSize).ToString() : null;
return Results.Ok(new
{
items,
total = filtered.Length,
cursor = nextCursor
});
});
group.MapGet("/stats", (
HttpContext context,
SignalsOptions options,
SignalsSealedModeMonitor sealedModeMonitor) =>
{
if (!Program.TryAuthorizeAny(context, [SignalsPolicies.Read, StellaOpsScopes.OrchRead], options.Authority.AllowAnonymousFallback, out var authFailure))
{
return authFailure ?? Results.Unauthorized();
}
if (!Program.TryEnsureSealedMode(sealedModeMonitor, out var sealedFailure))
{
return sealedFailure ?? Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
return Results.Ok(BuildStats(Signals));
});
return app;
}
private static CompatibilitySignalRecord[] ApplyFilters(string? type, string? status, string? provider) =>
Signals
.Where(signal => string.IsNullOrWhiteSpace(type) || string.Equals(signal.Type, type, StringComparison.OrdinalIgnoreCase))
.Where(signal => string.IsNullOrWhiteSpace(status) || string.Equals(signal.Status, status, StringComparison.OrdinalIgnoreCase))
.Where(signal => string.IsNullOrWhiteSpace(provider) || string.Equals(signal.Provider, provider, StringComparison.OrdinalIgnoreCase))
.ToArray();
private static int ParseCursor(string? cursor) =>
int.TryParse(cursor, out var offset) && offset >= 0 ? offset : 0;
private static object BuildStats(IReadOnlyCollection<CompatibilitySignalRecord> signals)
{
var byType = signals
.GroupBy(signal => signal.Type, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
var byStatus = signals
.GroupBy(signal => signal.Status, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
var byProvider = signals
.GroupBy(signal => signal.Provider, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
var successful = signals.Count(signal => string.Equals(signal.Status, "completed", StringComparison.OrdinalIgnoreCase));
var latencySamples = signals
.Select(signal => signal.Payload.TryGetValue("latencyMs", out var value) ? value : null)
.OfType<int>()
.ToArray();
return new
{
total = signals.Count,
byType,
byStatus,
byProvider,
lastHourCount = signals.Count,
successRate = signals.Count == 0 ? 100 : Math.Round((successful / (double)signals.Count) * 100, 2),
avgProcessingMs = latencySamples.Length == 0 ? 0 : Math.Round(latencySamples.Average(), 2)
};
}
internal sealed record CompatibilitySignalRecord(
string Id,
string Type,
string Provider,
string Status,
IReadOnlyDictionary<string, object?> Payload,
string? CorrelationId,
string? ArtifactRef,
IReadOnlyCollection<string> TriggeredActions,
string ReceivedAt,
string? ProcessedAt,
string? Error);
}