Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement journald logging driver #1062

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -473,11 +473,13 @@ Metadata flags:
- :nerd_face: `--pidfile`: file path to write the task's pid. The CLI syntax conforms to Podman convention.

Logging flags:
- :whale: `--log-driver=(json-file)`: Logging driver for the container (default `json-file`).
- :whale: `--log-driver=(json-file|journald)`: Logging driver for the container (default `json-file`).
fahedouch marked this conversation as resolved.
Show resolved Hide resolved
- :whale: `--log-driver=json-file`: The logs are formatted as JSON. The default logging driver for nerdctl.
- The `json-file` logging driver supports the following logging options:
- :whale: `--log-opt=max-size=<MAX-SIZE>`: The maximum size of the log before it is rolled. A positive integer plus a modifier representing the unit of measure (k, m, or g). Defaults to unlimited.
- :whale: `--log-opt=max-file=<MAX-FILE>`: The maximum number of log files that can be present. If rolling the logs creates excess files, the oldest file is removed. Only effective when `max-size` is also set. A positive integer. Defaults to 1.
- :whale: `--log-driver=journald`: Writes log messages to `journald`. The `journald` daemon must be running on the host machine.
- :whale: `--log-opt=tag=<TEMPLATE>`: Specify template to set `SYSLOG_IDENTIFIER` value in journald logs.

Shared memory flags:
- :whale: `--shm-size`: Size of `/dev/shm`
Expand Down
154 changes: 110 additions & 44 deletions cmd/nerdctl/logs.go
Expand Up @@ -18,15 +18,20 @@ package main

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"time"

"github.com/containerd/containerd"
"github.com/containerd/nerdctl/pkg/idutil/containerwalker"
"github.com/containerd/nerdctl/pkg/labels"
"github.com/containerd/nerdctl/pkg/logging"
"github.com/containerd/nerdctl/pkg/logging/jsonfile"

