Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
logs: support --follow
Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Mar 15, 2021
commit 29529931b6fb827a85ab92c7ad64440d237074ab
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@ Fetch the logs of a container.

:warning: Currently, only containers created with `nerdctl run -d` are supported.

Flags:
- :whale: `--f, --follow`: Follow log output

### :whale: nerdctl port
List port mappings or a specific mapping for the container.

Expand Down
59 changes: 55 additions & 4 deletions logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ package main

import (
"context"
"io"
"os"
"os/exec"

"github.com/AkihiroSuda/nerdctl/pkg/idutil/containerwalker"
"github.com/AkihiroSuda/nerdctl/pkg/logging/jsonfile"
"github.com/containerd/containerd"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
Expand All @@ -34,6 +37,13 @@ var logsCommand = &cli.Command{
ArgsUsage: "[flags] CONTAINER",
Action: logsAction,
BashComplete: logsBashComplete,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "follow",
Aliases: []string{"f"},
Usage: "Follow log output",
},
},
}

func logsAction(clicontext *cli.Context) error {
Expand Down Expand Up @@ -65,12 +75,32 @@ func logsAction(clicontext *cli.Context) error {
return errors.Errorf("ambiguous ID %q", found.Req)
}
logJSONFilePath := jsonfile.Path(dataStore, ns, found.Container.ID())
f, err := os.Open(logJSONFilePath)
if err != nil {
if _, err := os.Stat(logJSONFilePath); err != nil {
return errors.Wrapf(err, "failed to open %q, container is not created with `nerdctl run -d`?", logJSONFilePath)
}
defer f.Close()
return jsonfile.Decode(clicontext.App.Writer, clicontext.App.ErrWriter, f)
task, err := found.Container.Task(ctx, nil)
if err != nil {
return err
}
status, err := task.Status(ctx)
if err != nil {
return err
}
var reader io.Reader
if clicontext.Bool("follow") && status.Status == containerd.Running {
reader, err = newTailReader(ctx, task, logJSONFilePath)
if err != nil {
return err
}
} else {
f, err := os.Open(logJSONFilePath)
if err != nil {
return err
}
defer f.Close()
reader = f
}
return jsonfile.Decode(clicontext.App.Writer, clicontext.App.ErrWriter, reader)
},
}
req := clicontext.Args().First()
Expand All @@ -92,3 +122,24 @@ func logsBashComplete(clicontext *cli.Context) {
// show container names (TODO: only show containers with logs)
bashCompleteContainerNames(clicontext)
}

func newTailReader(ctx context.Context, task containerd.Task, filePath string) (io.Reader, error) {
cmd := exec.CommandContext(ctx, "tail", "-f", "-n", "+0", filePath)
cmd.Stderr = os.Stderr
r, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
waitCh, err := task.Wait(ctx)
if err != nil {
return nil, err
}
go func() {
<-waitCh
cmd.Process.Kill()
}()
return r, nil
}
49 changes: 49 additions & 0 deletions logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
Copyright (C) nerdctl authors.
Copyright (C) containerd authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"testing"

"github.com/AkihiroSuda/nerdctl/pkg/testutil"
)

func TestLogs(t *testing.T) {
base := testutil.NewBase(t)
const containerName = "nerdctl-test-logs"
defer base.Cmd("rm", containerName).Run()
base.Cmd("run", "-d", "--name", containerName, testutil.AlpineImage,
"sh", "-euxc", "echo foo; echo bar").AssertOK()
base.Cmd("logs", "-f", containerName).AssertOut("bar")
// Run logs twice, make sure that the logs are not removed
base.Cmd("logs", "-f", containerName).AssertOut("foo")
base.Cmd("rm", "-f", containerName).AssertOK()
}

func TestLogsWithFailingContainer(t *testing.T) {
base := testutil.NewBase(t)
const containerName = "nerdctl-test-logs"
defer base.Cmd("rm", containerName).Run()
base.Cmd("run", "-d", "--name", containerName, testutil.AlpineImage,
"sh", "-euxc", "echo foo; echo bar; exit 42; echo baz").AssertOK()
// AssertOut also asserts that the exit code of the logs command == 0,
// even when the container is failing
base.Cmd("logs", "-f", containerName).AssertOut("bar")
base.Cmd("logs", "-f", containerName).AssertNoOut("baz")
base.Cmd("rm", "-f", containerName).AssertOK()
}