From 6b7fc11a6e6e4277c8c9e55a8fba6ebb4cd81dd7 Mon Sep 17 00:00:00 2001 From: ClaytonNorthey92 Date: Fri, 7 Feb 2020 00:06:48 -0800 Subject: [PATCH] Feature #133 - Add a FollowOutput method to attach log consumers to --- README.md | 42 +++++++ container.go | 9 +- docker.go | 94 +++++++++++++++ go.mod | 4 +- go.sum | 33 ++++++ logconsumer.go | 22 ++++ logconsumer_test.go | 175 ++++++++++++++++++++++++++++ testresources/echoserver.Dockerfile | 13 +++ testresources/echoserver.go | 51 ++++++++ 9 files changed, 438 insertions(+), 5 deletions(-) create mode 100644 logconsumer.go create mode 100644 logconsumer_test.go create mode 100644 testresources/echoserver.Dockerfile create mode 100644 testresources/echoserver.go diff --git a/README.md b/README.md index 31741b8672..ef8c7290b7 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,45 @@ req := ContainerRequest{ Cmd: []string{"echo", "command override!"}, } ``` + +## Following Container Logs + +If you wish to follow container logs, you can set up `LogConsumer`s. The log following functionality follows +a producer-consumer model. You will need to explicitly start and stop the producer. As logs are written to either +`stdout`, or `stderr` (`stdin` is not supported) they will be forwarded (produced) to any associated `LogConsumer`s. You can associate `LogConsumer`s +with the `.FollowOutput` function. + +**Please note** if you start the producer you should always stop it explicitly. + +for example, this consumer will just add logs to a slice + +```go +type TestLogConsumer struct { + Msgs []string +} + +func (g *TestLogConsumer) Accept(l Log) { + g.Msgs = append(g.Msgs, string(l.Content)) +} +``` + +this can be used like so: +```go +g := TestLogConsumer{ + Msgs: []string{}, +} + +err := c.StartLogProducer(ctx) +if err != nil { + // do something with err +} + +c.FollowOutput(&g) + +// some stuff happens... + +err = c.StopLogProducer() +if err != nil { + // do something with err +} +``` diff --git a/container.go b/container.go index 628f78b831..cc19399a9e 100644 --- a/container.go +++ b/container.go @@ -38,9 +38,12 @@ type Container interface { Start(context.Context) error // start the container Terminate(context.Context) error // terminate the container Logs(context.Context) (io.ReadCloser, error) // Get logs of the container - Name(context.Context) (string, error) // get container name - Networks(context.Context) ([]string, error) // get container networks - NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network + FollowOutput(LogConsumer) + StartLogProducer(context.Context) error + StopLogProducer() error + Name(context.Context) (string, error) // get container name + Networks(context.Context) ([]string, error) // get container networks + NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network Exec(ctx context.Context, cmd []string) (int, error) } diff --git a/docker.go b/docker.go index 1e01e61c26..d2f7ddbfd5 100644 --- a/docker.go +++ b/docker.go @@ -3,6 +3,7 @@ package testcontainers import ( "bytes" "context" + "encoding/binary" "fmt" "io" "io/ioutil" @@ -43,6 +44,8 @@ type DockerContainer struct { sessionID uuid.UUID terminationSignal chan bool skipReaper bool + consumers []LogConsumer + stopProducer chan bool } func (c *DockerContainer) GetContainerID() string { @@ -192,6 +195,18 @@ func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) { return c.provider.client.ContainerLogs(ctx, c.ID, options) } +// FollowOutput adds a LogConsumer to be sent logs from the container's +// STDOUT and STDERR +func (c *DockerContainer) FollowOutput(consumer LogConsumer) { + if c.consumers == nil { + c.consumers = []LogConsumer{ + consumer, + } + } else { + c.consumers = append(c.consumers, consumer) + } +} + // Name gets the name of the container. func (c *DockerContainer) Name(ctx context.Context) (string, error) { inspect, err := c.inspectContainer(ctx) @@ -272,6 +287,84 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string) (int, error) { return exitCode, nil } +// StartLogProducer will start a concurrent process that will continuously read logs +// from the container and will send them to each added LogConsumer +func (c *DockerContainer) StartLogProducer(ctx context.Context) error { + go func() { + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + } + + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + + r, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options) + if err != nil { + // if we can't get the logs, panic, we can't return an error to anything + // from within this goroutine + panic(err) + } + + for { + select { + case <-c.stopProducer: + err := r.Close() + if err != nil { + // we can't close the read closer, this should never happen + panic(err) + } + return + default: + h := make([]byte, 8) + _, err := r.Read(h) + if err != nil { + // this explicitly ignores errors + // because we want to keep procesing even if one of our reads fails + continue + } + + count := binary.BigEndian.Uint32(h[4:]) + if count == 0 { + continue + } + logType := h[0] + if logType > 2 { + panic(fmt.Sprintf("received inavlid log type: %d", logType)) + } + + // a map of the log type --> int representation in the header, notice the first is blank, this is stdin, but the go docker client doesn't allow following that in logs + logTypes := []string{"", StdoutLog, StderrLog} + + b := make([]byte, count) + _, err = r.Read(b) + if err != nil { + // TODO: add-logger: use logger to log out this error + fmt.Fprintf(os.Stderr, "error occurred reading log with known length %s", err.Error()) + continue + } + for _, c := range c.consumers { + c.Accept(Log{ + LogType: logTypes[logType], + Content: b, + }) + } + } + + } + }() + + return nil +} + +// StopLogProducer will stop the concurrent process that is reading logs +// and sending them to each added LogConsumer +func (c *DockerContainer) StopLogProducer() error { + c.stopProducer <- true + return nil +} + // DockerNetwork represents a network started using Docker type DockerNetwork struct { ID string // Network ID from Docker @@ -486,6 +579,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque provider: p, terminationSignal: termSignal, skipReaper: req.SkipReaper, + stopProducer: make(chan bool), } return c, nil diff --git a/go.mod b/go.mod index 5a1bad5703..44116a2a40 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/docker/docker v0.7.3-0.20190506211059-b20a14b54661 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.3.3 // indirect + github.com/gin-gonic/gin v1.5.0 github.com/go-redis/redis v6.15.7+incompatible github.com/go-sql-driver/mysql v1.5.0 github.com/gogo/protobuf v1.2.0 // indirect @@ -27,11 +28,10 @@ require ( github.com/pkg/errors v0.9.1 github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.2.0 // indirect - golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb // indirect golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect google.golang.org/grpc v1.17.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gotest.tools v0.0.0-20181223230014-1083505acf35 // indirect + gotest.tools v0.0.0-20181223230014-1083505acf35 ) go 1.13 diff --git a/go.sum b/go.sum index 36b3b651cf..31cb282b15 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible h1:dvc1KSkIYTVjZgHf/CTC2diTYC8PzhaA5sFISRfNVrE= @@ -22,6 +23,14 @@ github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= @@ -36,14 +45,18 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -52,6 +65,12 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -80,9 +99,16 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -98,6 +124,8 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= @@ -115,10 +143,15 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v0.0.0-20181223230014-1083505acf35 h1:zpdCK+REwbk+rqjJmHhiCN6iBIigrZ39glqSF0P3KF0= gotest.tools v0.0.0-20181223230014-1083505acf35/go.mod h1:R//lfYlUuTOTfblYI3lGoAAAebUdzjvbmQsuB7Ykd90= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/logconsumer.go b/logconsumer.go new file mode 100644 index 0000000000..c5a2e29cf9 --- /dev/null +++ b/logconsumer.go @@ -0,0 +1,22 @@ +package testcontainers + +// StdoutLog is the log type for STDOUT +const StdoutLog = "STDOUT" + +// StderrLog is the log type for STDERR +const StderrLog = "STDERR" + +// 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 +} + +// 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) +} diff --git a/logconsumer_test.go b/logconsumer_test.go new file mode 100644 index 0000000000..11f636b91d --- /dev/null +++ b/logconsumer_test.go @@ -0,0 +1,175 @@ +package testcontainers + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/testcontainers/testcontainers-go/wait" + "gotest.tools/assert" +) + +const lastMessage = "DONE" + +type TestLogConsumer struct { + Msgs []string + Ack chan bool +} + +func (g *TestLogConsumer) Accept(l Log) { + if string(l.Content) == fmt.Sprintf("echo %s\n", lastMessage) { + g.Ack <- true + return + } + + g.Msgs = append(g.Msgs, string(l.Content)) +} + +func Test_LogConsumerGetsCalled(t *testing.T) { + /* + send one request at a time to a server that will + print whatever was sent in the "echo" parameter, the log + consumer should get all of the messages and append them + to the Msgs slice + */ + + ctx := context.Background() + req := ContainerRequest{ + FromDockerfile: FromDockerfile{ + Context: "./testresources/", + Dockerfile: "echoserver.Dockerfile", + }, + ExposedPorts: []string{"8080/tcp"}, + WaitingFor: wait.ForLog("ready"), + } + + gReq := GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + c, err := GenericContainer(ctx, gReq) + if err != nil { + t.Fatal(err) + } + + ep, err := c.Endpoint(ctx, "http") + if err != nil { + t.Fatal(err) + } + + g := TestLogConsumer{ + Msgs: []string{}, + Ack: make(chan bool), + } + + err = c.StartLogProducer(ctx) + if err != nil { + t.Fatal(err) + } + + c.FollowOutput(&g) + + _, err = http.Get(ep + "/stdout?echo=hello") + if err != nil { + t.Fatal(err) + } + + _, err = http.Get(ep + "/stdout?echo=there") + if err != nil { + t.Fatal(err) + } + + _, err = http.Get(ep + fmt.Sprintf("/stdout?echo=%s", lastMessage)) + if err != nil { + t.Fatal(err) + } + + <-g.Ack + c.StopLogProducer() + + // get rid of the server "ready" log + g.Msgs = g.Msgs[1:] + + assert.DeepEqual(t, []string{"echo hello\n", "echo there\n"}, g.Msgs) + c.Terminate(ctx) +} + +type TestLogTypeConsumer struct { + LogTypes map[string]string + Ack chan bool +} + +func (t *TestLogTypeConsumer) Accept(l Log) { + if string(l.Content) == fmt.Sprintf("echo %s\n", lastMessage) { + t.Ack <- true + return + } + + t.LogTypes[l.LogType] = string(l.Content) +} + +func Test_ShouldRecognizeLogTypes(t *testing.T) { + ctx := context.Background() + req := ContainerRequest{ + FromDockerfile: FromDockerfile{ + Context: "./testresources/", + Dockerfile: "echoserver.Dockerfile", + }, + ExposedPorts: []string{"8080/tcp"}, + WaitingFor: wait.ForLog("ready"), + } + + gReq := GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + c, err := GenericContainer(ctx, gReq) + if err != nil { + t.Fatal(err) + } + + ep, err := c.Endpoint(ctx, "http") + if err != nil { + t.Fatal(err) + } + + g := TestLogTypeConsumer{ + LogTypes: map[string]string{}, + Ack: make(chan bool), + } + + err = c.StartLogProducer(ctx) + if err != nil { + t.Fatal(err) + } + + c.FollowOutput(&g) + + _, err = http.Get(ep + "/stdout?echo=this-is-stdout") + if err != nil { + t.Fatal(err) + } + + _, err = http.Get(ep + "/stderr?echo=this-is-stderr") + if err != nil { + t.Fatal(err) + } + + _, err = http.Get(ep + fmt.Sprintf("/stdout?echo=%s", lastMessage)) + if err != nil { + t.Fatal(err) + } + + <-g.Ack + c.StopLogProducer() + + assert.DeepEqual(t, map[string]string{ + StdoutLog: "echo this-is-stdout\n", + StderrLog: "echo this-is-stderr\n", + }, g.LogTypes) + c.Terminate(ctx) + +} diff --git a/testresources/echoserver.Dockerfile b/testresources/echoserver.Dockerfile new file mode 100644 index 0000000000..2a67eaef3f --- /dev/null +++ b/testresources/echoserver.Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.13-alpine + +WORKDIR /app + +COPY echoserver.go . + +RUN apk add git + +RUN go get -u github.com/gin-gonic/gin + +ENV GIN_MODE=release + +CMD go run echoserver.go diff --git a/testresources/echoserver.go b/testresources/echoserver.go new file mode 100644 index 0000000000..943070aa42 --- /dev/null +++ b/testresources/echoserver.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" +) + +func Log(c *gin.Context, destination *os.File) { + echo := c.Request.URL.Query()["echo"][0] + + l := log.New(destination, "echo ", 0) + + l.Println(echo) + + c.AbortWithStatus(202) + +} + +// a simple server that will echo whatever is in the "echo" parameter to stdout +// in the /ping endpoint +func main() { + r := gin.New() + stop := make(chan bool) + + r.GET("/stdout", func(c *gin.Context) { + Log(c, os.Stdout) + }) + + r.GET("/stderr", func(c *gin.Context) { + Log(c, os.Stderr) + }) + + srv := &http.Server{ + Addr: ":8080", + Handler: r, + } + + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + panic(err) + } + }() + + fmt.Println("ready") + + <-stop +}