package docker import ( "bufio" "fmt" "io" "os/exec" "syscall" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/config" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/logsource" ) var _ logsource.LogSource = new(DockerLogSource) // DockerLogSource reads logs from a Docker container via `docker logs -f`. type DockerLogSource struct { cmd *exec.Cmd stdout io.ReadCloser stdoutscanner *bufio.Scanner } // ReadLine implements logsource.LogSource. func (d *DockerLogSource) ReadLine() (string, error) { if d.stdoutscanner.Scan() { return d.stdoutscanner.Text(), nil } else { if d.stdoutscanner.Err() == nil { return "", fmt.Errorf("%w: %s", logsource.ErrDead, io.EOF) } return "", d.stdoutscanner.Err() } } // Close implements logsource.LogSource. func (d *DockerLogSource) Close() error { _ = d.cmd.Process.Signal(syscall.SIGTERM) return d.cmd.Wait() } // New creates a Docker log source for the given container. // The container name is taken from cfg.Path (if set) or cfg.Name. func New(cfg config.ServiceConfig) (*DockerLogSource, error) { containerName := cfg.Name if cfg.Path != nil && *cfg.Path != "" { containerName = *cfg.Path } // docker logs -f --tail=0 --no-color // -f : follow new logs // --tail=0 : skip existing logs // --no-color: strip color codes for clean output cmd := exec.Command("docker", "logs", "-f", "--tail=0", "--no-color", containerName) //nolint:gosec stdout, err := cmd.StdoutPipe() if err != nil { return nil, fmt.Errorf("failed to create stdout pipe for docker logs: %w", err) } // Also capture stderr since docker logs merges stdout and stderr from the container stderr, err := cmd.StderrPipe() if err != nil { return nil, fmt.Errorf("failed to create stderr pipe for docker logs: %w", err) } err = cmd.Start() if err != nil { return nil, fmt.Errorf("failed to start docker logs for container %q: %w", containerName, err) } // Use MultiReader to merge stdout and stderr // Docker logs outputs container stdout+stderr to its own stdout, but we also // capture the docker CLI's stderr separately in case of errors (e.g. container not found) stdoutscanner := bufio.NewScanner(stdout) // Start a goroutine to consume stderr (we don't send docker CLI stderr as logs, // but we need to prevent the pipe from filling up) go func() { buf := make([]byte, 4096) for { _, err := stderr.Read(buf) if err != nil { return } } }() return &DockerLogSource{cmd, stdout, stdoutscanner}, nil }