OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user