Skip to content

Commit

Permalink
feat: provide container logs on container startup failures (#1297)
Browse files Browse the repository at this point in the history
* chore: move waitFor to the default container hooks

* chore: print wait for values in the log

* feat: add container logs when the starting/started hooks of the container fails
  • Loading branch information
mdelapenya authored Jun 22, 2023
1 parent 10f1547 commit eca8ba8
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 12 deletions.
33 changes: 21 additions & 12 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,23 +193,11 @@ func (c *DockerContainer) Start(ctx context.Context) error {
return err
}

shortID := c.ID[:12]

if err := c.provider.client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil {
return err
}
defer c.provider.Close()

// if a Wait Strategy has been specified, wait before returning
if c.WaitingFor != nil {
c.logger.Printf("🚧 Waiting for container id %s image: %s", shortID, c.Image)
if err := c.WaitingFor.WaitUntilReady(ctx, c); err != nil {
return err
}
}

c.isRunning = true

err = c.startedHook(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -1027,6 +1015,27 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
}
}

return nil
},
},
PostStarts: []ContainerHook{
// first post-start hook is to wait for the container to be ready
func(ctx context.Context, c Container) error {
dockerContainer := c.(*DockerContainer)

// if a Wait Strategy has been specified, wait before returning
if dockerContainer.WaitingFor != nil {
dockerContainer.logger.Printf(
"🚧 Waiting for container id %s image: %s. Waiting for: %+v",
dockerContainer.ID[:12], dockerContainer.Image, dockerContainer.WaitingFor,
)
if err := dockerContainer.WaitingFor.WaitUntilReady(ctx, c); err != nil {
return err
}
}

dockerContainer.isRunning = true

return nil
},
},
Expand Down
21 changes: 21 additions & 0 deletions lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package testcontainers

import (
"context"
"io"
"strings"

"github.com/docker/docker/api/types/container"
Expand Down Expand Up @@ -130,6 +131,7 @@ func (c *DockerContainer) startingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PreStarts)(c)
if err != nil {
c.printLogs(ctx)
return err
}
}
Expand All @@ -142,13 +144,32 @@ func (c *DockerContainer) startedHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
err := containerHookFn(ctx, lifecycleHooks.PostStarts)(c)
if err != nil {
c.printLogs(ctx)
return err
}
}

return nil
}

// printLogs is a helper function that will print the logs of a Docker container
// We are going to use this helper function to inform the user of the logs when an error occurs
func (c *DockerContainer) printLogs(ctx context.Context) {
reader, err := c.Logs(ctx)
if err != nil {
c.logger.Printf("failed accessing container logs: %w\n", err)
return
}

b, err := io.ReadAll(reader)
if err != nil {
c.logger.Printf("failed reading container logs: %w\n", err)
return
}

c.logger.Printf("container logs:\n%s", b)
}

// stoppingHook is a hook that will be called before a container is stopped
func (c *DockerContainer) stoppingHook(ctx context.Context) error {
for _, lifecycleHooks := range c.lifecycleHooks {
Expand Down
73 changes: 73 additions & 0 deletions lifecycle_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package testcontainers

import (
"bufio"
"context"
"fmt"
"strings"
Expand All @@ -14,6 +15,8 @@ import (
"github.com/docker/go-connections/nat"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/wait"
)

func TestPreCreateModifierHook(t *testing.T) {
Expand Down Expand Up @@ -634,6 +637,76 @@ func TestLifecycleHooks_WithMultipleHooks(t *testing.T) {
require.Equal(t, 20, len(dl.data))
}

type linesTestLogger struct {
data []string
}

func (l *linesTestLogger) Printf(format string, args ...interface{}) {
l.data = append(l.data, fmt.Sprintf(format, args...))
}

func TestPrintContainerLogsOnError(t *testing.T) {
ctx := context.Background()
client, err := testcontainersdocker.NewClient(ctx)
if err != nil {
t.Fatal(err)
}
defer client.Close()

req := ContainerRequest{
Image: "docker.io/alpine",
Cmd: []string{"echo", "-n", "I am expecting this"},
WaitingFor: wait.ForLog("I was expecting that").WithStartupTimeout(5 * time.Second),
}

arrayOfLinesLogger := linesTestLogger{
data: []string{},
}

container, err := GenericContainer(ctx, GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: req,
Logger: &arrayOfLinesLogger,
Started: true,
})
// it should fail because the waiting for condition is not met
if err == nil {
t.Fatal(err)
}
terminateContainerOnEnd(t, ctx, container)

containerLogs, err := container.Logs(ctx)
if err != nil {
t.Fatal(err)
}
defer containerLogs.Close()

// read container logs line by line, checking that each line is present in the stdout
rd := bufio.NewReader(containerLogs)
for {
line, err := rd.ReadString('\n')
if err != nil {
if err.Error() == "EOF" {
break
}

t.Fatal("Read Error:", err)
}

// the last line of the array should contain the line of interest,
// but we are checking all the lines to make sure that is present
found := false
for _, l := range arrayOfLinesLogger.data {
if strings.Contains(l, line) {
found = true
break
}
}
assert.True(t, found, "container log line not found in the output of the logger: %s", line)
}

}

func lifecycleHooksIsHonouredFn(t *testing.T, ctx context.Context, container Container, prints []string) {
require.Equal(t, 20, len(prints))

Expand Down

0 comments on commit eca8ba8

Please sign in to comment.