New server-streaming Logs RPC streams container output to the CLI. Supports --tail/-n, --follow/-f, --timestamps/-t, --since. Detects journald log driver and falls back to journalctl (podman logs can't read journald outside the originating user session). New containers default to k8s-file via mcp user's containers.conf. Also adds stream auth interceptor for the agent gRPC server (required for streaming RPCs). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
76 lines
2.0 KiB
Go
76 lines
2.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"bufio"
|
|
"io"
|
|
|
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/mc/mcp/internal/registry"
|
|
"git.wntrmute.dev/mc/mcp/internal/runtime"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// 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)
|
|
|
|
podman, ok := a.Runtime.(*runtime.Podman)
|
|
if !ok {
|
|
return status.Error(codes.Internal, "logs requires podman runtime")
|
|
}
|
|
|
|
cmd := podman.Logs(stream.Context(), containerName, int(req.GetTail()), req.GetFollow(), req.GetTimestamps(), req.GetSince())
|
|
|
|
a.Logger.Info("running podman logs", "container", containerName, "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() {
|
|
if err := stream.Send(&mcpv1.LogsResponse{
|
|
Data: append(scanner.Bytes(), '\n'),
|
|
}); err != nil {
|
|
_ = cmd.Process.Kill()
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|