Add mcp logs command for streaming container logs
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>
This commit is contained in:
@@ -100,6 +100,9 @@ func Run(cfg *config.AgentConfig) error {
|
||||
grpc.ChainUnaryInterceptor(
|
||||
auth.AuthInterceptor(validator),
|
||||
),
|
||||
grpc.ChainStreamInterceptor(
|
||||
auth.StreamAuthInterceptor(validator),
|
||||
),
|
||||
)
|
||||
mcpv1.RegisterMcpAgentServiceServer(server, a)
|
||||
|
||||
|
||||
75
internal/agent/logs.go
Normal file
75
internal/agent/logs.go
Normal file
@@ -0,0 +1,75 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user