audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -0,0 +1,25 @@
# Timeline WebService Module Charter
## Mission
- Provide timeline API endpoints and service orchestration.
## Responsibilities
- Implement HTTP endpoints and request validation.
- Orchestrate persistence and timeline workflows.
- Enforce deterministic ordering in responses.
## Required Reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- docs/modules/timeline-indexer/architecture.md
- docs/modules/timeline-indexer/README.md
## Working Agreement
- Deterministic ordering and invariant formatting.
- Use TimeProvider and IGuidGenerator where timestamps or IDs are created.
- Propagate CancellationToken for async operations.
## Testing Strategy
- API tests for validation and error handling.
- Determinism checks for response ordering.

View File

@@ -0,0 +1,157 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace StellaOps.Timeline.WebService.Authorization;
/// <summary>
/// Middleware for authorizing timeline access based on tenant/correlation ownership.
/// </summary>
public sealed class TimelineAuthorizationMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<TimelineAuthorizationMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="TimelineAuthorizationMiddleware"/> class.
/// </summary>
public TimelineAuthorizationMiddleware(
RequestDelegate next,
ILogger<TimelineAuthorizationMiddleware> logger)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Invokes the middleware.
/// </summary>
public async Task InvokeAsync(
HttpContext context,
ITimelineAuthorizationService authorizationService)
{
// Skip authorization for health endpoints
if (context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
// Extract correlation ID from route if present
var correlationId = ExtractCorrelationId(context);
if (!string.IsNullOrEmpty(correlationId))
{
var user = context.User;
var tenantId = user.FindFirstValue("tenant_id");
if (string.IsNullOrEmpty(tenantId))
{
_logger.LogWarning("No tenant_id claim found for timeline access");
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Unauthorized: Missing tenant claim");
return;
}
var hasAccess = await authorizationService.HasAccessToCorrelationAsync(
tenantId,
correlationId,
context.RequestAborted);
if (!hasAccess)
{
_logger.LogWarning(
"Tenant {TenantId} denied access to correlation {CorrelationId}",
tenantId,
correlationId);
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsync("Forbidden: No access to this correlation");
return;
}
// Log audit trail
_logger.LogInformation(
"Tenant {TenantId} accessed correlation {CorrelationId} via {Method} {Path}",
tenantId,
correlationId,
context.Request.Method,
context.Request.Path);
}
await _next(context);
}
private static string? ExtractCorrelationId(HttpContext context)
{
// Try to get from route values
if (context.Request.RouteValues.TryGetValue("correlationId", out var routeValue))
{
return routeValue?.ToString();
}
return null;
}
}
/// <summary>
/// Service for authorizing timeline access.
/// </summary>
public interface ITimelineAuthorizationService
{
/// <summary>
/// Checks if a tenant has access to a correlation ID.
/// </summary>
Task<bool> HasAccessToCorrelationAsync(
string tenantId,
string correlationId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Default implementation that allows all access.
/// Production should integrate with Authority service.
/// </summary>
public sealed class DefaultTimelineAuthorizationService : ITimelineAuthorizationService
{
private readonly ILogger<DefaultTimelineAuthorizationService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultTimelineAuthorizationService"/> class.
/// </summary>
public DefaultTimelineAuthorizationService(ILogger<DefaultTimelineAuthorizationService> logger)
{
_logger = logger;
}
/// <inheritdoc/>
public Task<bool> HasAccessToCorrelationAsync(
string tenantId,
string correlationId,
CancellationToken cancellationToken = default)
{
// Default: allow all access
// Production: check correlation -> tenant mapping in database or Authority
_logger.LogDebug(
"Authorization check: tenant={TenantId}, correlation={CorrelationId} -> allowed (default)",
tenantId,
correlationId);
return Task.FromResult(true);
}
}
/// <summary>
/// Extension methods for timeline authorization.
/// </summary>
public static class TimelineAuthorizationExtensions
{
/// <summary>
/// Adds timeline authorization middleware to the pipeline.
/// </summary>
public static IApplicationBuilder UseTimelineAuthorization(this IApplicationBuilder app)
{
return app.UseMiddleware<TimelineAuthorizationMiddleware>();
}
}

View File

@@ -0,0 +1,153 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using Microsoft.AspNetCore.Http.HttpResults;
using StellaOps.Timeline.Core;
namespace StellaOps.Timeline.WebService.Endpoints;
/// <summary>
/// Export endpoints for timeline bundles.
/// </summary>
public static class ExportEndpoints
{
/// <summary>
/// Maps export endpoints.
/// </summary>
public static void MapExportEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/timeline")
.WithTags("Export")
.WithOpenApi();
group.MapPost("/{correlationId}/export", ExportTimelineAsync)
.WithName("ExportTimeline")
.WithDescription("Export timeline events as NDJSON bundle with optional DSSE signing");
group.MapGet("/export/{exportId}", GetExportStatusAsync)
.WithName("GetExportStatus")
.WithDescription("Get the status of an export operation");
group.MapGet("/export/{exportId}/download", DownloadExportAsync)
.WithName("DownloadExport")
.WithDescription("Download the completed export bundle");
}
private static async Task<Results<Accepted<ExportInitiatedResponse>, BadRequest<string>>> ExportTimelineAsync(
string correlationId,
ExportRequest request,
ITimelineQueryService queryService,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(correlationId))
{
return TypedResults.BadRequest("Correlation ID is required");
}
// Validate the correlation exists
var result = await queryService.GetByCorrelationIdAsync(correlationId, new TimelineQueryOptions { Limit = 1 }, cancellationToken);
if (result.Events.Count == 0)
{
return TypedResults.BadRequest($"No events found for correlation ID: {correlationId}");
}
// TODO: Queue export job
var exportId = Guid.NewGuid().ToString("N")[..16];
return TypedResults.Accepted(
$"/api/v1/timeline/export/{exportId}",
new ExportInitiatedResponse
{
ExportId = exportId,
CorrelationId = correlationId,
Format = request.Format,
SignBundle = request.SignBundle,
Status = "INITIATED",
EstimatedEventCount = result.TotalCount
});
}
private static async Task<Results<Ok<ExportStatusResponse>, NotFound>> GetExportStatusAsync(
string exportId,
CancellationToken cancellationToken)
{
// TODO: Integrate with export state store
await Task.CompletedTask;
return TypedResults.Ok(new ExportStatusResponse
{
ExportId = exportId,
Status = "COMPLETED",
Format = "ndjson",
EventCount = 100,
FileSizeBytes = 45678,
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-1),
CompletedAt = DateTimeOffset.UtcNow.AddSeconds(-30)
});
}
private static async Task<Results<FileStreamHttpResult, NotFound>> DownloadExportAsync(
string exportId,
CancellationToken cancellationToken)
{
// TODO: Integrate with export storage
await Task.CompletedTask;
// Return stub for now - real implementation would stream from storage
var stubContent = """
{"event_id":"abc123","correlation_id":"scan-1","kind":"ENQUEUE"}
{"event_id":"def456","correlation_id":"scan-1","kind":"EXECUTE"}
""";
var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(stubContent));
return TypedResults.File(
stream,
contentType: "application/x-ndjson",
fileDownloadName: $"timeline-{exportId}.ndjson");
}
}
// DTOs
public sealed record ExportRequest
{
/// <summary>
/// Export format: "ndjson" or "json".
/// </summary>
public string Format { get; init; } = "ndjson";
/// <summary>
/// Whether to DSSE-sign the bundle.
/// </summary>
public bool SignBundle { get; init; } = false;
/// <summary>
/// Optional HLC range start.
/// </summary>
public string? FromHlc { get; init; }
/// <summary>
/// Optional HLC range end.
/// </summary>
public string? ToHlc { get; init; }
}
public sealed record ExportInitiatedResponse
{
public required string ExportId { get; init; }
public required string CorrelationId { get; init; }
public required string Format { get; init; }
public bool SignBundle { get; init; }
public required string Status { get; init; }
public long EstimatedEventCount { get; init; }
}
public sealed record ExportStatusResponse
{
public required string ExportId { get; init; }
public required string Status { get; init; }
public required string Format { get; init; }
public long EventCount { get; init; }
public long FileSizeBytes { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset? CompletedAt { get; init; }
public string? Error { get; init; }
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using Microsoft.Extensions.Diagnostics.HealthChecks;
using StellaOps.Eventing.Storage;
namespace StellaOps.Timeline.WebService.Endpoints;
/// <summary>
/// Health check endpoints.
/// </summary>
public static class HealthEndpoints
{
/// <summary>
/// Maps health check endpoints.
/// </summary>
public static void MapHealthEndpoints(this IEndpointRouteBuilder app)
{
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
}
}
/// <summary>
/// Health check for timeline service.
/// </summary>
public sealed class TimelineHealthCheck : IHealthCheck
{
private readonly ITimelineEventStore _eventStore;
/// <summary>
/// Initializes a new instance of the <see cref="TimelineHealthCheck"/> class.
/// </summary>
public TimelineHealthCheck(ITimelineEventStore eventStore)
{
_eventStore = eventStore;
}
/// <inheritdoc/>
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
// Simple check - try to count events for a nonexistent correlation
// This validates database connectivity
await _eventStore.CountByCorrelationIdAsync("__health_check__", cancellationToken);
return HealthCheckResult.Healthy("Timeline service is healthy");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Timeline service is unhealthy", ex);
}
}
}

