109 lines
3.7 KiB
C#
109 lines
3.7 KiB
C#
using Docker.DotNet;
|
|
using Docker.DotNet.Models;
|
|
using ControlPlane.Core.Services;
|
|
|
|
namespace ControlPlane.Api.Endpoints;
|
|
|
|
public static class TenantLogEndpoints
|
|
{
|
|
public static IEndpointRouteBuilder MapTenantLogEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
app.MapGet("/api/tenants/{subdomain}/logs", StreamTenantLogs).WithTags("Tenants");
|
|
return app;
|
|
}
|
|
|
|
private static async Task StreamTenantLogs(
|
|
string subdomain,
|
|
IConfiguration config,
|
|
TenantRegistryService registry,
|
|
HttpContext ctx,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var tenant = registry.GetAll().FirstOrDefault(t => t.Subdomain == subdomain);
|
|
if (tenant is null)
|
|
{
|
|
ctx.Response.StatusCode = 404;
|
|
return;
|
|
}
|
|
|
|
var containerName = tenant.ContainerName;
|
|
if (string.IsNullOrWhiteSpace(containerName))
|
|
{
|
|
ctx.Response.StatusCode = 404;
|
|
return;
|
|
}
|
|
|
|
ctx.Response.Headers.ContentType = "text/event-stream";
|
|
ctx.Response.Headers.CacheControl = "no-cache";
|
|
ctx.Response.Headers.Connection = "keep-alive";
|
|
|
|
var socketUri = config["Docker:Socket"] ?? "npipe://./pipe/docker_engine";
|
|
using var docker = new DockerClientConfiguration(new Uri(socketUri)).CreateClient();
|
|
|
|
var logParams = new ContainerLogsParameters
|
|
{
|
|
ShowStdout = true,
|
|
ShowStderr = true,
|
|
Follow = true,
|
|
Tail = "200",
|
|
Timestamps = true,
|
|
};
|
|
|
|
try
|
|
{
|
|
using var stream = await docker.Containers.GetContainerLogsAsync(
|
|
containerName, tty: false, logParams, cancellationToken);
|
|
|
|
// MultiplexedStream exposes CopyOutputToAsync which separates stdout/stderr
|
|
var stdoutBuf = new System.IO.MemoryStream();
|
|
var stderrBuf = new System.IO.MemoryStream();
|
|
|
|
// Stream with Follow=true won't complete until cancelled — use a pipe instead
|
|
var stdoutPipe = new System.IO.Pipelines.Pipe();
|
|
var stderrPipe = new System.IO.Pipelines.Pipe();
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await stream.CopyOutputToAsync(
|
|
System.IO.Stream.Null,
|
|
stdoutPipe.Writer.AsStream(),
|
|
stderrPipe.Writer.AsStream(),
|
|
cancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
stdoutPipe.Writer.Complete();
|
|
stderrPipe.Writer.Complete();
|
|
}
|
|
}, cancellationToken);
|
|
|
|
// Merge both pipes into SSE — read stdout line by line
|
|
var stdoutReader = new System.IO.StreamReader(stdoutPipe.Reader.AsStream());
|
|
var stderrReader = new System.IO.StreamReader(stderrPipe.Reader.AsStream());
|
|
|
|
var stdoutTask = ReadLinesAsync(stdoutReader, ctx, cancellationToken);
|
|
var stderrTask = ReadLinesAsync(stderrReader, ctx, cancellationToken);
|
|
await Task.WhenAll(stdoutTask, stderrTask);
|
|
}
|
|
catch (OperationCanceledException) { /* client disconnected — normal */ }
|
|
}
|
|
|
|
private static async Task ReadLinesAsync(
|
|
System.IO.StreamReader reader,
|
|
HttpContext ctx,
|
|
CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
var line = await reader.ReadLineAsync(ct);
|
|
if (line is null) break;
|
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
|
|
await ctx.Response.WriteAsync($"data: {line}\n\n", ct);
|
|
await ctx.Response.Body.FlushAsync(ct);
|
|
}
|
|
}
|
|
}
|