mirror of
https://github.com/containers/podman.git
synced 2025-05-20 08:36:23 +08:00

when running podman logs on a created container (which has no logs), podman should return gracefully (like docker) with a 0 return code. if multiple containers are provided and one is only in the created state (and no follow is used), we still display the logs for the other ids. fixes issue #2677 Signed-off-by: baude <bbaude@redhat.com>
214 lines
5.0 KiB
Go
214 lines
5.0 KiB
Go
package libpod
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hpcloud/tail"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
// logTimeFormat is the time format used in the log.
|
|
// It is a modified version of RFC3339Nano that guarantees trailing
|
|
// zeroes are not trimmed, taken from
|
|
// https://github.com/golang/go/issues/19635
|
|
logTimeFormat = "2006-01-02T15:04:05.000000000Z07:00"
|
|
)
|
|
|
|
// LogOptions is the options you can use for logs
|
|
type LogOptions struct {
|
|
Details bool
|
|
Follow bool
|
|
Since time.Time
|
|
Tail uint64
|
|
Timestamps bool
|
|
Multi bool
|
|
WaitGroup *sync.WaitGroup
|
|
}
|
|
|
|
// LogLine describes the information for each line of a log
|
|
type LogLine struct {
|
|
Device string
|
|
ParseLogType string
|
|
Time time.Time
|
|
Msg string
|
|
CID string
|
|
}
|
|
|
|
// Log is a runtime function that can read one or more container logs.
|
|
func (r *Runtime) Log(containers []*Container, options *LogOptions, logChannel chan *LogLine) error {
|
|
for _, ctr := range containers {
|
|
if err := ctr.ReadLog(options, logChannel); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadLog reads a containers log based on the input options and returns loglines over a channel
|
|
func (c *Container) ReadLog(options *LogOptions, logChannel chan *LogLine) error {
|
|
t, tailLog, err := getLogFile(c.LogPath(), options)
|
|
if err != nil {
|
|
// If the log file does not exist, this is not fatal.
|
|
if os.IsNotExist(errors.Cause(err)) {
|
|
return nil
|
|
}
|
|
return errors.Wrapf(err, "unable to read log file %s for %s ", c.ID(), c.LogPath())
|
|
}
|
|
options.WaitGroup.Add(1)
|
|
if len(tailLog) > 0 {
|
|
for _, nll := range tailLog {
|
|
nll.CID = c.ID()
|
|
if nll.Since(options.Since) {
|
|
logChannel <- nll
|
|
}
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
var partial string
|
|
for line := range t.Lines {
|
|
nll, err := newLogLine(line.Text)
|
|
if err != nil {
|
|
logrus.Error(err)
|
|
continue
|
|
}
|
|
if nll.Partial() {
|
|
partial = partial + nll.Msg
|
|
continue
|
|
} else if !nll.Partial() && len(partial) > 1 {
|
|
nll.Msg = partial
|
|
partial = ""
|
|
}
|
|
nll.CID = c.ID()
|
|
if nll.Since(options.Since) {
|
|
logChannel <- nll
|
|
}
|
|
}
|
|
options.WaitGroup.Done()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// getLogFile returns an hp tail for a container given options
|
|
func getLogFile(path string, options *LogOptions) (*tail.Tail, []*LogLine, error) {
|
|
var (
|
|
whence int
|
|
err error
|
|
logTail []*LogLine
|
|
)
|
|
// whence 0=origin, 2=end
|
|
if options.Tail > 0 {
|
|
whence = 2
|
|
logTail, err = getTailLog(path, int(options.Tail))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
seek := tail.SeekInfo{
|
|
Offset: 0,
|
|
Whence: whence,
|
|
}
|
|
|
|
t, err := tail.TailFile(path, tail.Config{MustExist: true, Poll: true, Follow: options.Follow, Location: &seek, Logger: tail.DiscardingLogger})
|
|
return t, logTail, err
|
|
}
|
|
|
|
func getTailLog(path string, tail int) ([]*LogLine, error) {
|
|
var (
|
|
tailLog []*LogLine
|
|
nlls []*LogLine
|
|
tailCounter int
|
|
partial string
|
|
)
|
|
content, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
splitContent := strings.Split(string(content), "\n")
|
|
// We read the content in reverse and add each nll until we have the same
|
|
// number of F type messages as the desired tail
|
|
for i := len(splitContent) - 1; i >= 0; i-- {
|
|
if len(splitContent[i]) == 0 {
|
|
continue
|
|
}
|
|
nll, err := newLogLine(splitContent[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nlls = append(nlls, nll)
|
|
if !nll.Partial() {
|
|
tailCounter = tailCounter + 1
|
|
}
|
|
if tailCounter == tail {
|
|
break
|
|
}
|
|
}
|
|
// Now we iterate the results and assemble partial messages to become full messages
|
|
for _, nll := range nlls {
|
|
if nll.Partial() {
|
|
partial = partial + nll.Msg
|
|
} else {
|
|
nll.Msg = nll.Msg + partial
|
|
tailLog = append(tailLog, nll)
|
|
partial = ""
|
|
}
|
|
}
|
|
return tailLog, nil
|
|
}
|
|
|
|
// String converts a logline to a string for output given whether a detail
|
|
// bool is specified.
|
|
func (l *LogLine) String(options *LogOptions) string {
|
|
var out string
|
|
if options.Multi {
|
|
cid := l.CID
|
|
if len(cid) > 12 {
|
|
cid = cid[:12]
|
|
}
|
|
out = fmt.Sprintf("%s ", cid)
|
|
}
|
|
if options.Timestamps {
|
|
out = out + fmt.Sprintf("%s ", l.Time.Format(logTimeFormat))
|
|
}
|
|
return out + l.Msg
|
|
}
|
|
|
|
// Since returns a bool as to whether a log line occurred after a given time
|
|
func (l *LogLine) Since(since time.Time) bool {
|
|
return l.Time.After(since)
|
|
}
|
|
|
|
// newLogLine creates a logLine struct from a container log string
|
|
func newLogLine(line string) (*LogLine, error) {
|
|
splitLine := strings.Split(line, " ")
|
|
if len(splitLine) < 4 {
|
|
return nil, errors.Errorf("'%s' is not a valid container log line", line)
|
|
}
|
|
logTime, err := time.Parse(time.RFC3339Nano, splitLine[0])
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to convert time %s from container log", splitLine[0])
|
|
}
|
|
l := LogLine{
|
|
Time: logTime,
|
|
Device: splitLine[1],
|
|
ParseLogType: splitLine[2],
|
|
Msg: strings.Join(splitLine[3:], " "),
|
|
}
|
|
return &l, nil
|
|
}
|
|
|
|
// Partial returns a bool if the log line is a partial log type
|
|
func (l *LogLine) Partial() bool {
|
|
if l.ParseLogType == "P" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|