View File

@@ -0,0 +1,129 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using Microsoft.AspNetCore.Http.HttpResults;
namespace StellaOps.Timeline.WebService.Endpoints;
/// <summary>
/// Replay endpoints for deterministic replay of event sequences.
/// </summary>
public static class ReplayEndpoints
{
/// <summary>
/// Maps replay endpoints.
/// </summary>
public static void MapReplayEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/timeline")
.WithTags("Replay")
.WithOpenApi();
group.MapPost("/{correlationId}/replay", InitiateReplayAsync)
.WithName("InitiateReplay")
.WithDescription("Initiate deterministic replay for a correlation ID");
group.MapGet("/replay/{replayId}", GetReplayStatusAsync)
.WithName("GetReplayStatus")
.WithDescription("Get the status of a replay operation");
group.MapPost("/replay/{replayId}/cancel", CancelReplayAsync)
.WithName("CancelReplay")
.WithDescription("Cancel an in-progress replay operation");
}
private static async Task<Results<Accepted<ReplayInitiatedResponse>, BadRequest<string>>> InitiateReplayAsync(
string correlationId,
ReplayRequest request,
CancellationToken cancellationToken)
{
// Validate request
if (string.IsNullOrWhiteSpace(correlationId))
{
return TypedResults.BadRequest("Correlation ID is required");
}
// TODO: Integrate with StellaOps.Replay.Core
// For now, return a stub response
var replayId = Guid.NewGuid().ToString("N")[..16];
await Task.CompletedTask; // Placeholder for actual implementation
return TypedResults.Accepted(
$"/api/v1/timeline/replay/{replayId}",
new ReplayInitiatedResponse
{
ReplayId = replayId,
CorrelationId = correlationId,
Mode = request.Mode,
Status = "INITIATED",
EstimatedDurationMs = 5000
});
}
private static async Task<Results<Ok<ReplayStatusResponse>, NotFound>> GetReplayStatusAsync(
string replayId,
CancellationToken cancellationToken)
{
// TODO: Integrate with replay state store
await Task.CompletedTask;
return TypedResults.Ok(new ReplayStatusResponse
{
ReplayId = replayId,
Status = "IN_PROGRESS",
Progress = 0.5,
EventsProcessed = 50,
TotalEvents = 100,
StartedAt = DateTimeOffset.UtcNow.AddSeconds(-5)
});
}
private static async Task<Results<Ok, NotFound>> CancelReplayAsync(
string replayId,
CancellationToken cancellationToken)
{
// TODO: Integrate with replay cancellation
await Task.CompletedTask;
return TypedResults.Ok();
}
}
// DTOs
public sealed record ReplayRequest
{
/// <summary>
/// Replay mode: "dry-run" or "verify".
/// </summary>
public string Mode { get; init; } = "dry-run";
/// <summary>
/// HLC to replay from (optional).
/// </summary>
public string? FromHlc { get; init; }
/// <summary>
/// HLC to replay to (optional).
/// </summary>
public string? ToHlc { get; init; }
}
public sealed record ReplayInitiatedResponse
{
public required string ReplayId { get; init; }
public required string CorrelationId { get; init; }
public required string Mode { get; init; }
public required string Status { get; init; }
public long EstimatedDurationMs { get; init; }
}
public sealed record ReplayStatusResponse
{
public required string ReplayId { get; init; }
public required string Status { get; init; }
public double Progress { get; init; }
public int EventsProcessed { get; init; }
public int TotalEvents { get; init; }
public DateTimeOffset StartedAt { get; init; }
public DateTimeOffset? CompletedAt { get; init; }
public string? Error { get; init; }
}

