mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
8811dc82bd
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
2.8 KiB
Go
110 lines
2.8 KiB
Go
package cloud
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/amir20/dozzle/internal/container"
|
|
pb "github.com/amir20/dozzle/proto/cloud"
|
|
)
|
|
|
|
type fetchLogsArgs struct {
|
|
ContainerID string `json:"container_id"`
|
|
Host string `json:"host_id"`
|
|
Start string `json:"start"`
|
|
End string `json:"end"`
|
|
Level string `json:"level"`
|
|
Query string `json:"query"`
|
|
Regex string `json:"regex"`
|
|
Inverse bool `json:"inverse"`
|
|
}
|
|
|
|
func executeFetchContainerLogs(ctx context.Context, argsJSON string, deps ToolDeps) (*pb.CallToolResponse, error) {
|
|
var args fetchLogsArgs
|
|
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
|
return nil, fmt.Errorf("failed to parse arguments: %w", err)
|
|
}
|
|
hostID, containerID, note, err := resolveContainerRefRead(args.ContainerID, args.Host, deps)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cs, err := deps.HostService.FindContainer(hostID, containerID, deps.Labels)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("container not found: %w", err)
|
|
}
|
|
|
|
start := time.Now().Add(-1 * time.Hour)
|
|
end := time.Now()
|
|
if args.Start != "" {
|
|
t, err := time.Parse(time.RFC3339, args.Start)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid start time format (expected RFC3339): %w", err)
|
|
}
|
|
start = t
|
|
}
|
|
if args.End != "" {
|
|
t, err := time.Parse(time.RFC3339, args.End)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid end time format (expected RFC3339): %w", err)
|
|
}
|
|
end = t
|
|
}
|
|
|
|
var re *regexp.Regexp
|
|
if args.Regex != "" {
|
|
var err error
|
|
re, err = regexp.Compile(args.Regex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid regex pattern: %w", err)
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
logCh, err := cs.LogsBetweenDates(ctx, start, end, container.STDOUT|container.STDERR)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch logs: %w", err)
|
|
}
|
|
|
|
const maxLines = 100
|
|
entries := make([]*pb.LogEntry, 0, maxLines)
|
|
for event := range logCh {
|
|
msg, matches := matchesFilters(event, &args, re)
|
|
if !matches {
|
|
continue
|
|
}
|
|
|
|
entries = append(entries, &pb.LogEntry{
|
|
Timestamp: event.Timestamp,
|
|
Message: msg,
|
|
Stream: event.Stream,
|
|
Level: event.Level,
|
|
})
|
|
|
|
if len(entries) >= maxLines {
|
|
break
|
|
}
|
|
}
|
|
|
|
return &pb.CallToolResponse{
|
|
Success: true,
|
|
Result: &pb.CallToolResponse_FetchLogs{FetchLogs: &pb.FetchLogsResult{ContainerName: withResolutionNote(cs.Container.Name, note), Entries: entries}},
|
|
}, nil
|
|
}
|
|
|
|
// withResolutionNote appends the read-path resolution note (if any) to a
|
|
// model-visible identity string, so the model learns which container an
|
|
// ambiguous name resolved to — and that siblings exist — in the same turn,
|
|
// without a round-trip. Empty note returns name unchanged.
|
|
func withResolutionNote(name, note string) string {
|
|
if note == "" {
|
|
return name
|
|
}
|
|
return name + " " + note
|
|
}
|