Following Container Logs¶
The log-following functionality follows a producer-consumer model: the container produces logs, and your code consumes them. So if you wish to follow container logs, you have to do two things:
- set up log consumers.
- configure the log production of the container (e.g. timeout for the logs).
As logs are written to either stdout
, or stderr
(stdin
is not supported) they will be forwarded (produced) to any associated log consumer.
Creating a LogConsumer¶
A LogConsumer
must implement the LogConsumer
interface, and it could be as simple as directly printing the log to stdout
,
as in the following example:
// LogConsumer represents any object that can
// handle a Log, it is up to the LogConsumer instance
// what to do with the log
type LogConsumer interface {
Accept(Log)
}
// Log represents a message that was created by a process,
// LogType is either "STDOUT" or "STDERR",
// Content is the byte contents of the message itself
type Log struct {
LogType string
Content []byte
}
// StdoutLogConsumer is a LogConsumer that prints the log to stdout
type StdoutLogConsumer struct{}
// Accept prints the log to stdout
func (lc *StdoutLogConsumer) Accept(l Log) {
fmt.Print(string(l.Content))
}
You can associate LogConsumer
s in two manners:
- as part of the
ContainerRequest
struct. - with the
FollowOutput
function (deprecated).
Passing the LogConsumers in the ContainerRequest¶
This will represent the current way for associating LogConsumer
s. You simply define your consumers, and attach them as a slice to the ContainerRequest
in the
LogConsumerCfg
field. See the following example, where g
is an instance of a given LogConsumer
struct.
req := ContainerRequest{
FromDockerfile: FromDockerfile{
Context: "./testdata/",
Dockerfile: "echoserver.Dockerfile",
},
ExposedPorts: []string{"8080/tcp"},
WaitingFor: wait.ForLog("ready"),
LogConsumerCfg: &LogConsumerConfig{
Opts: []LogProductionOption{WithLogProductionTimeout(10 * time.Second)},
Consumers: []LogConsumer{&g},
},
}
Please check that it's possible to configure the log production with an slice of functional options. These options must be of the LogProductionOption
type:
type LogProductionOption func(*DockerContainer)
At the moment, Testcontainers for Go exposes an option to set log production timeout, using the WithLogProductionTimeout
function.
Testcontainers for Go will read this log producer/consumer configuration to automatically start producing logs if an only if the consumers slice contains at least one valid LogConsumer
.
Manually using the FollowOutput function¶
Warning
This method is not recommended, as it requires you to manually manage the LogConsumer
lifecycle.
We recommend using the ContainerRequest
struct to associate LogConsumer
s, as it's the simplest and most straightforward method.
If you use both methods, you can get an error, as the StartLogProducer
function could be called twice, which is not allowed.
As a consequence, this lifecycle (StartLogProducer
, FollowOutput
and `StopLogProducer) will be deprecated in the future, delegating the control to the library.
Instead of passing the LogConsumer
as part of the ContainerRequest
struct, you can manually call the FollowOutput
function on a Container
instance.
This allows you to dynamically add LogConsumer
s to a running container, although it forces you to manually manage the LogConsumer
lifecycle,
calling StartLogProducer
after the FollowOutput
function, and do it just once.
You can define a log consumer like so:
type TestLogConsumer struct {
Msgs []string // store the logs as a slice of strings
}
func (g *TestLogConsumer) Accept(l Log) {
g.Msgs = append(g.Msgs, string(l.Content))
}
And then associate it with a container like so:
g := TestLogConsumer{
Msgs: []string{},
}
// Remember that this method will be deprecated in the future
c.FollowOutput(&g) // must be called before StarLogProducer
// Remember that this method will be deprecated in the future
err := c.StartLogProducer(ctx)
if err != nil {
// do something with err
}
// some stuff happens...
// Remember that this method will be deprecated in the future
err = c.StopLogProducer()
if err != nil {
// do something with err
}
Stopping the Log Production¶
The production of logs is automatically stopped in c.Terminate()
, so you don't have to worry about that.
Warning
It can be done manually during container lifecycle using c.StopLogProducer()
, but it's not recommended, as it will be deprecated in the future.
Listening to errors¶
When the log production fails to start within given timeout (causing a context deadline) or there's an error returned while closing the reader it will no longer panic, but instead will return an error over a channel. You can listen to it using DockerContainer.GetLogProductionErrorChannel()
method:
func (c *DockerContainer) GetLogProductionErrorChannel() <-chan error {
return c.producerError
}
This allows you to, for example, retry restarting the log production if it fails to start the first time.
For example, you would start the log production normally, defining the log production configuration at the ContainerRequest
struct, and then:
// start log production normally, using the ContainerRequest struct, or
// using the deprecated c.StartLogProducer method.
// err = container.StartLogProducer(ctx, WithLogProductionTimeout(10*time.Second))
// listen to errors in a detached goroutine
go func(done chan struct{}, timeout time.Duration) {
for {
select {
case logErr := <-container.GetLogProductionErrorChannel():
if logErr != nil {
// do something with error
// for example, retry starting the log production
// (here we retry it once, in real life you might want to retry it more times)
startErr := container.StartLogProducer(ctx, timeout)
if startErr != nil {
return
}
case <-done:
return
}
}
}(cons.logListeningDone, time.Duration(10*time.Second))