timetypes "github.com/docker/docker/api/types/time"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -76,80 +81,131 @@ func logsAction(cmd *cobra.Command, args []string) error {
if found.MatchCount > 1 {
return fmt.Errorf("ambiguous ID %q", found.Req)
}
logJSONFilePath := jsonfile.Path(dataStore, ns, found.Container.ID())
if _, err := os.Stat(logJSONFilePath); err != nil {
return fmt.Errorf("failed to open %q, container is not created with `nerdctl run -d`?: %w", logJSONFilePath, err)
follow, err := cmd.Flags().GetBool("follow")
if err != nil {
return err
}
task, err := found.Container.Task(ctx, nil)
tail, err := cmd.Flags().GetString("tail")
if err != nil {
return err
}
status, err := task.Status(ctx)
timestamps, err := cmd.Flags().GetBool("timestamps")
if err != nil {
return err
}
var reader io.Reader
var execCmd *exec.Cmd
//chan for non-follow tail to check the logsEOF
logsEOFChan := make(chan struct{})
follow, err := cmd.Flags().GetBool("follow")
since, err := cmd.Flags().GetString("since")
if err != nil {
return err
}
tail, err := cmd.Flags().GetString("tail")
until, err := cmd.Flags().GetString("until")
if err != nil {
return err
}
l, err := found.Container.Labels(ctx)
if err != nil {
return err
}
logConfigFilePath := logging.LogConfigFilePath(dataStore, l[labels.Namespace], found.Container.ID())
var logConfig logging.LogConfig
logConfigFileB, err := os.ReadFile(logConfigFilePath)
if err != nil {
return err
}
if follow && status.Status == containerd.Running {
waitCh, err := task.Wait(ctx)
if err = json.Unmarshal(logConfigFileB, &logConfig); err != nil {
return err
}
switch logConfig.Driver {
case "json-file":
logJSONFilePath := jsonfile.Path(dataStore, ns, found.Container.ID())
if _, err := os.Stat(logJSONFilePath); err != nil {
return fmt.Errorf("failed to open %q, container is not created with `nerdctl run -d`?: %w", logJSONFilePath, err)
}
task, err := found.Container.Task(ctx, nil)
if err != nil {
return err
}
reader, execCmd, err = newTailReader(ctx, task, logJSONFilePath, follow, tail)
status, err := task.Status(ctx)
if err != nil {
return err
}

go func() {
<-waitCh
execCmd.Process.Kill()
}()
} else {
if tail != "" {
reader, execCmd, err = newTailReader(ctx, task, logJSONFilePath, false, tail)
var reader io.Reader
var execCmd *exec.Cmd
//chan for non-follow tail to check the logsEOF
logsEOFChan := make(chan struct{})
if follow && status.Status == containerd.Running {
waitCh, err := task.Wait(ctx)
if err != nil {
return err
}
reader, execCmd, err = newTailReader(ctx, task, logJSONFilePath, follow, tail)
if err != nil {
return err
}

go func() {
<-logsEOFChan
<-waitCh
execCmd.Process.Kill()
}()

} else {
f, err := os.Open(logJSONFilePath)
if tail != "" {
reader, execCmd, err = newTailReader(ctx, task, logJSONFilePath, false, tail)
if err != nil {
return err
}
go func() {
<-logsEOFChan
execCmd.Process.Kill()
}()

} else {
f, err := os.Open(logJSONFilePath)
if err != nil {
return err
}
defer f.Close()
reader = f
go func() {
<-logsEOFChan
}()
}
}
return jsonfile.Decode(os.Stdout, os.Stderr, reader, timestamps, since, until, logsEOFChan)
case "journald":
shortID := found.Container.ID()[:12]
var journalctlArgs = []string{fmt.Sprintf("SYSLOG_IDENTIFIER=%s", shortID), "--output=cat"}
if follow {
journalctlArgs = append(journalctlArgs, "-f")
}
if since != "" {
// using GetTimestamp from moby to keep time format consistency
ts, err := timetypes.GetTimestamp(since, time.Now())
if err != nil {
return fmt.Errorf("invalid value for \"since\": %w", err)
}
date, err := prepareJournalCtlDate(ts)
if err != nil {
return err
}
defer f.Close()
reader = f
go func() {
<-logsEOFChan
}()
journalctlArgs = append(journalctlArgs, "--since", date)
}
if timestamps {
logrus.Warnf("unsupported timestamps option for jounrald driver")
}
if until != "" {
// using GetTimestamp from moby to keep time format consistency
ts, err := timetypes.GetTimestamp(until, time.Now())
if err != nil {
return fmt.Errorf("invalid value for \"until\": %w", err)
}
date, err := prepareJournalCtlDate(ts)
if err != nil {
return err
}
journalctlArgs = append(journalctlArgs, "--until", date)
}
return logging.FetchLogs(journalctlArgs)
}
timestamps, err := cmd.Flags().GetBool("timestamps")
if err != nil {
return err
}
since, err := cmd.Flags().GetString("since")
if err != nil {
return err
}
until, err := cmd.Flags().GetString("until")
if err != nil {
return err
}
return jsonfile.Decode(os.Stdout, os.Stderr, reader, timestamps, since, until, logsEOFChan)
return nil
},
}
req := args[0]
Expand Down Expand Up @@ -198,3 +254,13 @@ func newTailReader(ctx context.Context, task containerd.Task, filePath string, f
}
return r, cmd, nil
}

func prepareJournalCtlDate(t string) (string, error) {
i, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return "", err
}
tm := time.Unix(i, 0)
s := tm.Format("2006-01-02 15:04:05")
return s, nil
}
31 changes: 31 additions & 0 deletions cmd/nerdctl/logs_test.go
Expand Up @@ -19,6 +19,7 @@ package main
import (
"fmt"
"runtime"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -65,6 +66,36 @@ bar`
base.Cmd("rm", "-f", containerName).AssertOK()
}

func TestLogsOfJournaldDriver(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("`nerdctl logs` is not implemented on Windows (why?)")
}
base := testutil.NewBase(t)
containerName := testutil.Identifier(t)

defer base.Cmd("rm", containerName).Run()
base.Cmd("run", "-d", "--network", "none", "--log-driver", "journald", "--name", containerName, testutil.CommonImage,
"sh", "-euxc", "echo foo; echo bar").AssertOK()

time.Sleep(3 * time.Second)
base.Cmd("logs", containerName).AssertOutContains("bar")
// Run logs twice, make sure that the logs are not removed
base.Cmd("logs", containerName).AssertOutContains("foo")

base.Cmd("logs", "--since", "5s", containerName).AssertOutWithFunc(func(stdout string) error {
if !strings.Contains(stdout, "bar") {
return fmt.Errorf("expected bar, got %s", stdout)
}
if !strings.Contains(stdout, "foo") {
return fmt.Errorf("expected foo, got %s", stdout)
}
return nil
})

base.Cmd("rm", "-f", containerName).AssertOK()
}

func TestLogsWithFailingContainer(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
Expand Down
11 changes: 5 additions & 6 deletions cmd/nerdctl/run.go
Expand Up @@ -230,6 +230,9 @@ func setCreateFlags(cmd *cobra.Command) {
// #region logging flags
// log-opt needs to be StringArray, not StringSlice, to prevent "env=os,customer" from being split to {"env=os", "customer"}
cmd.Flags().String("log-driver", "json-file", "Logging driver for the container")
cmd.RegisterFlagCompletionFunc("log-driver", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json-file", "journald"}, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().StringArray("log-opt", nil, "Log driver options")
// #endregion

Expand Down Expand Up @@ -479,12 +482,8 @@ func createContainer(cmd *cobra.Command, ctx context.Context, client *containerd
return nil, "", nil, err
}
logConfig := &logging.LogConfig{
Drivers: []logging.LogDriverConfig{
{
Driver: logDriver,
Opts: logOptMap,
},
},
Driver: logDriver,
Opts: logOptMap,
}
logConfigB, err := json.Marshal(logConfig)
if err != nil {
Expand Down
66 changes: 63 additions & 3 deletions cmd/nerdctl/run_test.go
Expand Up @@ -20,15 +20,17 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"

"github.com/containerd/nerdctl/pkg/testutil"

"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)

func TestRunEntrypointWithBuild(t *testing.T) {
Expand Down Expand Up @@ -214,9 +216,9 @@ func TestRunStdin(t *testing.T) {
base.Cmd("run", "--rm", "-i", testutil.CommonImage, "cat").CmdOption(opts...).AssertOutExactly(testStr)
}

func TestRunWithLogOpt(t *testing.T) {
func TestRunWithJsonFileLogDriver(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("`logging options` are not yet implemented on Windows")
t.Skip("json-file log driver is not yet implemented on Windows")
}
base := testutil.NewBase(t)
containerName := testutil.Identifier(t)
Expand Down Expand Up @@ -245,3 +247,61 @@ func TestRunWithLogOpt(t *testing.T) {
}
}
}

func TestRunWithJournaldLogDriver(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("journald log driver is not yet implemented on Windows")
}
base := testutil.NewBase(t)
containerName := testutil.Identifier(t)

defer base.Cmd("rm", "-f", containerName).AssertOK()
base.Cmd("run", "-d", "--log-driver", "journald", "--name", containerName, testutil.CommonImage,
"sh", "-euxc", "echo foo; echo bar").AssertOK()

time.Sleep(3 * time.Second)
journalctl, err := exec.LookPath("journalctl")
assert.NilError(t, err)
inspectedContainer := base.InspectContainer(containerName)
found := 0
check := func(log poll.LogT) poll.Result {
res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID[:12])))
assert.Equal(t, 0, res.ExitCode, res.Combined())
if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") {
found = 1
return poll.Success()
}
return poll.Continue("reading from journald is not yet finished")
}
poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second))
assert.Equal(t, 1, found)
}

func TestRunWithJournaldLogDriverAndLogOpt(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("journald log driver is not yet implemented on Windows")
}
base := testutil.NewBase(t)
containerName := testutil.Identifier(t)

defer base.Cmd("rm", "-f", containerName).AssertOK()
base.Cmd("run", "-d", "--log-driver", "journald", "--log-opt", "tag={{.FullID}}", "--name", containerName, testutil.CommonImage,
"sh", "-euxc", "echo foo; echo bar").AssertOK()

time.Sleep(3 * time.Second)
journalctl, err := exec.LookPath("journalctl")
assert.NilError(t, err)
inspectedContainer := base.InspectContainer(containerName)
found := 0
check := func(log poll.LogT) poll.Result {
res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID)))
assert.Equal(t, 0, res.ExitCode, res.Combined())
if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") {
found = 1
return poll.Success()
}
return poll.Continue("reading from journald is not yet finished")
}
poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second))
assert.Equal(t, 1, found)
}