View File

@@ -0,0 +1,153 @@
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
using Microsoft.AspNetCore.Http.HttpResults;
using StellaOps.HybridLogicalClock;
using StellaOps.Timeline.Core;
namespace StellaOps.Timeline.WebService.Endpoints;
/// <summary>
/// Timeline query endpoints.
/// </summary>
public static class TimelineEndpoints
{
/// <summary>
/// Maps timeline query endpoints.
/// </summary>
public static void MapTimelineEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/timeline")
.WithTags("Timeline")
.WithOpenApi();
group.MapGet("/{correlationId}", GetTimelineAsync)
.WithName("GetTimeline")
.WithDescription("Get events for a correlation ID, ordered by HLC timestamp");
group.MapGet("/{correlationId}/critical-path", GetCriticalPathAsync)
.WithName("GetCriticalPath")
.WithDescription("Get the critical path (longest latency stages) for a correlation");
}
private static async Task<Results<Ok<TimelineResponse>, NotFound>> GetTimelineAsync(
string correlationId,
ITimelineQueryService queryService,
int? limit,
int? offset,
string? fromHlc,
string? toHlc,
string? services,
string? kinds,
CancellationToken cancellationToken)
{
var options = new TimelineQueryOptions
{
Limit = limit ?? 100,
Offset = offset ?? 0,
FromHlc = !string.IsNullOrEmpty(fromHlc) ? HlcTimestamp.Parse(fromHlc) : null,
ToHlc = !string.IsNullOrEmpty(toHlc) ? HlcTimestamp.Parse(toHlc) : null,
Services = !string.IsNullOrEmpty(services)
? services.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
: null,
Kinds = !string.IsNullOrEmpty(kinds)
? kinds.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
: null
};
var result = await queryService.GetByCorrelationIdAsync(correlationId, options, cancellationToken);
if (result.Events.Count == 0)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(new TimelineResponse
{
CorrelationId = correlationId,
Events = result.Events.Select(e => new TimelineEventDto
{
EventId = e.EventId,
THlc = e.THlc.ToSortableString(),
TsWall = e.TsWall,
Service = e.Service,
Kind = e.Kind,
Payload = e.Payload,
EngineVersion = new EngineVersionDto(
e.EngineVersion.EngineName,
e.EngineVersion.Version,
e.EngineVersion.SourceDigest)
}).ToList(),
TotalCount = result.TotalCount,
HasMore = result.HasMore,
NextCursor = result.NextCursor
});
}
private static async Task<Results<Ok<CriticalPathResponse>, NotFound>> GetCriticalPathAsync(
string correlationId,
ITimelineQueryService queryService,
CancellationToken cancellationToken)
{
var result = await queryService.GetCriticalPathAsync(correlationId, cancellationToken);
if (result.Stages.Count == 0)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(new CriticalPathResponse
{
CorrelationId = result.CorrelationId,
TotalDurationMs = result.TotalDuration.TotalMilliseconds,
Stages = result.Stages.Select(s => new CriticalPathStageDto
{
Stage = s.Stage,
Service = s.Service,
DurationMs = s.Duration.TotalMilliseconds,
Percentage = s.Percentage,
FromHlc = s.FromHlc.ToSortableString(),
ToHlc = s.ToHlc.ToSortableString()
}).ToList()
});
}
}
// DTOs
public sealed record TimelineResponse
{
public required string CorrelationId { get; init; }
public required IReadOnlyList<TimelineEventDto> Events { get; init; }
public long TotalCount { get; init; }
public bool HasMore { get; init; }
public string? NextCursor { get; init; }
}
public sealed record TimelineEventDto
{
public required string EventId { get; init; }
public required string THlc { get; init; }
public required DateTimeOffset TsWall { get; init; }
public required string Service { get; init; }
public required string Kind { get; init; }
public required string Payload { get; init; }
public required EngineVersionDto EngineVersion { get; init; }
}
public sealed record EngineVersionDto(string EngineName, string Version, string SourceDigest);
public sealed record CriticalPathResponse
{
public required string CorrelationId { get; init; }
public double TotalDurationMs { get; init; }
public required IReadOnlyList<CriticalPathStageDto> Stages { get; init; }
}
public sealed record CriticalPathStageDto
{
public required string Stage { get; init; }
public required string Service { get; init; }
public double DurationMs { get; init; }
public double Percentage { get; init; }
public required string FromHlc { get; init; }
public required string ToHlc { get; init; }
}

