up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,194 +1,194 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Fallback;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// REST API endpoints for fallback handler operations.
|
||||
/// </summary>
|
||||
public static class FallbackEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps fallback API endpoints.
|
||||
/// </summary>
|
||||
public static RouteGroupBuilder MapFallbackEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/v2/fallback")
|
||||
.WithTags("Fallback")
|
||||
.WithOpenApi();
|
||||
|
||||
// Get fallback statistics
|
||||
group.MapGet("/statistics", async (
|
||||
int? windowHours,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
var window = windowHours.HasValue ? TimeSpan.FromHours(windowHours.Value) : (TimeSpan?)null;
|
||||
|
||||
var stats = await fallbackHandler.GetStatisticsAsync(tenantId, window, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
stats.TenantId,
|
||||
window = stats.Window.ToString(),
|
||||
stats.TotalDeliveries,
|
||||
stats.PrimarySuccesses,
|
||||
stats.FallbackAttempts,
|
||||
stats.FallbackSuccesses,
|
||||
stats.ExhaustedDeliveries,
|
||||
successRate = $"{stats.SuccessRate:P1}",
|
||||
fallbackUtilizationRate = $"{stats.FallbackUtilizationRate:P1}",
|
||||
failuresByChannel = stats.FailuresByChannel.ToDictionary(
|
||||
kvp => kvp.Key.ToString(),
|
||||
kvp => kvp.Value)
|
||||
});
|
||||
})
|
||||
.WithName("GetFallbackStatistics")
|
||||
.WithSummary("Gets fallback handling statistics for a tenant");
|
||||
|
||||
// Get fallback chain for a channel
|
||||
group.MapGet("/chains/{channelType}", async (
|
||||
NotifyChannelType channelType,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
|
||||
var chain = await fallbackHandler.GetFallbackChainAsync(tenantId, channelType, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
tenantId,
|
||||
primaryChannel = channelType.ToString(),
|
||||
fallbackChain = chain.Select(c => c.ToString()).ToList(),
|
||||
chainLength = chain.Count
|
||||
});
|
||||
})
|
||||
.WithName("GetFallbackChain")
|
||||
.WithSummary("Gets the fallback chain for a channel type");
|
||||
|
||||
// Set fallback chain for a channel
|
||||
group.MapPut("/chains/{channelType}", async (
|
||||
NotifyChannelType channelType,
|
||||
SetFallbackChainRequest request,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
var actor = context.Request.Headers["X-Actor"].FirstOrDefault() ?? "system";
|
||||
|
||||
var chain = request.FallbackChain
|
||||
.Select(s => Enum.TryParse<NotifyChannelType>(s, out var t) ? t : (NotifyChannelType?)null)
|
||||
.Where(t => t.HasValue)
|
||||
.Select(t => t!.Value)
|
||||
.ToList();
|
||||
|
||||
await fallbackHandler.SetFallbackChainAsync(tenantId, channelType, chain, actor, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
message = "Fallback chain updated successfully",
|
||||
primaryChannel = channelType.ToString(),
|
||||
fallbackChain = chain.Select(c => c.ToString()).ToList()
|
||||
});
|
||||
})
|
||||
.WithName("SetFallbackChain")
|
||||
.WithSummary("Sets a custom fallback chain for a channel type");
|
||||
|
||||
// Test fallback resolution
|
||||
group.MapPost("/test", async (
|
||||
TestFallbackRequest request,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
|
||||
if (!Enum.TryParse<NotifyChannelType>(request.FailedChannelType, out var channelType))
|
||||
{
|
||||
return Results.BadRequest(new { error = $"Invalid channel type: {request.FailedChannelType}" });
|
||||
}
|
||||
|
||||
var deliveryId = $"test-{Guid.NewGuid():N}"[..20];
|
||||
|
||||
// Simulate failure recording
|
||||
await fallbackHandler.RecordFailureAsync(
|
||||
tenantId, deliveryId, channelType, "Test failure", cancellationToken);
|
||||
|
||||
// Get fallback result
|
||||
var result = await fallbackHandler.GetFallbackAsync(
|
||||
tenantId, channelType, deliveryId, cancellationToken);
|
||||
|
||||
// Clean up test state
|
||||
await fallbackHandler.ClearDeliveryStateAsync(tenantId, deliveryId, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
testDeliveryId = deliveryId,
|
||||
result.HasFallback,
|
||||
nextChannelType = result.NextChannelType?.ToString(),
|
||||
result.AttemptNumber,
|
||||
result.TotalChannels,
|
||||
result.IsExhausted,
|
||||
result.ExhaustionReason,
|
||||
failedChannels = result.FailedChannels.Select(f => new
|
||||
{
|
||||
channelType = f.ChannelType.ToString(),
|
||||
f.Reason,
|
||||
f.FailedAt,
|
||||
f.AttemptNumber
|
||||
}).ToList()
|
||||
});
|
||||
})
|
||||
.WithName("TestFallback")
|
||||
.WithSummary("Tests fallback resolution without affecting real deliveries");
|
||||
|
||||
// Clear delivery state
|
||||
group.MapDelete("/deliveries/{deliveryId}", async (
|
||||
string deliveryId,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
|
||||
await fallbackHandler.ClearDeliveryStateAsync(tenantId, deliveryId, cancellationToken);
|
||||
|
||||
return Results.Ok(new { message = $"Delivery state for '{deliveryId}' cleared" });
|
||||
})
|
||||
.WithName("ClearDeliveryFallbackState")
|
||||
.WithSummary("Clears fallback state for a specific delivery");
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to set a custom fallback chain.
|
||||
/// </summary>
|
||||
public sealed record SetFallbackChainRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Ordered list of fallback channel types.
|
||||
/// </summary>
|
||||
public required List<string> FallbackChain { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to test fallback resolution.
|
||||
/// </summary>
|
||||
public sealed record TestFallbackRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The channel type that "failed".
|
||||
/// </summary>
|
||||
public required string FailedChannelType { get; init; }
|
||||
}
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Fallback;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// REST API endpoints for fallback handler operations.
|
||||
/// </summary>
|
||||
public static class FallbackEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps fallback API endpoints.
|
||||
/// </summary>
|
||||
public static RouteGroupBuilder MapFallbackEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/v2/fallback")
|
||||
.WithTags("Fallback")
|
||||
.WithOpenApi();
|
||||
|
||||
// Get fallback statistics
|
||||
group.MapGet("/statistics", async (
|
||||
int? windowHours,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
var window = windowHours.HasValue ? TimeSpan.FromHours(windowHours.Value) : (TimeSpan?)null;
|
||||
|
||||
var stats = await fallbackHandler.GetStatisticsAsync(tenantId, window, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
stats.TenantId,
|
||||
window = stats.Window.ToString(),
|
||||
stats.TotalDeliveries,
|
||||
stats.PrimarySuccesses,
|
||||
stats.FallbackAttempts,
|
||||
stats.FallbackSuccesses,
|
||||
stats.ExhaustedDeliveries,
|
||||
successRate = $"{stats.SuccessRate:P1}",
|
||||
fallbackUtilizationRate = $"{stats.FallbackUtilizationRate:P1}",
|
||||
failuresByChannel = stats.FailuresByChannel.ToDictionary(
|
||||
kvp => kvp.Key.ToString(),
|
||||
kvp => kvp.Value)
|
||||
});
|
||||
})
|
||||
.WithName("GetFallbackStatistics")
|
||||
.WithSummary("Gets fallback handling statistics for a tenant");
|
||||
|
||||
// Get fallback chain for a channel
|
||||
group.MapGet("/chains/{channelType}", async (
|
||||
NotifyChannelType channelType,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
|
||||
var chain = await fallbackHandler.GetFallbackChainAsync(tenantId, channelType, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
tenantId,
|
||||
primaryChannel = channelType.ToString(),
|
||||
fallbackChain = chain.Select(c => c.ToString()).ToList(),
|
||||
chainLength = chain.Count
|
||||
});
|
||||
})
|
||||
.WithName("GetFallbackChain")
|
||||
.WithSummary("Gets the fallback chain for a channel type");
|
||||
|
||||
// Set fallback chain for a channel
|
||||
group.MapPut("/chains/{channelType}", async (
|
||||
NotifyChannelType channelType,
|
||||
SetFallbackChainRequest request,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
var actor = context.Request.Headers["X-Actor"].FirstOrDefault() ?? "system";
|
||||
|
||||
var chain = request.FallbackChain
|
||||
.Select(s => Enum.TryParse<NotifyChannelType>(s, out var t) ? t : (NotifyChannelType?)null)
|
||||
.Where(t => t.HasValue)
|
||||
.Select(t => t!.Value)
|
||||
.ToList();
|
||||
|
||||
await fallbackHandler.SetFallbackChainAsync(tenantId, channelType, chain, actor, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
message = "Fallback chain updated successfully",
|
||||
primaryChannel = channelType.ToString(),
|
||||
fallbackChain = chain.Select(c => c.ToString()).ToList()
|
||||
});
|
||||
})
|
||||
.WithName("SetFallbackChain")
|
||||
.WithSummary("Sets a custom fallback chain for a channel type");
|
||||
|
||||
// Test fallback resolution
|
||||
group.MapPost("/test", async (
|
||||
TestFallbackRequest request,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
|
||||
if (!Enum.TryParse<NotifyChannelType>(request.FailedChannelType, out var channelType))
|
||||
{
|
||||
return Results.BadRequest(new { error = $"Invalid channel type: {request.FailedChannelType}" });
|
||||
}
|
||||
|
||||
var deliveryId = $"test-{Guid.NewGuid():N}"[..20];
|
||||
|
||||
// Simulate failure recording
|
||||
await fallbackHandler.RecordFailureAsync(
|
||||
tenantId, deliveryId, channelType, "Test failure", cancellationToken);
|
||||
|
||||
// Get fallback result
|
||||
var result = await fallbackHandler.GetFallbackAsync(
|
||||
tenantId, channelType, deliveryId, cancellationToken);
|
||||
|
||||
// Clean up test state
|
||||
await fallbackHandler.ClearDeliveryStateAsync(tenantId, deliveryId, cancellationToken);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
testDeliveryId = deliveryId,
|
||||
result.HasFallback,
|
||||
nextChannelType = result.NextChannelType?.ToString(),
|
||||
result.AttemptNumber,
|
||||
result.TotalChannels,
|
||||
result.IsExhausted,
|
||||
result.ExhaustionReason,
|
||||
failedChannels = result.FailedChannels.Select(f => new
|
||||
{
|
||||
channelType = f.ChannelType.ToString(),
|
||||
f.Reason,
|
||||
f.FailedAt,
|
||||
f.AttemptNumber
|
||||
}).ToList()
|
||||
});
|
||||
})
|
||||
.WithName("TestFallback")
|
||||
.WithSummary("Tests fallback resolution without affecting real deliveries");
|
||||
|
||||
// Clear delivery state
|
||||
group.MapDelete("/deliveries/{deliveryId}", async (
|
||||
string deliveryId,
|
||||
HttpContext context,
|
||||
IFallbackHandler fallbackHandler,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "default";
|
||||
|
||||
await fallbackHandler.ClearDeliveryStateAsync(tenantId, deliveryId, cancellationToken);
|
||||
|
||||
return Results.Ok(new { message = $"Delivery state for '{deliveryId}' cleared" });
|
||||
})
|
||||
.WithName("ClearDeliveryFallbackState")
|
||||
.WithSummary("Clears fallback state for a specific delivery");
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to set a custom fallback chain.
|
||||
/// </summary>
|
||||
public sealed record SetFallbackChainRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Ordered list of fallback channel types.
|
||||
/// </summary>
|
||||
public required List<string> FallbackChain { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to test fallback resolution.
|
||||
/// </summary>
|
||||
public sealed record TestFallbackRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The channel type that "failed".
|
||||
/// </summary>
|
||||
public required string FailedChannelType { get; init; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user