diff --git a/pkg/client/logs.go b/pkg/client/logs.go index 651ed46c..48dbf4f5 100644 --- a/pkg/client/logs.go +++ b/pkg/client/logs.go @@ -94,6 +94,12 @@ func (cli *Client) ContainerLogs( Since: opts.Since, Until: opts.Until, } + if !opts.Follow && opts.Tail == 0 { + // If not following and tail is 0, set tail to -1 to return all logs. + // Otherwise, no logs will be returned at all. + req.Tail = -1 + } + stream, err := cli.Docker.GRPCClient.ContainerLogs(proxyCtx, req) if err != nil { return nil, err diff --git a/test/e2e/exec_test.go b/test/e2e/exec_test.go index 0dbfc846..356de07f 100644 --- a/test/e2e/exec_test.go +++ b/test/e2e/exec_test.go @@ -29,10 +29,7 @@ func deployTestService(t *testing.T, ctx context.Context, cli *client.Client, na } deployment := cli.NewDeployment(spec, nil) - err := deployment.Validate(ctx) - require.NoError(t, err) - - _, err = deployment.Run(ctx) + _, err := deployment.Run(ctx) require.NoError(t, err) // Wait for all replicas to be running @@ -41,9 +38,8 @@ func deployTestService(t *testing.T, ctx context.Context, cli *client.Client, na if err != nil { return false } - if uint(len(service.Containers)) != replicas { - return false - } + assertServiceMatchesSpec(t, service, spec) + for _, ctr := range service.Containers { if ctr.Container.State.Status != "running" { return false diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go index ec14de1b..034b03d3 100644 --- a/test/e2e/service_test.go +++ b/test/e2e/service_test.go @@ -3,6 +3,7 @@ package e2e import ( "context" "errors" + "fmt" "net/netip" "slices" "strings" @@ -1682,4 +1683,114 @@ func TestServiceLifecycle(t *testing.T) { assert.Equal(t, api.ServiceModeGlobal, svc.Mode) assert.Len(t, svc.Containers, 3, "expected 1 container on each machine") }) + + t.Run("service logs", func(t *testing.T) { + t.Parallel() + + name := "test-service-logs" + t.Cleanup(func() { + err := cli.RemoveService(ctx, name) + if !errors.Is(err, api.ErrNotFound) { + require.NoError(t, err) + } + }) + + spec := api.ServiceSpec{ + Name: name, + Container: api.ContainerSpec{ + Image: "busybox:1.37.0-musl", + Command: []string{ + "sh", + "-c", + "trap 'exit 0' TERM INT; " + + "echo \"Hello from $HOSTNAME\"; sleep 0.5;" + + "echo \"Hello stderr from $HOSTNAME\" >&2; " + + "while true; do sleep 1; done"}, + }, + Replicas: 3, + } + deployment := cli.NewDeployment(spec, nil) + _, err := deployment.Run(ctx) + require.NoError(t, err) + + svc, err := cli.InspectService(ctx, name) + require.NoError(t, err) + assertServiceMatchesSpec(t, svc, spec) + + machines := serviceMachines(svc) + assert.Len(t, machines.ToSlice(), 3, "should have containers on all 3 machines") + + time.Sleep(1 * time.Second) // Wait for containers to produce the logs. + + logsSvc, stream, err := cli.ServiceLogs(ctx, name, api.ServiceLogsOptions{}) + require.NoError(t, err) + assertServiceMatchesSpec(t, logsSvc, spec) + + collectLogs := func(stream <-chan api.ServiceLogEntry) []string { + var entries []string + timeout := time.After(5 * time.Second) + + for { + select { + case entry, ok := <-stream: + if !ok { + return entries + } + + require.NoError(t, entry.Err) + if entry.Stream == api.LogStreamStdout || entry.Stream == api.LogStreamStderr { + entries = append(entries, string(entry.Message)) + } + case <-timeout: + require.FailNow(t, "timed out waiting for logs") + } + } + } + + logs := collectLogs(stream) + require.Len(t, logs, 6, "should have 6 log entries (1 stdout and 1 stderr from each of 3 replicas)") + + for _, ctr := range svc.Containers { + stdoutMsg := fmt.Sprintf("Hello from %s\n", ctr.Container.Name) + stderrMsg := fmt.Sprintf("Hello stderr from %s\n", ctr.Container.Name) + + assert.Contains(t, logs, stdoutMsg) + assert.Contains(t, logs, stderrMsg) + + outIdx := slices.Index(logs, stdoutMsg) + errIdx := slices.Index(logs, stderrMsg) + assert.Less(t, outIdx, errIdx, "stdout log should appear before stderr log") + } + + // Test filter logs by machine ID. + machineID := c.Machines[0].ID + _, machineStream, err := cli.ServiceLogs(ctx, name, api.ServiceLogsOptions{ + Machines: []string{machineID}, + }) + require.NoError(t, err) + + machineLogs := collectLogs(machineStream) + require.Len(t, machineLogs, 2, "should have 2 log entries (1 stdout and 1 stderr) from the filtered machine") + + ctrName := "" + for _, ctr := range svc.Containers { + if ctr.MachineID == machineID { + ctrName = ctr.Container.Name + break + } + } + require.NotEmpty(t, ctrName, "should have found container on the target machine") + + stdoutMsg := fmt.Sprintf("Hello from %s\n", ctrName) + stderrMsg := fmt.Sprintf("Hello stderr from %s\n", ctrName) + + assert.Equal(t, []string{stdoutMsg, stderrMsg}, machineLogs) + + // Test non-existent machine filter returns error. + _, _, err = cli.ServiceLogs(ctx, name, api.ServiceLogsOptions{ + Machines: []string{"non-existent-machine"}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "machines not found") + }) }