View File

@@ -0,0 +1,42 @@
using StellaOps.Eventing;
using StellaOps.Timeline.Core;
using StellaOps.Timeline.WebService.Endpoints;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddStellaOpsEventing(builder.Configuration);
builder.Services.AddTimelineServices(builder.Configuration);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new()
{
Title = "StellaOps Timeline API",
Version = "v1",
Description = "Unified event timeline API for querying, replaying, and exporting HLC-ordered events"
});
});
builder.Services.AddHealthChecks()
.AddCheck<TimelineHealthCheck>("timeline");
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Map endpoints
app.MapTimelineEndpoints();
app.MapReplayEndpoints();
app.MapExportEndpoints();
app.MapHealthEndpoints();
app.Run();

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<RootNamespace>StellaOps.Timeline.WebService</RootNamespace>
<Description>StellaOps Timeline Service - Unified event timeline API</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.Timeline.Core\StellaOps.Timeline.Core.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Eventing\StellaOps.Eventing.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.HybridLogicalClock\StellaOps.HybridLogicalClock.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"StellaOps": "Debug"
}
},
"AllowedHosts": "*",
"Eventing": {
"ServiceName": "Timeline",
"UseInMemoryStore": false,
"ConnectionString": "",
"SignEvents": false,
"EnableOutbox": false
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://+:8080"
}
}
}
}

