diff --git a/apis/server/container_bridge.go b/apis/server/container_bridge.go index 8d3036fb69..7b95369add 100644 --- a/apis/server/container_bridge.go +++ b/apis/server/container_bridge.go @@ -340,9 +340,7 @@ func (s *Server) logsContainer(ctx context.Context, rw http.ResponseWriter, req Until: req.Form.Get("until"), Follow: httputils.BoolValue(req, "follow"), Timestamps: httputils.BoolValue(req, "timestamps"), - - // TODO: support the details - // Details: httputils.BoolValue(r, "details"), + Details: httputils.BoolValue(req, "details"), } name := mux.Vars(req)["name"] @@ -350,7 +348,6 @@ func (s *Server) logsContainer(ctx context.Context, rw http.ResponseWriter, req if err != nil { return err } - writeLogStream(ctx, rw, tty, opts, msgCh) return nil } diff --git a/apis/server/utils.go b/apis/server/utils.go index d3ea3b2b45..957c1177a6 100644 --- a/apis/server/utils.go +++ b/apis/server/utils.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "github.com/alibaba/pouch/apis/types" "github.com/alibaba/pouch/daemon/logger" @@ -73,6 +74,15 @@ func writeLogStream(ctx context.Context, w io.Writer, tty bool, opt *types.Conta } logLine := msg.Line + + if opt.Details { + var ss []string + for k, v := range msg.Attrs { + ss = append(ss, k+"="+v) + } + logLine = append([]byte(strings.Join(ss, ",")+" "), logLine...) + } + if opt.Timestamps { logLine = append([]byte(msg.Timestamp.Format(utils.TimeLayout)+" "), logLine...) } diff --git a/cli/logs.go b/cli/logs.go index 4148f20028..966a2de862 100644 --- a/cli/logs.go +++ b/cli/logs.go @@ -49,8 +49,7 @@ func (lc *LogsCommand) addFlags() { flagSet.StringVarP(&lc.until, "until", "", "", "Show logs before timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") flagSet.StringVarP(&lc.tail, "tail", "", "all", "Number of lines to show from the end of the logs default \"all\"") flagSet.BoolVarP(&lc.timestamps, "timestamps", "t", false, "Show timestamps") - - // TODO(fuwei): support the detail functionality + flagSet.BoolVar(&lc.details, "details", false, "Show extra details provided to logs") } // runLogs is the entry of LogsCommand command. @@ -68,6 +67,7 @@ func (lc *LogsCommand) runLogs(args []string) error { Timestamps: lc.timestamps, Follow: lc.follow, Tail: lc.tail, + Details: lc.details, } body, err := apiClient.ContainerLogs(ctx, containerName, opts) diff --git a/client/container_logs.go b/client/container_logs.go index 80153bf948..6c755a0772 100644 --- a/client/container_logs.go +++ b/client/container_logs.go @@ -46,6 +46,10 @@ func (client *APIClient) ContainerLogs(ctx context.Context, name string, options if options.Follow { query.Set("follow", "1") } + + if options.Details { + query.Set("details", "1") + } query.Set("tail", options.Tail) resp, err := client.get(ctx, "/containers/"+name+"/logs", query, nil) diff --git a/daemon/containerio/jsonfile.go b/daemon/containerio/jsonfile.go index ff913785b9..d4626fc888 100644 --- a/daemon/containerio/jsonfile.go +++ b/daemon/containerio/jsonfile.go @@ -12,6 +12,7 @@ import ( "github.com/alibaba/pouch/daemon/logger/jsonfile" "github.com/sirupsen/logrus" + "encoding/json" ) func init() { @@ -43,7 +44,25 @@ func (jf *jsonFile) Init(opt *Option) error { } logPath := filepath.Join(rootDir, jsonFilePathName) - w, err := jsonfile.NewJSONLogFile(logPath, 0644) + attrs, err := opt.info.ExtraAttributes(nil) + if err != nil { + return err + } + + var extra []byte + if len(attrs) > 0 { + var err error + extra, err = json.Marshal(attrs) + if err != nil { + return err + } + } + + marshalFunc := func(msg *logger.LogMessage) ([]byte, error) { + return jsonfile.Marshal(msg, extra) + } + + w, err := jsonfile.NewJSONLogFile(logPath, 0644, marshalFunc) if err != nil { return err } diff --git a/daemon/logger/jsonfile/encode.go b/daemon/logger/jsonfile/encode.go index f1525a0205..01ff2018e2 100644 --- a/daemon/logger/jsonfile/encode.go +++ b/daemon/logger/jsonfile/encode.go @@ -12,9 +12,10 @@ import ( ) type jsonLog struct { - Stream string `json:"stream,omitempty"` - Log string `json:"log,omitempty"` - Timestamp time.Time `json:"time"` + Stream string `json:"stream,omitempty"` + Log string `json:"log,omitempty"` + Timestamp time.Time `json:"time"` + Attrs map[string]string `json:"attrs,omitempty"` } func newUnmarshal(r io.Reader) func() (*logger.LogMessage, error) { @@ -30,11 +31,13 @@ func newUnmarshal(r io.Reader) func() (*logger.LogMessage, error) { Source: jl.Stream, Line: []byte(jl.Log), Timestamp: jl.Timestamp, + Attrs: jl.Attrs, }, nil } } -func marshal(msg *logger.LogMessage) ([]byte, error) { +//Marshal used to marshal LogMessage to byte[] +func Marshal(msg *logger.LogMessage, rawAttrs []byte) ([]byte, error) { var ( first = true buf bytes.Buffer @@ -62,6 +65,13 @@ func marshal(msg *logger.LogMessage) ([]byte, error) { buf.WriteString(`"time":`) buf.WriteString(msg.Timestamp.UTC().Format(`"` + utils.TimeLayout + `"`)) + + if len(rawAttrs) > 0 { + buf.WriteString(`,`) + buf.WriteString(`"attrs":`) + buf.Write(rawAttrs) + } + buf.WriteString(`}`) // NOTE: add newline here to make the decoder easier diff --git a/daemon/logger/jsonfile/encode_test.go b/daemon/logger/jsonfile/encode_test.go index 28a775b3e0..7de0bd66fc 100644 --- a/daemon/logger/jsonfile/encode_test.go +++ b/daemon/logger/jsonfile/encode_test.go @@ -10,13 +10,16 @@ import ( ) func TestMarshalAndUnmarshal(t *testing.T) { + + attrs := map[string]string{"env": "test"} expectedMsg := &logger.LogMessage{ Source: "stdout", Line: []byte("hello pouch"), Timestamp: time.Now().UTC(), + Attrs: attrs, } - bs, err := marshal(expectedMsg) + bs, err := Marshal(expectedMsg, attrs) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/daemon/logger/jsonfile/jsonfile.go b/daemon/logger/jsonfile/jsonfile.go index a1affc5496..dfac168ffb 100644 --- a/daemon/logger/jsonfile/jsonfile.go +++ b/daemon/logger/jsonfile/jsonfile.go @@ -7,32 +7,37 @@ import ( "github.com/alibaba/pouch/daemon/logger" ) +//MarshalFunc is the function of marshal the logMessage +type MarshalFunc func(message *logger.LogMessage) ([]byte, error) + // JSONLogFile is uses to log the container's stdout and stderr. type JSONLogFile struct { mu sync.Mutex - f *os.File - perms os.FileMode - closed bool + f *os.File + perms os.FileMode + closed bool + marshalFunc MarshalFunc } // NewJSONLogFile returns new JSONLogFile instance. -func NewJSONLogFile(logPath string, perms os.FileMode) (*JSONLogFile, error) { +func NewJSONLogFile(logPath string, perms os.FileMode, marshalFunc MarshalFunc) (*JSONLogFile, error) { f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perms) if err != nil { return nil, err } return &JSONLogFile{ - f: f, - perms: perms, - closed: false, + f: f, + perms: perms, + closed: false, + marshalFunc: marshalFunc, }, nil } // WriteLogMessage will write the LogMessage into the file. func (lf *JSONLogFile) WriteLogMessage(msg *logger.LogMessage) error { - b, err := marshal(msg) + b, err := lf.marshalFunc(msg) if err != nil { return err } diff --git a/daemon/logger/jsonfile/jsonfile_read_test.go b/daemon/logger/jsonfile/jsonfile_read_test.go index 9267327076..d8c9e5038c 100644 --- a/daemon/logger/jsonfile/jsonfile_read_test.go +++ b/daemon/logger/jsonfile/jsonfile_read_test.go @@ -17,7 +17,7 @@ func TestReadLogMessagesWithRemoveFileInFollowMode(t *testing.T) { defer f.Close() defer os.RemoveAll(f.Name()) - jf, err := NewJSONLogFile(f.Name(), 0640) + jf, err := NewJSONLogFile(f.Name(), 0640, nil) if err != nil { t.Fatalf("unexpected error during create JSONLogFile: %v", err) } @@ -52,7 +52,7 @@ func TestReadLogMessagesForEmptyFileWithoutFollow(t *testing.T) { defer f.Close() defer os.RemoveAll(f.Name()) - jf, err := NewJSONLogFile(f.Name(), 0644) + jf, err := NewJSONLogFile(f.Name(), 0644, nil) if err != nil { t.Fatalf("unexpected error during create JSONLogFile: %v", err) } diff --git a/daemon/logger/logmessage.go b/daemon/logger/logmessage.go index 70e8a72a4c..47190ecfd0 100644 --- a/daemon/logger/logmessage.go +++ b/daemon/logger/logmessage.go @@ -10,8 +10,8 @@ type LogMessage struct { Source string // Source means stdin, stdout or stderr Line []byte // Line means the log content, but it maybe partial Timestamp time.Time // Timestamp means the created time of line - - Err error + Attrs map[string]string + Err error } // LogWatcher is used to pass the log message to the reader. @@ -48,8 +48,9 @@ func (w *LogWatcher) WatchClose() <-chan struct{} { // ReadConfig is used to type ReadConfig struct { - Since time.Time - Until time.Time - Tail int - Follow bool + Since time.Time + Until time.Time + Tail int + Follow bool + Details bool } diff --git a/daemon/mgr/container_logs.go b/daemon/mgr/container_logs.go index 53bd855104..56719b092b 100644 --- a/daemon/mgr/container_logs.go +++ b/daemon/mgr/container_logs.go @@ -49,7 +49,9 @@ func (mgr *ContainerManager) Logs(ctx context.Context, name string, logOpt *type } fileName := filepath.Join(mgr.Store.Path(c.ID), "json.log") - jf, err := jsonfile.NewJSONLogFile(fileName, 0640) + + jf, err := jsonfile.NewJSONLogFile(fileName, 0640, nil) + if err != nil { return nil, false, err } @@ -137,9 +139,10 @@ func convContainerLogsOptionsToReadConfig(logOpt *types.ContainerLogsOptions) (* } return &logger.ReadConfig{ - Since: since, - Until: until, - Follow: logOpt.Follow, - Tail: lines, + Since: since, + Until: until, + Follow: logOpt.Follow, + Tail: lines, + Details: logOpt.Details, }, nil } diff --git a/test/cli_logs_test.go b/test/cli_logs_test.go index 5cf9e657b7..7a229fa864 100644 --- a/test/cli_logs_test.go +++ b/test/cli_logs_test.go @@ -24,6 +24,8 @@ func init() { func (suite *PouchLogsSuite) SetUpSuite(c *check.C) { SkipIfFalse(c, environment.IsLinux) + environment.PruneAllContainers(apiClient) + PullImage(c, busyboxImage) } @@ -39,6 +41,30 @@ func (suite *PouchLogsSuite) TestCreatedContainerLogIsEmpty(c *check.C) { c.Assert(res.Combined(), check.Equals, "") } +func (suite *PouchLogsSuite) TestLogsSeparateStderr(c *check.C) { + cname := "TestLogsSeparateStderr" + msg := "stderr_log" + command.PouchRun("run", "-d", "--name", cname, busyboxImage, "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg)).Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, cname) + + command.PouchRun("logs", cname).Assert(c, icmd.Expected{ + Out: "", + Err: msg, + }) +} + +func (suite *PouchLogsSuite) TestLogsStderrInStdout(c *check.C) { + cname := "TestLogsStderrInStdout" + msg := "stderr_log" + command.PouchRun("run", "-d", "-t", "--name", cname, busyboxImage, "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg)).Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, cname) + + command.PouchRun("logs", cname).Assert(c, icmd.Expected{ + Out: msg, + Err: "", + }) +} + // TestSinceAndUntil tests the since and until. func (suite *PouchLogsSuite) TestSinceAndUntil(c *check.C) { cname := "TestCLILogs_Since_and_Until" @@ -93,7 +119,7 @@ func (suite *PouchLogsSuite) TestTimestamp(c *check.C) { // TestTailMode tests follow mode. func (suite *PouchLogsSuite) TestTailLine(c *check.C) { cname := "TestCLILogs_tail_line" - DelContainerForceMultyTime(c, cname) + totalLine := 100 command.PouchRun( @@ -103,6 +129,7 @@ func (suite *PouchLogsSuite) TestTailLine(c *check.C) { busyboxImage, "sh", "-c", fmt.Sprintf("for i in $(seq 1 %v); do echo hello-$i; done;", totalLine), ).Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, cname) for _, tc := range []struct { input string @@ -148,6 +175,22 @@ func (suite *PouchLogsSuite) TestFollowMode(c *check.C) { } } +// TestLogsWithDetails tests details opt. +func (suite *PouchLogsSuite) TestLogsWithDetails(c *check.C) { + cname := "TestLogsWithDetails" + + res := command.PouchRun("run", "--name", cname, "--label", "foo=bar", "-e", "baz=qux", "--log-opt", "labels=foo", "--log-opt", "env=baz", "busybox", "echo", "hello") + res.Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, cname) + + allLogs := suite.syncLogs(c, cname, "--details") + c.Assert(len(allLogs), check.Equals, 1) + logFields := strings.Split(allLogs[0], " ") + + c.Assert(logFields[0], check.Equals, "baz=qux") + c.Assert(logFields[1], check.Equals, "foo=bar") +} + func (suite *PouchLogsSuite) syncLogs(c *check.C, cname string, flags ...string) []string { args := append([]string{"logs"}, flags...)