Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions availableVersions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"production": "1.4.5",
"test": "1.4.5",
"local": "1.4.5",
"all": "1.4.5"
"production": "1.4.6",
"test": "1.4.6",
"local": "1.4.6",
"all": "1.4.6"
}
63 changes: 7 additions & 56 deletions src/container/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -623,38 +622,14 @@ func (c *Compose) DownRemoveOrphansContext(ctx context.Context, dockerComposePat
// caller predating time-windowed queries expects. Since and Until are RFC3339
// or Unix-seconds strings: resolve relative durations with common.ParseLogTime
// before filling them in, so the device's clock is applied exactly once and the
// same absolute window reaches both the Docker and the compose path.
// same absolute window reaches every container a read covers.
type LogQuery struct {
Tail uint64
Since string
Until string
Timestamps bool
}

// ComposeArgs renders the query as `docker compose logs` flags.
//
// --no-color is unconditional: compose only auto-disables colour when it
// detects a TTY, and an ANSI escape that survives into a log line is noise for
// every consumer we have.
func (q LogQuery) ComposeArgs() []string {
args := []string{"logs", "--no-color"}
if q.Tail > 0 {
args = append(args, "--tail", strconv.FormatUint(q.Tail, 10))
} else {
args = append(args, "--tail", "all")
}
if q.Timestamps {
args = append(args, "--timestamps")
}
if q.Since != "" {
args = append(args, "--since", q.Since)
}
if q.Until != "" {
args = append(args, "--until", q.Until)
}
return args
}

// DockerOptions renders the query as option keys for Container.Logs.
func (q LogQuery) DockerOptions() common.Dict {
options := common.Dict{"follow": false, "stdout": true, "stderr": true}
Expand Down Expand Up @@ -685,39 +660,15 @@ func ListComposeProjectContainers(ctx context.Context, cont Container, composeAp
project := common.NormalizeComposeProjectName(composeAppName)
return cont.ListContainers(ctx, common.Dict{
"all": true,
"filters": filters.NewArgs(filters.Arg("label", "com.docker.compose.project="+project)),
"filters": filters.NewArgs(filters.Arg("label", ComposeProjectLabel+"="+project)),
})
}

// ComposeConfigFilesLabel carries the compose file(s) a container was created
// from — the same value `docker compose ls` reports as ConfigFiles.
const ComposeConfigFilesLabel = "com.docker.compose.project.config_files"

// Logs reads the logs of a whole compose project.
//
// Every service in the project is included, each line prefixed with its service
// name — for a multi-container app that prefix is the only thing identifying
// which service spoke, so it is deliberately kept. Output is CombinedOutput, so
// compose's own progress and warning text on stderr is interleaved with the
// container output.
func (c *Compose) Logs(ctx context.Context, dockerComposePath string, query LogQuery) (io.ReadCloser, error) {
binary := c.binary
if binary == "" {
binary = "docker"
}

args := append([]string{"compose", "-f", dockerComposePath}, query.ComposeArgs()...)

output, err := exec.CommandContext(ctx, binary, args...).CombinedOutput() //nolint:gosec // G204: fixed docker compose CLI; arguments are compose file paths and fixed flags, values as separate argv entries
if err != nil {
return nil, err
}

reader := strings.NewReader(string(output))
readCloser := io.NopCloser(reader)

return readCloser, nil
}
// Labels docker compose stamps on every container it creates.
const (
ComposeProjectLabel = "com.docker.compose.project"
ComposeServiceLabel = "com.docker.compose.service"
)

func (c *Compose) LogStream(dockerComposePath string) (chan string, error) {
// c.logStreamMapMutex.Lock()
Expand Down
34 changes: 4 additions & 30 deletions src/container/logquery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import (
"github.com/stretchr/testify/assert"
)

// The two renderers below are the whole contract between a windowed log query
// and the two very different things that can serve it — the Docker API and the
// compose CLI. Getting a flag name or a zero-value default wrong here is
// invisible at compile time and shows up as "the time range was ignored", so
// they are asserted directly rather than through a daemon.
// The renderer below is the whole contract between a windowed log query and the
// Docker API that serves it. Getting an option name or a zero-value default
// wrong here is invisible at compile time and shows up as "the time range was
// ignored", so it is asserted directly rather than through a daemon.

func TestLogQueryDockerOptions(t *testing.T) {
t.Run("zero value reads everything without timestamps", func(t *testing.T) {
Expand Down Expand Up @@ -41,31 +40,6 @@ func TestLogQueryDockerOptions(t *testing.T) {
})
}

func TestLogQueryComposeArgs(t *testing.T) {
t.Run("zero value reads everything", func(t *testing.T) {
args := LogQuery{}.ComposeArgs()

assert.Equal(t, []string{"logs", "--no-color", "--tail", "all"}, args)
})

t.Run("renders every bound as a flag", func(t *testing.T) {
args := LogQuery{
Tail: 50,
Since: "2026-08-04T09:00:00Z",
Until: "2026-08-04T09:15:00Z",
Timestamps: true,
}.ComposeArgs()

assert.Equal(t, []string{
"logs", "--no-color",
"--tail", "50",
"--timestamps",
"--since", "2026-08-04T09:00:00Z",
"--until", "2026-08-04T09:15:00Z",
}, args)
})
}

func TestLogsOptionsFromDict(t *testing.T) {
t.Run("maps every recognised key", func(t *testing.T) {
options := logsOptionsFromDict(common.Dict{
Expand Down
13 changes: 1 addition & 12 deletions src/logging/applog.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,23 +727,12 @@ func (lm *LogManager) getNonAgentLogsCompose(containerName string) ([]string, er
}

if containsOnlyAgentLogs || len(history) == 0 {
reader, err := lm.composeLogs(context.Background(), containerName, container.LogQuery{Tail: 50})
containerHistory, _, err := lm.composeLogs(context.Background(), containerName, container.LogQuery{Tail: 50})
if err != nil {
log.Warn().Err(err).Msgf("No log history found for: %s", containerName)
return []string{}, nil
}

var containerHistory []string
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
containerHistory = append(containerHistory, scanner.Text())
}

err = reader.Close()
if err != nil {
log.Error().Err(err).Msg("failed to close reader after getting logs")
}

return containerHistory, nil
}

Expand Down
Loading
Loading