View File

@@ -0,0 +1,536 @@
openapi: 3.1.0
info:
title: StellaOps Timeline API
version: 1.0.0
description: |
Unified event timeline API for querying, replaying, and exporting HLC-ordered events
across all StellaOps services.
license:
name: AGPL-3.0-or-later
url: https://www.gnu.org/licenses/agpl-3.0.html
contact:
name: StellaOps
url: https://stellaops.io
servers:
- url: /api/v1
description: Timeline API v1
tags:
- name: Timeline
description: Query timeline events
- name: Replay
description: Deterministic replay operations
- name: Export
description: Export timeline bundles
paths:
/timeline/{correlationId}:
get:
summary: Get timeline events
description: Returns events for a correlation ID, ordered by HLC timestamp
operationId: getTimeline
tags:
- Timeline
parameters:
- name: correlationId
in: path
required: true
schema:
type: string
description: The correlation ID to query
- name: limit
in: query
schema:
type: integer
default: 100
minimum: 1
maximum: 1000
description: Maximum number of events to return
- name: offset
in: query
schema:
type: integer
default: 0
minimum: 0
description: Number of events to skip
- name: services
in: query
schema:
type: string
description: Comma-separated list of services to filter by
- name: kinds
in: query
schema:
type: string
description: Comma-separated list of event kinds to filter by
- name: fromHlc
in: query
schema:
type: string
description: Start of HLC range (inclusive)
- name: toHlc
in: query
schema:
type: string
description: End of HLC range (inclusive)
responses:
'200':
description: Timeline events
content:
application/json:
schema:
$ref: '#/components/schemas/TimelineResponse'
'404':
description: No events found for correlation ID
/timeline/{correlationId}/critical-path:
get:
summary: Get critical path
description: Returns the critical path (longest latency stages) for a correlation
operationId: getCriticalPath
tags:
- Timeline
parameters:
- name: correlationId
in: path
required: true
schema:
type: string
description: The correlation ID to analyze
responses:
'200':
description: Critical path analysis
content:
application/json:
schema:
$ref: '#/components/schemas/CriticalPathResponse'
'404':
description: No events found for correlation ID
/timeline/{correlationId}/replay:
post:
summary: Initiate replay
description: Initiate deterministic replay for a correlation ID
operationId: initiateReplay
tags:
- Replay
parameters:
- name: correlationId
in: path
required: true
schema:
type: string
description: The correlation ID to replay
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReplayRequest'
responses:
'202':
description: Replay initiated
content:
application/json:
schema:
$ref: '#/components/schemas/ReplayInitiatedResponse'
'400':
description: Invalid request
/timeline/replay/{replayId}:
get:
summary: Get replay status
description: Get the status of a replay operation
operationId: getReplayStatus
tags:
- Replay
parameters:
- name: replayId
in: path
required: true
schema:
type: string
description: The replay operation ID
responses:
'200':
description: Replay status
content:
application/json:
schema:
$ref: '#/components/schemas/ReplayStatusResponse'
'404':
description: Replay not found
/timeline/{correlationId}/export:
post:
summary: Export timeline
description: Export timeline events as NDJSON bundle with optional DSSE signing
operationId: exportTimeline
tags:
- Export
parameters:
- name: correlationId
in: path
required: true
schema:
type: string
description: The correlation ID to export
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExportRequest'
responses:
'202':
description: Export initiated
content:
application/json:
schema:
$ref: '#/components/schemas/ExportInitiatedResponse'
'400':
description: Invalid request or no events found
/timeline/export/{exportId}:
get:
summary: Get export status
description: Get the status of an export operation
operationId: getExportStatus
tags:
- Export
parameters:
- name: exportId
in: path
required: true
schema:
type: string
description: The export operation ID
responses:
'200':
description: Export status
content:
application/json:
schema:
$ref: '#/components/schemas/ExportStatusResponse'
'404':
description: Export not found
/timeline/export/{exportId}/download:
get:
summary: Download export
description: Download the completed export bundle
operationId: downloadExport
tags:
- Export
parameters:
- name: exportId
in: path
required: true
schema:
type: string
description: The export operation ID
responses:
'200':
description: Export bundle
content:
application/x-ndjson:
schema:
type: string
format: binary
'404':
description: Export not found or not completed
components:
schemas:
TimelineResponse:
type: object
required:
- correlationId
- events
properties:
correlationId:
type: string
description: The correlation ID queried
events:
type: array
items:
$ref: '#/components/schemas/TimelineEvent'
totalCount:
type: integer
format: int64
description: Total number of events for this correlation
hasMore:
type: boolean
description: Whether there are more results
nextCursor:
type: string
description: Cursor for next page (HLC of last event)
TimelineEvent:
type: object
required:
- eventId
- tHlc
- tsWall
- service
- kind
- payload
- engineVersion
properties:
eventId:
type: string
description: Deterministic event ID (SHA-256 hash)
tHlc:
type: string
description: HLC timestamp in sortable format
tsWall:
type: string
format: date-time
description: Wall-clock time (ISO 8601)
service:
type: string
description: Service that emitted the event
kind:
type: string
description: Event kind (ENQUEUE, EXECUTE, etc.)
payload:
type: string
description: RFC 8785 canonicalized JSON payload
engineVersion:
$ref: '#/components/schemas/EngineVersion'
EngineVersion:
type: object
required:
- engineName
- version
- sourceDigest
properties:
engineName:
type: string
description: Engine/service name
version:
type: string
description: Engine version
sourceDigest:
type: string
description: Source/assembly digest
CriticalPathResponse:
type: object
required:
- correlationId
- stages
properties:
correlationId:
type: string
description: The correlation ID analyzed
totalDurationMs:
type: number
format: double
description: Total duration in milliseconds
stages:
type: array
items:
$ref: '#/components/schemas/CriticalPathStage'
CriticalPathStage:
type: object
required:
- stage
- service
- fromHlc
- toHlc
properties:
stage:
type: string
description: Stage label (e.g., "ENQUEUE -> EXECUTE")
service:
type: string
description: Service where stage occurred
durationMs:
type: number
format: double
description: Duration in milliseconds
percentage:
type: number
format: double
description: Percentage of total duration
fromHlc:
type: string
description: HLC at start of stage
toHlc:
type: string
description: HLC at end of stage
ReplayRequest:
type: object
properties:
mode:
type: string
enum:
- dry-run
- verify
default: dry-run
description: Replay mode
fromHlc:
type: string
description: HLC to replay from (optional)
toHlc:
type: string
description: HLC to replay to (optional)
ReplayInitiatedResponse:
type: object
required:
- replayId
- correlationId
- mode
- status
properties:
replayId:
type: string
description: Unique replay operation ID
correlationId:
type: string
description: The correlation ID being replayed
mode:
type: string
description: Replay mode
status:
type: string
description: Initial status (INITIATED)
estimatedDurationMs:
type: integer
format: int64
description: Estimated duration in milliseconds
ReplayStatusResponse:
type: object
required:
- replayId
- status
properties:
replayId:
type: string
description: Unique replay operation ID
status:
type: string
enum:
- INITIATED
- IN_PROGRESS
- COMPLETED
- FAILED
description: Current status
progress:
type: number
format: double
description: Progress (0.0 to 1.0)
eventsProcessed:
type: integer
description: Number of events processed
totalEvents:
type: integer
description: Total number of events
startedAt:
type: string
format: date-time
description: Start time
completedAt:
type: string
format: date-time
description: Completion time (if completed)
error:
type: string
description: Error message (if failed)
ExportRequest:
type: object
properties:
format:
type: string
enum:
- ndjson
- json
default: ndjson
description: Export format
signBundle:
type: boolean
default: false
description: Whether to DSSE-sign the bundle
fromHlc:
type: string
description: HLC range start (optional)
toHlc:
type: string
description: HLC range end (optional)
ExportInitiatedResponse:
type: object
required:
- exportId
- correlationId
- format
- status
properties:
exportId:
type: string
description: Unique export operation ID
correlationId:
type: string
description: The correlation ID being exported
format:
type: string
description: Export format
signBundle:
type: boolean
description: Whether bundle is signed
status:
type: string
description: Initial status (INITIATED)
estimatedEventCount:
type: integer
format: int64
description: Estimated number of events
ExportStatusResponse:
type: object
required:
- exportId
- status
- format
properties:
exportId:
type: string
description: Unique export operation ID
status:
type: string
enum:
- INITIATED
- IN_PROGRESS
- COMPLETED
- FAILED
description: Current status
format:
type: string
description: Export format
eventCount:
type: integer
format: int64
description: Number of events exported
fileSizeBytes:
type: integer
format: int64
description: Size of export file
createdAt:
type: string
format: date-time
description: Creation time
completedAt:
type: string
format: date-time
description: Completion time (if completed)
error:
type: string
description: Error message (if failed)