package agent import ( "bufio" "context" "io" "os/exec" mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" "git.wntrmute.dev/mc/mcp/internal/registry" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // logStreamer is implemented by both the Podman (container logs) and QEMU // (serial console) runtimes. type logStreamer interface { Logs(ctx context.Context, name string, tail int, follow, timestamps bool, since string) *exec.Cmd } // Logs streams container logs for a service component. func (a *Agent) Logs(req *mcpv1.LogsRequest, stream mcpv1.McpAgentService_LogsServer) error { if req.GetService() == "" { return status.Error(codes.InvalidArgument, "service name is required") } // Resolve component name. component := req.GetComponent() if component == "" { components, err := registry.ListComponents(a.DB, req.GetService()) if err != nil { return status.Errorf(codes.Internal, "list components: %v", err) } if len(components) == 0 { return status.Error(codes.NotFound, "no components found for service") } component = components[0].Name } containerName := ContainerNameFor(req.GetService(), component) // Select the runtime for this component (container vs unikernel) and // stream its logs (podman logs / journald, or the VM serial console). var compRuntime string if c, err := registry.GetComponent(a.DB, req.GetService(), component); err == nil { compRuntime = c.Runtime } ls, ok := a.runtimeFor(compRuntime).(logStreamer) if !ok { return status.Error(codes.Internal, "selected runtime does not support log streaming") } cmd := ls.Logs(stream.Context(), containerName, int(req.GetTail()), req.GetFollow(), req.GetTimestamps(), req.GetSince()) a.Logger.Info("streaming logs", "container", containerName, "runtime", compRuntime, "args", cmd.Args) // Podman writes container stdout to its stdout and container stderr // to its stderr. Merge both into a single pipe. pr, pw := io.Pipe() cmd.Stdout = pw cmd.Stderr = pw if err := cmd.Start(); err != nil { _ = pw.Close() return status.Errorf(codes.Internal, "start podman logs: %v", err) } // Close the write end when the command exits so the scanner finishes. go func() { err := cmd.Wait() if err != nil { a.Logger.Warn("podman logs exited", "container", containerName, "error", err) } _ = pw.Close() }() scanner := bufio.NewScanner(pr) for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 { continue } if err := stream.Send(&mcpv1.LogsResponse{ Data: append(line, '\n'), }); err != nil { _ = cmd.Process.Kill() return err } } return nil }