Repository: threefoldtech/zos
Pull request #578 (2020-03-16 19:04:41)

container: support user defined logs endpoint
This PR allows user to forward container stdout/stderr to external endpoint. Right now, only redis is supported. You can define on container reservation a new field `logs` which contains information where to push logs:
```
        "logs": [
          {
            "type": "redis",
            "data": {
              "endpoint": "redis://10.241.0.232:6370",
              "channel": "debug"
            }
          }
        ],
```

This endpoint is a reachable redis, and channel is a pub/sub where logs will be sent (one line at a time).

codecov[bot]: # [Codecov](https://codecov.io/gh/threefoldtech/zos/pull/578?src=pr&el=h1) Report
> Merging [#578](https://codecov.io/gh/threefoldtech/zos/pull/578?src=pr&el=desc) into [master](https://codecov.io/gh/threefoldtech/zos/commit/f41cfc241ee8348501fb4f9c49df62f3bf32cab1&el=desc) will **decrease** coverage by `0.25%`.
> The diff coverage is `6.38%`.

[![Impacted file tree graph](https://codecov.io/gh/threefoldtech/zos/pull/578/graphs/tree.svg?width=650&height=150&src=pr&token=f7Vmq1OAEm)](https://codecov.io/gh/threefoldtech/zos/pull/578?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #578      +/-   ##
==========================================
- Coverage   35.99%   35.73%   -0.26%     
==========================================
  Files          71       71              
  Lines        5254     5300      +46     
==========================================
+ Hits         1891     1894       +3     
- Misses       3093     3134      +41     
- Partials      270      272       +2     
```


| [Impacted Files](https://codecov.io/gh/threefoldtech/zos/pull/578?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/container/container.go](https://codecov.io/gh/threefoldtech/zos/pull/578/diff?src=pr&el=tree#diff-cGtnL2NvbnRhaW5lci9jb250YWluZXIuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/provision/converter.go](https://codecov.io/gh/threefoldtech/zos/pull/578/diff?src=pr&el=tree#diff-cGtnL3Byb3Zpc2lvbi9jb252ZXJ0ZXIuZ28=) | `43.71% <6.66%> (-3.66%)` | :arrow_down: |
| [pkg/gedis/converter.go](https://codecov.io/gh/threefoldtech/zos/pull/578/diff?src=pr&el=tree#diff-cGtnL2dlZGlzL2NvbnZlcnRlci5nbw==) | `24.16% <7.69%> (-1.58%)` | :arrow_down: |
| [pkg/provision/container.go](https://codecov.io/gh/threefoldtech/zos/pull/578/diff?src=pr&el=tree#diff-cGtnL3Byb3Zpc2lvbi9jb250YWluZXIuZ28=) | `60.68% <100.00%> (+0.27%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/threefoldtech/zos/pull/578?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/threefoldtech/zos/pull/578?src=pr&el=footer). Last update [f41cfc2...fdde50e](https://codecov.io/gh/threefoldtech/zos/pull/578?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

maxux: Latest implementation use the `MultiWriter` that @zaibon suggested, reducing amount of code and complexity. Thanks :)
maxux: From 0d6d56f commit message:

- Move pkg/logger to pkg/container/logger since logger package are only for container purpose
- Update name of struct inside logger package for shorter names
  - ContainerLoggerConsole -> Console
  - ContainerLoggerFile -> File
  - ContainerLoggerRedis -> Redis
  - NewContainerLoggers -> NewLoggers
  - NewContainerLoggerFile -> NewFile
  - NewContainerLoggerRedis -> NewRedis
  - NewContainerLoggerConsole -> NewConsole
- Update provision to replace endpoint/channel with redis url for stdout and stderr
- Update tfuser to support new provision fields
  - Removing `--logs` and `--channel`
  - Adding `--stdout` and `--stderr` options
- Argument are now always using `redis://host:port/channel` format

Patch:
diff --git a/docs/provision/provision.md b/docs/provision/provision.md
index 078e750a..01651804 100644
--- a/docs/provision/provision.md
+++ b/docs/provision/provision.md
@@ -109,6 +109,8 @@ type Container struct {
 	Network Network `json:"network"`
 	// ContainerCapacity is the amount of resource to allocate to the container
 	Capacity ContainerCapacity `json:"capacity"`
+	// Logs contains a list of endpoint where to send containerlogs
+	Logs []logger.Logs `json:"logs,omitempty"`
 }
 
 // ContainerResult is the information return to the BCDB
@@ -118,6 +120,21 @@ type ContainerResult struct {
 	IPv6 string `json:"ipv6"`
 	IPv4 string `json:"ipv4"`
 }
+
+//
+// Package logger
+//
+// Logs defines a custom (redis for now) backend 
+type Logs struct {
+	Type string    `json:"type"`
+	Data LogsRedis `json:"data"`
+}
+
+// LogsRedis defines how to connect a redis logs backend
+type LogsRedis struct {
+	Endpoint string `json:"endpoint"`
+	Channel  string `json:"channel"`
+}
 ```
 
 Volume provision schema
diff --git a/pkg/container.go b/pkg/container.go
index fda0d697..4d50cf5d 100644
--- a/pkg/container.go
+++ b/pkg/container.go
@@ -3,6 +3,10 @@ package pkg
 //go:generate mkdir -p stubs
 //go:generate zbusc -module container -version 0.0.1 -name container -package stubs github.com/threefoldtech/zos/pkg+ContainerModule stubs/container_stub.go
 
+import (
+	"github.com/threefoldtech/zos/pkg/container/logger"
+)
+
 // ContainerID type
 type ContainerID string
 
@@ -46,6 +50,8 @@ type Container struct {
 	CPU uint
 	// Memory limit in bytes
 	Memory uint64
+	// Logs backends
+	Logs []logger.Logs
 }
 
 // ContainerModule defines rpc interface to containerd
diff --git a/pkg/container/container.go b/pkg/container/container.go
index 43b543a6..86cf789e 100644
--- a/pkg/container/container.go
+++ b/pkg/container/container.go
@@ -19,12 +19,12 @@ import (
 	"github.com/opencontainers/runtime-spec/specs-go"
 
 	"github.com/containerd/containerd"
-	"github.com/containerd/containerd/cio"
 	"github.com/containerd/containerd/namespaces"
 	"github.com/containerd/containerd/oci"
 	"github.com/containerd/containerd/runtime/restart"
 	"github.com/google/shlex"
 	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/container/logger"
 )
 
 const (
@@ -173,7 +173,36 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID
 		return id, err
 	}
 
-	task, err := container.NewTask(ctx, cio.LogFile(path.Join(logs, fmt.Sprintf("%s.log", container.ID()))))
+	loggers := logger.NewLoggers()
+
+	// hardcode local logfile
+	filepath := path.Join(logs, fmt.Sprintf("%s.log", container.ID()))
+	fileout, fileerr, err := logger.NewFile(filepath, filepath)
+	if err != nil {
+		return id, err
+	}
+
+	loggers.Add(fileout, fileerr)
+
+	// set user defined endpoint logging
+	for _, l := range data.Logs {
+		switch l.Type {
+		case logger.RedisType:
+			lo, le, err := logger.NewRedis(l.Data.Stdout, l.Data.Stderr)
+
+			if err != nil {
+				log.Error().Err(err).Msg("redis logger")
+				continue
+			}
+
+			loggers.Add(lo, le)
+
+		default:
+			log.Error().Str("type", l.Type).Msg("invalid logging type requested")
+		}
+	}
+
+	task, err := container.NewTask(ctx, loggers.Log())
 	if err != nil {
 		return id, err
 	}
diff --git a/pkg/container/logger/logger.go b/pkg/container/logger/logger.go
new file mode 100644
index 00000000..21e92ae4
--- /dev/null
+++ b/pkg/container/logger/logger.go
@@ -0,0 +1,16 @@
+package logger
+
+// Logs defines a custom backend with variable settings
+type Logs struct {
+	Type string    `json:"type"`
+	Data LogsRedis `json:"data"`
+}
+
+// LogsRedis defines how to connect a redis logs backend
+type LogsRedis struct {
+	// Stdout is the redis url for stdout (redis://host/channel)
+	Stdout string `json:"stdout"`
+
+	// Stderr is the redis url for stderr (redis://host/channel)
+	Stderr string `json:"stderr"`
+}
diff --git a/pkg/container/logger/logger_console.go b/pkg/container/logger/logger_console.go
new file mode 100644
index 00000000..081c2e64
--- /dev/null
+++ b/pkg/container/logger/logger_console.go
@@ -0,0 +1,45 @@
+package logger
+
+import (
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/rs/zerolog/log"
+)
+
+// ConsoleType defines console logger type name
+const ConsoleType = "console"
+
+// Console does nothing else that print
+// logs on console stdout/stderr, there are no config
+type Console struct {
+	prefix string
+	target *os.File
+}
+
+// NewConsole does nothing, it's here for consistancy
+func NewConsole() (io.Writer, io.Writer, error) {
+	log.Debug().Msg("initializing console logging")
+	stdout := &Console{
+		prefix: "O: ",
+		target: os.Stdout,
+	}
+
+	stderr := &Console{
+		prefix: "E: ",
+		target: os.Stderr,
+	}
+
+	return stdout, stderr, nil
+}
+
+func (c *Console) Write(data []byte) (n int, err error) {
+	fmt.Fprintf(c.target, "%s", c.prefix)
+
+	// note: cannot use Fprintf("%s%s", c.prefix, data)
+	// caller seems to expect that return amount of byte
+	// matches to data length
+
+	return c.target.Write(data)
+}
diff --git a/pkg/container/logger/logger_file.go b/pkg/container/logger/logger_file.go
new file mode 100644
index 00000000..e5d733a6
--- /dev/null
+++ b/pkg/container/logger/logger_file.go
@@ -0,0 +1,51 @@
+package logger
+
+import (
+	"io"
+	"os"
+
+	"github.com/rs/zerolog/log"
+)
+
+// FileType defines file logger type name
+const FileType = "file"
+
+// File write stdout/stderr to files
+type File struct {
+	target *os.File
+}
+
+// NewFile open file and prepare logs writing
+func NewFile(stdout string, stderr string) (io.Writer, io.Writer, error) {
+	log.Debug().Str("stdout", stdout).Str("stderr", stderr).Msg("initializing localfile logging")
+
+	fo, err := os.Create(stdout)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// If stdout and stderr are the same, only one file is open
+	fe := fo
+
+	if stdout != stderr {
+		fe, err = os.Create(stderr)
+		if err != nil {
+			return nil, nil, err
+		}
+	}
+
+	fstdout := &File{
+		target: fo,
+	}
+
+	fstderr := &File{
+		target: fe,
+	}
+
+	return fstdout, fstderr, nil
+}
+
+// Write forwards write to underlaying layer
+func (c *File) Write(data []byte) (n int, err error) {
+	return c.target.Write(data)
+}
diff --git a/pkg/container/logger/logger_redis.go b/pkg/container/logger/logger_redis.go
new file mode 100644
index 00000000..a2de265e
--- /dev/null
+++ b/pkg/container/logger/logger_redis.go
@@ -0,0 +1,104 @@
+package logger
+
+import (
+	"fmt"
+	"github.com/gomodule/redigo/redis"
+	"github.com/rs/zerolog/log"
+	"io"
+	"net/url"
+)
+
+// RedisType defines redis logger type name
+const RedisType = "redis"
+
+// Redis send stdout/stderr to a Redis PubSub channel
+type Redis struct {
+	channel string
+	conn    redis.Conn
+}
+
+// RedisParseURL parse an url and returns interresting part after validation
+func RedisParseURL(address string) (host string, channel string, err error) {
+	u, err := url.Parse(address)
+	if err != nil {
+		return "", "", err
+	}
+
+	if u.Scheme != "redis" {
+		err = fmt.Errorf("invalid scheme, expected redis://")
+		return "", "", err
+	}
+
+	if u.RequestURI() == "/" {
+		err = fmt.Errorf("missing channel name, expected: redis://host/channel")
+		return "", "", err
+	}
+
+	host = u.Host
+
+	if u.Port() == "" {
+		host += ":6379"
+	}
+
+	channel = u.RequestURI()[1:]
+
+	return host, channel, err
+}
+
+// NewRedis create new redis backend and initialize connection
+func NewRedis(stdout string, stderr string) (io.WriteCloser, io.WriteCloser, error) {
+	log.Debug().Msg("initializing redis logging")
+
+	ohost, ochannel, err := RedisParseURL(stdout)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	rhost, rchannel, err := RedisParseURL(stderr)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cout, err := redis.Dial("tcp", ohost)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cerr, err := redis.Dial("tcp", rhost)
+	if err != nil {
+		cout.Close()
+		return nil, nil, err
+	}
+
+	log.Debug().Str("stdout", ohost).Str("channel", ochannel).Msg("redis stdout")
+	log.Debug().Str("stderr", rhost).Str("channel", rchannel).Msg("redis stder")
+
+	rstdout := &Redis{
+		channel: ochannel,
+		conn:    cout,
+	}
+
+	rstderr := &Redis{
+		channel: rchannel,
+		conn:    cerr,
+	}
+
+	return rstdout, rstderr, nil
+}
+
+// Write will write to the channel
+func (c *Redis) Write(data []byte) (int, error) {
+	_, err := c.conn.Do("PUBLISH", c.channel, data)
+	if err != nil {
+		return 0, err
+	}
+
+	return len(data), nil
+}
+
+// Close closes redis connection
+func (c *Redis) Close() error {
+	log.Debug().Str("channel", c.channel).Msg("closing redis backend")
+	c.conn.Close()
+	return nil
+}
diff --git a/pkg/container/logger/loggers.go b/pkg/container/logger/loggers.go
new file mode 100644
index 00000000..3f9b03f1
--- /dev/null
+++ b/pkg/container/logger/loggers.go
@@ -0,0 +1,35 @@
+package logger
+
+import (
+	"io"
+
+	"github.com/containerd/containerd/cio"
+)
+
+// Loggers keeps stdout and stderr backend list
+type Loggers struct {
+	stdouts []io.Writer
+	stderrs []io.Writer
+}
+
+// NewLoggers initialize empty lists
+func NewLoggers() *Loggers {
+	return &Loggers{
+		stdouts: []io.Writer{},
+		stderrs: []io.Writer{},
+	}
+}
+
+// Add adds a defined backend on the list
+func (c *Loggers) Add(stdout io.Writer, stderr io.Writer) {
+	c.stdouts = append(c.stdouts, stdout)
+	c.stderrs = append(c.stderrs, stderr)
+}
+
+// Log create the containers logs redirector
+func (c *Loggers) Log() cio.Creator {
+	mwo := io.MultiWriter(c.stdouts...)
+	mwe := io.MultiWriter(c.stderrs...)
+
+	return cio.NewCreator(cio.WithStreams(nil, mwo, mwe))
+}
diff --git a/pkg/gedis/converter.go b/pkg/gedis/converter.go
index fc3afa9f..f16a7769 100644
--- a/pkg/gedis/converter.go
+++ b/pkg/gedis/converter.go
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/container/logger"
 	types "github.com/threefoldtech/zos/pkg/gedis/types/provision"
 	"github.com/threefoldtech/zos/pkg/provision"
 	"github.com/threefoldtech/zos/pkg/schema"
@@ -129,6 +130,7 @@ func containerReservation(i interface{}, nodeID string) types.TfgridReservationC
 		Entrypoint:        c.Entrypoint,
 		Interactive:       c.Interactive,
 		Volumes:           make([]types.TfgridReservationContainerMount1, len(c.Mounts)),
+		Logs:              make([]types.TfgridReservationLogs1, len(c.Logs)),
 		NetworkConnection: []types.TfgridReservationNetworkConnection1{
 			{
 				NetworkID: string(c.Network.NetworkID),
@@ -146,6 +148,27 @@ func containerReservation(i interface{}, nodeID string) types.TfgridReservationC
 			Mountpoint: v.Mountpoint,
 		}
 	}
+
+	for i, v := range c.Logs {
+		// Only allow redis for now
+		if v.Type != logger.RedisType {
+			container.Logs[i] = types.TfgridReservationLogs1{
+				Type: "invalid",
+				Data: types.TfgridReservationLogsRedis1{},
+			}
+
+			continue
+		}
+
+		container.Logs[i] = types.TfgridReservationLogs1{
+			Type: v.Type,
+			Data: types.TfgridReservationLogsRedis1{
+				Stdout: v.Data.Stdout,
+				Stderr: v.Data.Stderr,
+			},
+		}
+	}
+
 	return container
 }
 
diff --git a/pkg/gedis/types/provision/tfgrid_reservation_container_1.go b/pkg/gedis/types/provision/tfgrid_reservation_container_1.go
index 5d6426eb..06be3005 100644
--- a/pkg/gedis/types/provision/tfgrid_reservation_container_1.go
+++ b/pkg/gedis/types/provision/tfgrid_reservation_container_1.go
@@ -17,6 +17,7 @@ type TfgridReservationContainer1 struct {
 	Volumes           []TfgridReservationContainerMount1    `json:"volumes"`
 	NetworkConnection []TfgridReservationNetworkConnection1 `json:"network_connection"`
 	StatsAggregator   []TfgridReservationStatsaggregator1   `json:"stats_aggregator"`
+	Logs              []TfgridReservationLogs1              `json:"logs"`
 }
 
 //TfgridReservationContainerMount1 jsx schema
@@ -31,3 +32,15 @@ type TfgridReservationNetworkConnection1 struct {
 	Ipaddress net.IP `json:"ipaddress"`
 	PublicIP6 bool   `json:"public_ip6"`
 }
+
+//TfgridReservationLogs1 user defined log description
+type TfgridReservationLogs1 struct {
+	Type string                      `json:"type"`
+	Data TfgridReservationLogsRedis1 `json:"data"`
+}
+
+//TfgridReservationLogsRedis1 redis log backend
+type TfgridReservationLogsRedis1 struct {
+	Stdout string `json:"stdout"`
+	Stderr string `json:"stderr"`
+}
diff --git a/pkg/provision/container.go b/pkg/provision/container.go
index bd88b0a4..801466d2 100644
--- a/pkg/provision/container.go
+++ b/pkg/provision/container.go
@@ -12,6 +12,7 @@ import (
 	"github.com/rs/zerolog/log"
 
 	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/container/logger"
 	"github.com/threefoldtech/zos/pkg/stubs"
 )
 
@@ -51,6 +52,8 @@ type Container struct {
 	Network Network `json:"network"`
 	// ContainerCapacity is the amount of resource to allocate to the container
 	Capacity ContainerCapacity `json:"capacity"`
+	// Logs contains a list of endpoint where to send containerlogs
+	Logs []logger.Logs `json:"logs,omitempty"`
 }
 
 // ContainerResult is the information return to the BCDB
@@ -206,6 +209,7 @@ func containerProvisionImpl(ctx context.Context, reservation *Reservation) (Cont
 			Interactive: config.Interactive,
 			CPU:         config.Capacity.CPU,
 			Memory:      config.Capacity.Memory * 1024 * 1024,
+			Logs:        config.Logs,
 		},
 	)
 	if err != nil {
diff --git a/pkg/provision/converter.go b/pkg/provision/converter.go
index 1e3f296c..c7f297fa 100644
--- a/pkg/provision/converter.go
+++ b/pkg/provision/converter.go
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/container/logger"
 	generated "github.com/threefoldtech/zos/pkg/gedis/types/provision"
 	"github.com/threefoldtech/zos/pkg/network/types"
 )
@@ -21,7 +22,9 @@ func ContainerToProvisionType(c generated.TfgridReservationContainer1) (Containe
 		Entrypoint:   c.Entrypoint,
 		Interactive:  c.Interactive,
 		Mounts:       make([]Mount, len(c.Volumes)),
+		Logs:         make([]logger.Logs, len(c.Logs)),
 	}
+
 	if len(c.NetworkConnection) > 0 {
 		container.Network = Network{
 			IPs:       []net.IP{c.NetworkConnection[0].Ipaddress},
@@ -36,6 +39,27 @@ func ContainerToProvisionType(c generated.TfgridReservationContainer1) (Containe
 		}
 	}
 
+	for i, lg := range c.Logs {
+		// Only support redis for now
+		if lg.Type != logger.RedisType {
+			container.Logs[i] = logger.Logs{
+				Type: "unknown",
+				Data: logger.LogsRedis{
+					Stdout: "",
+					Stderr: "",
+				},
+			}
+		}
+
+		container.Logs[i] = logger.Logs{
+			Type: lg.Type,
+			Data: logger.LogsRedis{
+				Stdout: lg.Data.Stdout,
+				Stderr: lg.Data.Stderr,
+			},
+		}
+	}
+
 	return container, c.NodeID, nil
 }
 
diff --git a/pkg/provision/local_store.go b/pkg/provision/local_store.go
index c1a951e8..3228ee93 100644
--- a/pkg/provision/local_store.go
+++ b/pkg/provision/local_store.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path"
 	"path/filepath"
 	"sync"
 	"sync/atomic"
@@ -121,6 +122,12 @@ func (s *FSStore) removeAllButPersistent(rootPath string) error {
 		if r != nil {
 			return r
 		}
+		// if a file with size 0 is present we can assume its empty and remove it
+		if info.Size() == 0 {
+			log.Warn().Str("filename", info.Name()).Msg("cached reservation %d found, but file is empty, removing.")
+			return os.Remove(path)
+		}
+
 		if info.IsDir() {
 			return nil
 		}
@@ -271,6 +278,14 @@ func (s *FSStore) GetExpired() ([]*Reservation, error) {
 			continue
 		}
 
+		// if the file is empty, remove it and return.
+		if info.Size() == 0 {
+			if info.Size() == 0 {
+				log.Warn().Str("filename", info.Name()).Msg("cached reservation %d found, but file is empty, removing.")
+				return nil, os.Remove(path.Join(s.root, info.Name()))
+			}
+		}
+
 		r, err := s.get(info.Name())
 		if err != nil {
 			return nil, err
diff --git a/tools/bcdb_mock/models/generated/workloads/workloads.go b/tools/bcdb_mock/models/generated/workloads/workloads.go
index 7691b209..79d496b8 100644
--- a/tools/bcdb_mock/models/generated/workloads/workloads.go
+++ b/tools/bcdb_mock/models/generated/workloads/workloads.go
@@ -93,9 +93,20 @@ type TfgridWorkloadsReservationContainer1 struct {
 	Volumes           []TfgridWorkloadsReservationContainerMount1    `bson:"volumes" json:"volumes"`
 	NetworkConnection []TfgridWorkloadsReservationNetworkConnection1 `bson:"network_connection" json:"network_connection"`
 	StatsAggregator   []TfgridWorkloadsReservationStatsaggregator1   `bson:"stats_aggregator" json:"stats_aggregator"`
+	Logs              []TfgridWorkloadsReservationLogs1              `bson:"logs" json:"logs"`
 	FarmerTid         int64                                          `bson:"farmer_tid" json:"farmer_tid"`
 }
 
+type TfgridWorkloadsReservationLogs1 struct {
+	Type string                               `bson:"type" json:"type"`
+	Data TfgridWorkloadsReservationLogsRedis1 `bson:"data" json:"data"`
+}
+
+type TfgridWorkloadsReservationLogsRedis1 struct {
+	Stdout string `bson:"stdout" json:"stdout"`
+	Stderr string `bson:"stderr" json:"stderr"`
+}
+
 func NewTfgridWorkloadsReservationContainer1() (TfgridWorkloadsReservationContainer1, error) {
 	const value = "{\"interactive\": true}"
 	var object TfgridWorkloadsReservationContainer1
diff --git a/tools/bcdb_mock/models/schema/workloads/tfgrid_workloads_reservation_container_1.toml b/tools/bcdb_mock/models/schema/workloads/tfgrid_workloads_reservation_container_1.toml
index 2dd45b7f..f5eec2a5 100644
--- a/tools/bcdb_mock/models/schema/workloads/tfgrid_workloads_reservation_container_1.toml
+++ b/tools/bcdb_mock/models/schema/workloads/tfgrid_workloads_reservation_container_1.toml
@@ -23,7 +23,7 @@ network_connection = (LO) !tfgrid.workloads.reservation.network.connection.1
 stats_aggregator = (LO) !tfgrid.workloads.reservation.statsaggregator.1
 #id of threebot who is the farmer
 farmer_tid = (I)
-
+logs = (LO) !tfgrid.workloads.reservation.container.logs.1
 
 
 @url = tfgrid.workloads.reservation.container.mount.1
@@ -33,3 +33,11 @@ mountpoint = (S)
 @url = tfgrid.workloads.reservation.network.connection.1
 network_id = (S)
 ipaddress = (ipaddress)
+
+@url = tfgrid.workloads.reservation.container.logs.1
+type = (S)
+data = (LO) !tfgrid.workloads.reservation.container.logsredis.1
+
+@url = tfgrid.workloads.reservation.container.logsredis.1
+stdout = (S)
+stderr = (S)
diff --git a/tools/bcdb_mock/models/utils.go b/tools/bcdb_mock/models/utils.go
index 35146c53..dbca8558 100644
--- a/tools/bcdb_mock/models/utils.go
+++ b/tools/bcdb_mock/models/utils.go
@@ -1,6 +1,7 @@
 package models
 
 import (
+	"math"
 	"net/http"
 	"strconv"
 
@@ -23,7 +24,7 @@ func Page(p int64, size ...int64) Pager {
 		ps = size[0]
 	}
 	skip := p * ps
-	return options.Find().SetLimit(ps).SetSkip(skip).SetSort(bson.D{{Key: "_id", Value: -1}})
+	return options.Find().SetLimit(ps).SetSkip(skip).SetSort(bson.D{{Key: "_id", Value: 1}})
 }
 
 // PageFromRequest return page information from the page & size url params
@@ -55,3 +56,8 @@ func PageFromRequest(r *http.Request) Pager {
 
 	return Page(page, size)
 }
+
+// NrPages compute the number of page of a collection
+func NrPages(total, pageSize int64) int64 {
+	return int64(math.Ceil(float64(total) / float64(pageSize)))
+}
diff --git a/tools/bcdb_mock/mw/action.go b/tools/bcdb_mock/mw/action.go
index 33e3d1d4..52e6f945 100644
--- a/tools/bcdb_mock/mw/action.go
+++ b/tools/bcdb_mock/mw/action.go
@@ -12,6 +12,11 @@ import (
 type Response interface {
 	Status() int
 	Err() error
+
+	// header getter
+	Header() http.Header
+	// header setter
+	WithHeader(k, v string) Response
 }
 
 // Action interface
@@ -21,10 +26,20 @@ type Action func(r *http.Request) (interface{}, Response)
 func AsHandlerFunc(a Action) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		object, result := a(r)
+
 		w.Header().Set("Content-Type", "application/json")
+
 		if result == nil {
 			w.WriteHeader(http.StatusOK)
 		} else {
+
+			h := result.Header()
+			for k := range h {
+				for _, v := range h.Values(k) {
+					w.Header().Add(k, v)
+				}
+			}
+
 			w.WriteHeader(result.Status())
 			if err := result.Err(); err != nil {
 				log.Error().Msgf("%s", err.Error())
@@ -45,6 +60,7 @@ func AsHandlerFunc(a Action) http.HandlerFunc {
 type genericResponse struct {
 	status int
 	err    error
+	header http.Header
 }
 
 func (r genericResponse) Status() int {
@@ -55,11 +71,32 @@ func (r genericResponse) Err() error {
 	return r.err
 }
 
+func (r genericResponse) Header() http.Header {
+	if r.header == nil {
+		r.header = http.Header{}
+	}
+	return r.header
+}
+
+func (r genericResponse) WithHeader(k, v string) Response {
+	if r.header == nil {
+		r.header = http.Header{}
+	}
+
+	r.header.Add(k, v)
+	return r
+}
+
 // Created return a created response
 func Created() Response {
 	return genericResponse{status: http.StatusCreated}
 }
 
+// Ok return a ok response
+func Ok() Response {
+	return genericResponse{status: http.StatusOK}
+}
+
 // Error generic error response
 func Error(err error, code ...int) Response {
 	status := http.StatusInternalServerError
diff --git a/tools/bcdb_mock/pkg/directory/farms_handlers.go b/tools/bcdb_mock/pkg/directory/farms_handlers.go
index 98db84ae..68fd865c 100644
--- a/tools/bcdb_mock/pkg/directory/farms_handlers.go
+++ b/tools/bcdb_mock/pkg/directory/farms_handlers.go
@@ -51,12 +51,14 @@ func (s *FarmAPI) listFarm(r *http.Request) (interface{}, mw.Response) {
 		return nil, mw.Error(err, http.StatusBadRequest)
 	}
 
-	farms, err := s.List(r.Context(), db, tid, models.PageFromRequest(r))
+	pager := models.PageFromRequest(r)
+	farms, total, err := s.List(r.Context(), db, tid)
 	if err != nil {
 		return nil, mw.Error(err)
 	}
 
-	return farms, nil
+	pages := fmt.Sprintf("%d", models.NrPages(total, *pager.Limit))
+	return farms, mw.Ok().WithHeader("Pages", pages)
 }
 
 func parseOwnerID(r *http.Request) (tid int64, err error) {
@@ -71,21 +73,6 @@ func parseOwnerID(r *http.Request) (tid int64, err error) {
 	return tid, err
 }
 
-func (s *FarmAPI) cockpitListFarm(r *http.Request) (interface{}, mw.Response) {
-	// TODO: do we need this ?
-	db := mw.Database(r)
-	farms, err := s.List(r.Context(), db, 0)
-	if err != nil {
-		return nil, mw.Error(err)
-	}
-
-	x := struct {
-		Farms []directory.Farm `json:"farms"`
-	}{farms}
-
-	return x, nil
-}
-
 func (s *FarmAPI) getFarm(r *http.Request) (interface{}, mw.Response) {
 	sid := mux.Vars(r)["farm_id"]
 
diff --git a/tools/bcdb_mock/pkg/directory/farms_store.go b/tools/bcdb_mock/pkg/directory/farms_store.go
index fec8c294..5122b9f2 100644
--- a/tools/bcdb_mock/pkg/directory/farms_store.go
+++ b/tools/bcdb_mock/pkg/directory/farms_store.go
@@ -16,7 +16,7 @@ type FarmAPI struct{}
 
 // List farms
 // TODO: add paging arguments
-func (s *FarmAPI) List(ctx context.Context, db *mongo.Database, tid int64, opts ...*options.FindOptions) ([]directory.Farm, error) {
+func (s *FarmAPI) List(ctx context.Context, db *mongo.Database, tid int64, opts ...*options.FindOptions) ([]directory.Farm, int64, error) {
 	var filter directory.FarmFilter
 
 	if tid != 0 {
@@ -26,15 +26,20 @@ func (s *FarmAPI) List(ctx context.Context, db *mongo.Database, tid int64, opts
 
 	cur, err := filter.Find(ctx, db, opts...)
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to list farms")
+		return nil, 0, errors.Wrap(err, "failed to list farms")
 	}
 	defer cur.Close(ctx)
 	out := []directory.Farm{}
 	if err := cur.All(ctx, &out); err != nil {
-		return nil, errors.Wrap(err, "failed to load farm list")
+		return nil, 0, errors.Wrap(err, "failed to load farm list")
 	}
 
-	return out, nil
+	count, err := filter.Count(ctx, db)
+	if err != nil {
+		return nil, 0, errors.Wrap(err, "failed to count entries in farms collection")
+	}
+
+	return out, count, nil
 }
 
 // GetByName gets a farm by name
diff --git a/tools/bcdb_mock/pkg/directory/node_handlers.go b/tools/bcdb_mock/pkg/directory/node_handlers.go
index 6309a5ea..0b8e84a4 100644
--- a/tools/bcdb_mock/pkg/directory/node_handlers.go
+++ b/tools/bcdb_mock/pkg/directory/node_handlers.go
@@ -2,6 +2,7 @@ package directory
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 
 	"github.com/rs/zerolog/log"
@@ -62,32 +63,14 @@ func (s *NodeAPI) listNodes(r *http.Request) (interface{}, mw.Response) {
 	}
 
 	db := mw.Database(r)
-	nodes, err := s.List(r.Context(), db, q, models.PageFromRequest(r))
+	pager := models.PageFromRequest(r)
+	nodes, total, err := s.List(r.Context(), db, q, pager)
 	if err != nil {
 		return nil, mw.Error(err)
 	}
 
-	return nodes, nil
-}
-
-func (s *NodeAPI) cockpitListNodes(r *http.Request) (interface{}, mw.Response) {
-	q := nodeQuery{}
-	if err := q.Parse(r); err != nil {
-		return nil, err
-	}
-
-	db := mw.Database(r)
-
-	nodes, err := s.List(r.Context(), db, q, models.PageFromRequest(r))
-	if err != nil {
-		return nil, mw.Error(err)
-	}
-
-	x := struct {
-		Node []directory.Node `json:"nodes"`
-	}{nodes}
-
-	return x, nil
+	pages := fmt.Sprintf("%d", models.NrPages(total, *pager.Limit))
+	return nodes, mw.Ok().WithHeader("Pages", pages)
 }
 
 func (s *NodeAPI) registerCapacity(r *http.Request) (interface{}, mw.Response) {
diff --git a/tools/bcdb_mock/pkg/directory/nodes_store.go b/tools/bcdb_mock/pkg/directory/nodes_store.go
index ba912119..5aba4a5a 100644
--- a/tools/bcdb_mock/pkg/directory/nodes_store.go
+++ b/tools/bcdb_mock/pkg/directory/nodes_store.go
@@ -76,7 +76,7 @@ func queryInt(r *http.Request, q string) (int64, error) {
 
 // List farms
 // TODO: add paging arguments
-func (s *NodeAPI) List(ctx context.Context, db *mongo.Database, q nodeQuery, opts ...*options.FindOptions) ([]directory.Node, error) {
+func (s *NodeAPI) List(ctx context.Context, db *mongo.Database, q nodeQuery, opts ...*options.FindOptions) ([]directory.Node, int64, error) {
 	var filter directory.NodeFilter
 	if q.FarmID > 0 {
 		filter = filter.WithFarmID(schema.ID(q.FarmID))
@@ -93,15 +93,21 @@ func (s *NodeAPI) List(ctx context.Context, db *mongo.Database, q nodeQuery, opt
 
 	cur, err := filter.Find(ctx, db, opts...)
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to list nodes")
+		return nil, 0, errors.Wrap(err, "failed to list nodes")
 	}
+
 	defer cur.Close(ctx)
 	out := []directory.Node{}
 	if err := cur.All(ctx, &out); err != nil {
-		return nil, errors.Wrap(err, "failed to load node list")
+		return nil, 0, errors.Wrap(err, "failed to load node list")
 	}
 
-	return out, nil
+	count, err := filter.Count(ctx, db)
+	if err != nil {
+		return nil, 0, errors.Wrap(err, "failed to count entries in nodes collection")
+	}
+
+	return out, count, nil
 }
 
 // Get a single node
@@ -124,6 +130,11 @@ func (s *NodeAPI) Exists(ctx context.Context, db *mongo.Database, nodeID string)
 	return count > 0, nil
 }
 
+// Count counts the number of document in the collection
+func (s *NodeAPI) Count(ctx context.Context, db *mongo.Database, filter directory.NodeFilter) (int64, error) {
+	return filter.Count(ctx, db)
+}
+
 // Add a node to the store
 func (s *NodeAPI) Add(ctx context.Context, db *mongo.Database, node directory.Node) (schema.ID, error) {
 	return directory.NodeCreate(ctx, db, node)
diff --git a/tools/bcdb_mock/pkg/directory/setup.go b/tools/bcdb_mock/pkg/directory/setup.go
index ec9b272a..5da630b3 100644
--- a/tools/bcdb_mock/pkg/directory/setup.go
+++ b/tools/bcdb_mock/pkg/directory/setup.go
@@ -21,8 +21,6 @@ func Setup(parent *mux.Router, db *mongo.Database) error {
 	farms.HandleFunc("", mw.AsHandlerFunc(farmAPI.registerFarm)).Methods("POST")
 	farms.HandleFunc("", mw.AsHandlerFunc(farmAPI.listFarm)).Methods("GET")
 	farms.HandleFunc("/{farm_id}", mw.AsHandlerFunc(farmAPI.getFarm)).Methods("GET")
-	// compatibility with gedis_http
-	farms.HandleFunc("/list", mw.AsHandlerFunc(farmAPI.cockpitListFarm)).Methods("POST")
 
 	var nodeAPI NodeAPI
 	nodes := parent.PathPrefix("/nodes").Subrouter()
@@ -36,8 +34,5 @@ func Setup(parent *mux.Router, db *mongo.Database) error {
 	nodes.HandleFunc("/{node_id}/uptime", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.updateUptimeHandler))).Methods("POST")
 	nodes.HandleFunc("/{node_id}/used_resources", mw.AsHandlerFunc(nodeAPI.Requires("node_id", nodeAPI.updateReservedResources))).Methods("POST")
 
-	// compatibility with gedis_http
-	nodes.HandleFunc("/list", mw.AsHandlerFunc(nodeAPI.cockpitListNodes)).Methods("POST")
-
 	return nil
 }
diff --git a/tools/bcdb_mock/pkg/directory/types/farm.go b/tools/bcdb_mock/pkg/directory/types/farm.go
index 61eba1d9..e714de4a 100644
--- a/tools/bcdb_mock/pkg/directory/types/farm.go
+++ b/tools/bcdb_mock/pkg/directory/types/farm.go
@@ -69,6 +69,16 @@ func (f FarmFilter) Find(ctx context.Context, db *mongo.Database, opts ...*optio
 	return col.Find(ctx, f, opts...)
 }
 
+// Count number of documents matching
+func (f FarmFilter) Count(ctx context.Context, db *mongo.Database) (int64, error) {
+	col := db.Collection(NodeCollection)
+	if f == nil {
+		f = FarmFilter{}
+	}
+
+	return col.CountDocuments(ctx, f)
+}
+
 // Get one farm that matches the filter
 func (f FarmFilter) Get(ctx context.Context, db *mongo.Database) (farm Farm, err error) {
 	if f == nil {
diff --git a/tools/bcdb_mock/pkg/phonebook/types/user.go b/tools/bcdb_mock/pkg/phonebook/types/user.go
index 67251c0e..b4eb4754 100644
--- a/tools/bcdb_mock/pkg/phonebook/types/user.go
+++ b/tools/bcdb_mock/pkg/phonebook/types/user.go
@@ -88,6 +88,16 @@ func (f UserFilter) Find(ctx context.Context, db *mongo.Database, opts ...*optio
 	return db.Collection(UserCollection).Find(ctx, f, opts...)
 }
 
+// Count number of documents matching
+func (f UserFilter) Count(ctx context.Context, db *mongo.Database) (int64, error) {
+	col := db.Collection(UserCollection)
+	if f == nil {
+		f = UserFilter{}
+	}
+
+	return col.CountDocuments(ctx, f)
+}
+
 // Get single user
 func (f UserFilter) Get(ctx context.Context, db *mongo.Database) (user User, err error) {
 	if f == nil {
diff --git a/tools/bcdb_mock/pkg/phonebook/user.go b/tools/bcdb_mock/pkg/phonebook/user.go
index 00eaa748..edd5549f 100644
--- a/tools/bcdb_mock/pkg/phonebook/user.go
+++ b/tools/bcdb_mock/pkg/phonebook/user.go
@@ -5,6 +5,7 @@ import (
 	"encoding/hex"
 	"encoding/json"
 	"fmt"
+	"math"
 	"net/http"
 	"strconv"
 
@@ -89,7 +90,8 @@ func (u *UserAPI) list(r *http.Request) (interface{}, mw.Response) {
 	filter = filter.WithEmail(r.FormValue("email"))
 
 	db := mw.Database(r)
-	cur, err := filter.Find(r.Context(), db, models.PageFromRequest(r))
+	pager := models.PageFromRequest(r)
+	cur, err := filter.Find(r.Context(), db, pager)
 	if err != nil {
 		return nil, mw.Error(err)
 	}
@@ -99,7 +101,15 @@ func (u *UserAPI) list(r *http.Request) (interface{}, mw.Response) {
 		return nil, mw.Error(err)
 	}
 
-	return users, nil
+	total, err := filter.Count(r.Context(), db)
+	if err != nil {
+		return nil, mw.Error(err, http.StatusInternalServerError)
+	}
+
+	nrPages := math.Ceil(float64(total) / float64(*pager.Limit))
+	pages := fmt.Sprintf("%d", int64(nrPages))
+
+	return users, mw.Ok().WithHeader("Pages", pages)
 }
 
 func (u *UserAPI) parseID(id string) (schema.ID, error) {
diff --git a/tools/bcdb_mock/pkg/workloads/reservation.go b/tools/bcdb_mock/pkg/workloads/reservation.go
index 82bcf8fd..31006cfb 100644
--- a/tools/bcdb_mock/pkg/workloads/reservation.go
+++ b/tools/bcdb_mock/pkg/workloads/reservation.go
@@ -128,13 +128,19 @@ func (a *API) list(r *http.Request) (interface{}, mw.Response) {
 	var filter types.ReservationFilter
 
 	db := mw.Database(r)
-	cur, err := filter.Find(r.Context(), db, models.PageFromRequest(r))
+	pager := models.PageFromRequest(r)
+	cur, err := filter.Find(r.Context(), db, pager)
 	if err != nil {
 		return nil, mw.Error(err)
 	}
 
 	defer cur.Close(r.Context())
 
+	total, err := filter.Count(r.Context(), db)
+	if err != nil {
+		return nil, mw.Error(err)
+	}
+
 	reservations := []types.Reservation{}
 
 	for cur.Next(r.Context()) {
@@ -152,7 +158,8 @@ func (a *API) list(r *http.Request) (interface{}, mw.Response) {
 		reservations = append(reservations, reservation)
 	}
 
-	return reservations, nil
+	pages := fmt.Sprintf("%d", models.NrPages(total, *pager.Limit))
+	return reservations, mw.Ok().WithHeader("Pages", pages)
 }
 
 func (a *API) queued(ctx context.Context, db *mongo.Database, nodeID string, limit int64) ([]types.Workload, error) {
diff --git a/tools/bcdb_mock/pkg/workloads/types/reservation.go b/tools/bcdb_mock/pkg/workloads/types/reservation.go
index 360f20a6..5c062600 100644
--- a/tools/bcdb_mock/pkg/workloads/types/reservation.go
+++ b/tools/bcdb_mock/pkg/workloads/types/reservation.go
@@ -115,6 +115,16 @@ func (f ReservationFilter) Find(ctx context.Context, db *mongo.Database, opts ..
 	return db.Collection(ReservationCollection).Find(ctx, f, opts...)
 }
 
+// Count number of documents matching
+func (f ReservationFilter) Count(ctx context.Context, db *mongo.Database) (int64, error) {
+	col := db.Collection(ReservationCollection)
+	if f == nil {
+		f = ReservationFilter{}
+	}
+
+	return col.CountDocuments(ctx, f)
+}
+
 // Reservation is a wrapper around generated type
 type Reservation generated.TfgridWorkloadsReservation1
 
diff --git a/tools/tfuser/cmds_container.go b/tools/tfuser/cmds_container.go
index a82ceb2f..cb685203 100644
--- a/tools/tfuser/cmds_container.go
+++ b/tools/tfuser/cmds_container.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/threefoldtech/zos/pkg"
 
+	"github.com/threefoldtech/zos/pkg/container/logger"
 	"github.com/threefoldtech/zos/pkg/provision"
 	"github.com/urfave/cli"
 )
@@ -28,6 +29,39 @@ func generateContainer(c *cli.Context) error {
 		Memory: c.Uint64("memory"),
 	}
 
+	var logs []logger.Logs
+	if lo := c.String("stdout"); lo != "" {
+		// validating stdout argument
+		_, _, err := logger.RedisParseURL(lo)
+		if err != nil {
+			return err
+		}
+
+		// copy stdout to stderr
+		lr := lo
+
+		// check if stderr is specified
+		if nlr := c.String("stderr"); nlr != "" {
+			// validating stderr argument
+			_, _, err := logger.RedisParseURL(nlr)
+			if err != nil {
+				return nil
+			}
+
+			lr = nlr
+		}
+
+		lg := logger.Logs{
+			Type: "redis",
+			Data: logger.LogsRedis{
+				Stdout: lo,
+				Stderr: lr,
+			},
+		}
+
+		logs = append(logs, lg)
+	}
+
 	container := provision.Container{
 		FList:        c.String("flist"),
 		FlistStorage: c.String("storage"),
@@ -43,6 +77,7 @@ func generateContainer(c *cli.Context) error {
 			PublicIP6: c.Bool("public6"),
 		},
 		Capacity: cap,
+		Logs:     logs,
 	}
 
 	if err := validateContainer(container); err != nil {
diff --git a/tools/tfuser/main.go b/tools/tfuser/main.go
index 765a0d71..4cc494f5 100644
--- a/tools/tfuser/main.go
+++ b/tools/tfuser/main.go
@@ -244,6 +244,14 @@ func main() {
 							Name:  "public6",
 							Usage: "when enabled, the container will have a public IPv6 interface",
 						},
+						cli.StringFlag{
+							Name:  "stdout",
+							Usage: "custom redis url to send process stdout (eg: redis://example.tld:1234/channel)",
+						},
+						cli.StringFlag{
+							Name:  "stderr",
+							Usage: "like stdout but for stderr, if only stdout is set, stderr will use the same",
+						},
 					},
 					Action: generateContainer,
 				},


Test patch:
diff --git a/pkg/gedis/commands_provision_test.go b/pkg/gedis/commands_provision_test.go
index 433366ea..fe96f77a 100644
--- a/pkg/gedis/commands_provision_test.go
+++ b/pkg/gedis/commands_provision_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/container/logger"
 	types "github.com/threefoldtech/zos/pkg/gedis/types/provision"
 	"github.com/threefoldtech/zos/pkg/provision"
 	"github.com/threefoldtech/zos/pkg/schema"
@@ -177,6 +178,7 @@ func TestProvisionReserve(t *testing.T) {
 				NetworkID: "123",
 				IPs:       []net.IP{net.ParseIP("192.168.1.1")},
 			},
+			Logs: []logger.Logs{},
 		})),
 		Duration:  10 * time.Minute,
 		Signature: []byte("signature"),
@@ -197,6 +199,7 @@ func TestProvisionReserve(t *testing.T) {
 						{NetworkID: "123", Ipaddress: net.ParseIP("192.168.1.1")},
 					},
 					Volumes: []types.TfgridReservationContainerMount1{},
+					Logs:    []types.TfgridReservationLogs1{},
 				},
 			},
 		},
diff --git a/pkg/provision/converter_test.go b/pkg/provision/converter_test.go
index 7c31ac33..69f7c08a 100644
--- a/pkg/provision/converter_test.go
+++ b/pkg/provision/converter_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/threefoldtech/zos/pkg"
 
 	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zos/pkg/container/logger"
 	generated "github.com/threefoldtech/zos/pkg/gedis/types/provision"
 	"github.com/threefoldtech/zos/pkg/provision"
 	schema "github.com/threefoldtech/zos/pkg/schema"
@@ -72,6 +73,7 @@ func TestTfgridReservationContainer1_ToProvisionType(t *testing.T) {
 				Interactive:  false,
 				Mounts:       []provision.Mount{},
 				Network:      provision.Network{},
+				Logs:         []logger.Logs{},
 			},
 			wantErr: false,
 		},
@@ -123,6 +125,7 @@ func TestTfgridReservationContainer1_ToProvisionType(t *testing.T) {
 					NetworkID: "net1",
 					IPs:       []net.IP{net.ParseIP("10.0.0.1")},
 				},
+				Logs: []logger.Logs{},
 			},
 			wantErr: false,
 		},