Skip to content

Commit 214c411

Browse files
authored
feat: adds option to tail logs (#278)
Signed-off-by: ChrisJBurns <[email protected]>
1 parent b42b139 commit 214c411

File tree

8 files changed

+91
-64
lines changed

8 files changed

+91
-64
lines changed

cmd/thv/app/commands.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func NewRootCmd() *cobra.Command {
4949
rootCmd.AddCommand(proxyCmd)
5050
rootCmd.AddCommand(restartCmd)
5151
rootCmd.AddCommand(newVersionCmd())
52-
rootCmd.AddCommand(newLogsCommand())
52+
rootCmd.AddCommand(logsCommand())
5353
rootCmd.AddCommand(newSecretCommand())
5454

5555
// Skip update check for completion command

cmd/thv/app/logs.go

+73-57
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,92 @@ package app
22

33
import (
44
"context"
5+
"fmt"
56
"strings"
67

78
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
810

911
"github.com/StacklokLabs/toolhive/pkg/container"
1012
"github.com/StacklokLabs/toolhive/pkg/labels"
1113
"github.com/StacklokLabs/toolhive/pkg/logger"
1214
)
1315

14-
func newLogsCommand() *cobra.Command {
15-
return &cobra.Command{
16+
var (
17+
tailFlag bool
18+
)
19+
20+
func logsCommand() *cobra.Command {
21+
logsCommand := &cobra.Command{
1622
Use: "logs [container-name]",
1723
Short: "Output the logs of an MCP server",
1824
Long: `Output the logs of an MCP server managed by Vibe Tool.`,
1925
Args: cobra.ExactArgs(1),
20-
Run: func(_ *cobra.Command, args []string) {
21-
// Get container name
22-
containerName := args[0]
23-
24-
// Create context
25-
ctx, cancel := context.WithCancel(context.Background())
26-
defer cancel()
27-
28-
// Create container runtime
29-
runtime, err := container.NewFactory().Create(ctx)
30-
if err != nil {
31-
logger.Log.Errorf("failed to create container runtime: %v", err)
32-
return
33-
}
34-
35-
// List containers to find the one with the given name
36-
containers, err := runtime.ListContainers(ctx)
37-
if err != nil {
38-
logger.Log.Errorf("failed to list containers: %v", err)
39-
return
40-
}
41-
42-
// Find the container with the given name
43-
var containerID string
44-
for _, c := range containers {
45-
// Check if the container is managed by Vibe Tool
46-
if !labels.IsToolHiveContainer(c.Labels) {
47-
continue
48-
}
49-
50-
// Check if the container name matches
51-
name := labels.GetContainerName(c.Labels)
52-
if name == "" {
53-
name = c.Name // Fallback to container name
54-
}
55-
56-
// Check if the name matches (exact match or prefix match)
57-
if name == containerName || strings.HasPrefix(c.ID, containerName) {
58-
containerID = c.ID
59-
break
60-
}
61-
}
62-
63-
if containerID == "" {
64-
logger.Log.Infof("container %s not found", containerName)
65-
return
66-
}
67-
68-
logs, err := runtime.ContainerLogs(ctx, containerID)
69-
if err != nil {
70-
logger.Log.Errorf("failed to get container logs: %v", err)
71-
return
72-
}
73-
logger.Log.Infof(logs)
74-
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
return logsCmdFunc(cmd, args)
7528
},
7629
}
30+
31+
logsCommand.Flags().BoolVarP(&tailFlag, "tail", "t", false, "Tail the logs")
32+
err := viper.BindPFlag("tail", logsCommand.Flags().Lookup("tail"))
33+
if err != nil {
34+
logger.Log.Errorf("failed to bind flag: %v", err)
35+
}
36+
37+
return logsCommand
38+
}
39+
40+
func logsCmdFunc(_ *cobra.Command, args []string) error {
41+
// Get container name
42+
containerName := args[0]
43+
44+
// Create context
45+
ctx, cancel := context.WithCancel(context.Background())
46+
defer cancel()
47+
48+
// Create container runtime
49+
runtime, err := container.NewFactory().Create(ctx)
50+
if err != nil {
51+
return fmt.Errorf("failed to create container runtime: %v", err)
52+
}
53+
54+
// List containers to find the one with the given name
55+
containers, err := runtime.ListContainers(ctx)
56+
if err != nil {
57+
return fmt.Errorf("failed to list containers: %w", err)
58+
}
59+
60+
// Find the container with the given name
61+
var containerID string
62+
for _, c := range containers {
63+
// Check if the container is managed by Vibe Tool
64+
if !labels.IsToolHiveContainer(c.Labels) {
65+
continue
66+
}
67+
68+
// Check if the container name matches
69+
name := labels.GetContainerName(c.Labels)
70+
if name == "" {
71+
name = c.Name // Fallback to container name
72+
}
73+
74+
// Check if the name matches (exact match or prefix match)
75+
if name == containerName || strings.HasPrefix(c.ID, containerName) {
76+
containerID = c.ID
77+
break
78+
}
79+
}
80+
81+
if containerID == "" {
82+
logger.Log.Infof("container %s not found", containerName)
83+
return nil
84+
}
85+
86+
tail := viper.GetBool("tail")
87+
logs, err := runtime.ContainerLogs(ctx, containerID, tail)
88+
if err != nil {
89+
return fmt.Errorf("failed to get container logs: %v", err)
90+
}
91+
fmt.Print(logs)
92+
return nil
7793
}

docs/cli/thv_logs.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/container/docker/client.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,12 @@ func (c *Client) RemoveContainer(ctx context.Context, containerID string) error
452452
}
453453

454454
// ContainerLogs gets container logs
455-
func (c *Client) ContainerLogs(ctx context.Context, containerID string) (string, error) {
455+
func (c *Client) ContainerLogs(ctx context.Context, containerID string, tail bool) (string, error) {
456456
options := container.LogsOptions{
457457
ShowStdout: true,
458458
ShowStderr: true,
459+
Follow: tail,
460+
Tail: "100",
459461
}
460462

461463
// Get logs
@@ -465,6 +467,14 @@ func (c *Client) ContainerLogs(ctx context.Context, containerID string) (string,
465467
}
466468
defer logs.Close()
467469

470+
if tail {
471+
_, err = io.Copy(os.Stdout, logs)
472+
if err != nil && err != io.EOF {
473+
logger.Log.Errorf("Error reading container logs: %v", err)
474+
return "", NewContainerError(err, containerID, fmt.Sprintf("failed to tail container logs: %v", err))
475+
}
476+
}
477+
468478
// Read logs
469479
logBytes, err := io.ReadAll(logs)
470480
if err != nil {

pkg/container/docker/monitor.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (m *ContainerMonitor) monitor(ctx context.Context) {
117117
// Container has exited, get logs and info
118118
// Create a short timeout context for these operations
119119
infoCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
120-
logs, _ := m.runtime.ContainerLogs(infoCtx, m.containerID)
120+
logs, _ := m.runtime.ContainerLogs(infoCtx, m.containerID, false)
121121
info, _ := m.runtime.GetContainerInfo(infoCtx, m.containerID)
122122
cancel() // Always cancel the context to avoid leaks
123123

pkg/container/kubernetes/client.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ func (c *Client) AttachContainer(ctx context.Context, containerID string) (io.Wr
202202
}
203203

204204
// ContainerLogs implements runtime.Runtime.
205-
func (c *Client) ContainerLogs(ctx context.Context, containerID string) (string, error) {
205+
func (c *Client) ContainerLogs(ctx context.Context, containerID string, tail bool) (string, error) {
206206
// In Kubernetes, containerID is the statefulset name
207207
namespace := getCurrentNamespace()
208208

@@ -224,7 +224,7 @@ func (c *Client) ContainerLogs(ctx context.Context, containerID string) (string,
224224
// Get logs from the pod
225225
logOptions := &corev1.PodLogOptions{
226226
Container: mcpContainerName,
227-
Follow: false,
227+
Follow: tail,
228228
Previous: false,
229229
Timestamps: true,
230230
}

pkg/container/runtime/types.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ type Runtime interface {
6565
RemoveContainer(ctx context.Context, containerID string) error
6666

6767
// ContainerLogs gets container logs
68-
ContainerLogs(ctx context.Context, containerID string) (string, error)
68+
ContainerLogs(ctx context.Context, containerID string, tail bool) (string, error)
6969

7070
// IsContainerRunning checks if a container is running
7171
IsContainerRunning(ctx context.Context, containerID string) (bool, error)

pkg/runner/config_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (*mockRuntime) RemoveContainer(_ context.Context, _ string) error {
4949
return nil
5050
}
5151

52-
func (*mockRuntime) ContainerLogs(_ context.Context, _ string) (string, error) {
52+
func (*mockRuntime) ContainerLogs(_ context.Context, _ string, _ bool) (string, error) {
5353
return "", nil
5454
}
5555

0 commit comments

Comments
 (0)