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

feature: finish the CLI logs part #1472

Merged
merged 1 commit into from
Jun 12, 2018
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
35 changes: 21 additions & 14 deletions cli/logs.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package main

import (
"bytes"
"context"
"fmt"
"io"
"os"

"github.com/alibaba/pouch/apis/types"
"github.com/docker/docker/pkg/stdcopy"

"github.com/spf13/cobra"
)
Expand All @@ -25,6 +26,7 @@ type LogsCommand struct {
follow bool
since string
tail string
until string
timestamps bool
}

Expand All @@ -47,17 +49,17 @@ func (lc *LogsCommand) Init(c *Cli) {
// addFlags adds flags for specific command.
func (lc *LogsCommand) addFlags() {
flagSet := lc.cmd.Flags()
flagSet.BoolVarP(&lc.details, "details", "", false, "Show extra provided to logs")
flagSet.BoolVarP(&lc.follow, "follow", "f", false, "Follow log output")
flagSet.StringVarP(&lc.since, "since", "", "", "Show logs since timestamp")
flagSet.StringVarP(&lc.since, "since", "", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
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
}

// runLogs is the entry of LogsCommand command.
func (lc *LogsCommand) runLogs(args []string) error {
// TODO

containerName := args[0]

ctx := context.Background()
Expand All @@ -67,25 +69,30 @@ func (lc *LogsCommand) runLogs(args []string) error {
ShowStdout: true,
ShowStderr: true,
Since: lc.since,
Until: lc.until,
Timestamps: lc.timestamps,
Follow: lc.follow,
Tail: lc.tail,
Details: lc.details,
}

resp, err := apiClient.ContainerLogs(ctx, containerName, opts)
body, err := apiClient.ContainerLogs(ctx, containerName, opts)
if err != nil {
return err
}

defer resp.Close()
defer body.Close()

buf := new(bytes.Buffer)
buf.ReadFrom(resp)

fmt.Printf(buf.String())
c, err := apiClient.ContainerGet(ctx, containerName)
if err != nil {
return err
}

return nil
if c.Config.Tty {
_, err = io.Copy(os.Stdout, body)
} else {
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, body)
}
return err
}

// logsExample shows examples in logs command, and is used in auto-generated cli docs.
Expand Down
21 changes: 14 additions & 7 deletions client/container_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import (
"context"
"io"
"net/url"
"time"

"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/pkg/utils"
)

// ContainerLogs return the logs generated by a container in an io.ReadCloser.
func (client *APIClient) ContainerLogs(ctx context.Context, name string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
now := time.Now()

query := url.Values{}
if options.ShowStdout {
query.Set("stdout", "1")
Expand All @@ -20,21 +24,25 @@ func (client *APIClient) ContainerLogs(ctx context.Context, name string, options
}

if options.Since != "" {
// TODO
sinceTs, err := utils.GetUnixTimestamp(options.Since, now)
if err != nil {
return nil, err
}
query.Set("since", sinceTs)
}

if options.Until != "" {
// TODO
untilTs, err := utils.GetUnixTimestamp(options.Until, now)
if err != nil {
return nil, err
}
query.Set("until", untilTs)
}

if options.Timestamps {
query.Set("timestamps", "1")
}

if options.Details {
query.Set("details", "1")
}

if options.Follow {
query.Set("follow", "1")
}
Expand All @@ -44,6 +52,5 @@ func (client *APIClient) ContainerLogs(ctx context.Context, name string, options
if err != nil {
return nil, err
}
ensureCloseReader(resp)
return resp.Body, nil
}
94 changes: 94 additions & 0 deletions client/container_logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package client

import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"

"github.com/alibaba/pouch/apis/types"
)

func TestContainerLogsServerError(t *testing.T) {
client := &APIClient{
HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")),
}

_, err := client.ContainerLogs(context.Background(), "nothing", types.ContainerLogsOptions{})
if err == nil || !strings.Contains(err.Error(), "Server error") {
t.Fatalf("expected a Server Error, got %v", err)
}
}

func TestContainerLogsOK(t *testing.T) {
expectedURL := "/containers/container_id/logs"
expectedSinceTS := "1531728000.000000000" // 2018-07-16T08:00Z
expectedUntilTS := "1531728300.000000000" // 2018-07-16T08:05Z

opts := types.ContainerLogsOptions{
Follow: true,
ShowStdout: true,
ShowStderr: false,
Timestamps: true,

Since: "2018-07-16T08:00Z",
Until: "2018-07-16T08:05Z",
Tail: "10",
}

httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("expected URL %s, got %s", expectedURL, req.URL)
}

if req.Method != http.MethodGet {
return nil, fmt.Errorf("expected HTTP Method = %s, got %s", http.MethodGet, req.Method)
}

query := req.URL.Query()
if got := query.Get("follow"); got != "1" {
return nil, fmt.Errorf("expected follow mode (1), got %v", got)
}

if got := query.Get("stdout"); got != "1" {
return nil, fmt.Errorf("expected stdout mode (1), got %v", got)
}

if got := query.Get("stderr"); got != "" {
return nil, fmt.Errorf("expected without stderr mode, got %v", got)
}

if got := query.Get("timestamps"); got != "1" {
return nil, fmt.Errorf("expected timestamps mode, got %v", got)
}

if got := query.Get("tail"); got != "10" {
return nil, fmt.Errorf("expected tail = %v, got %v", opts.Tail, got)
}

if got := query.Get("since"); got != expectedSinceTS {
return nil, fmt.Errorf("expected since = %v, got %v", expectedSinceTS, got)
}

if got := query.Get("until"); got != expectedUntilTS {
return nil, fmt.Errorf("expected since = %v, got %v", expectedUntilTS, got)
}

return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
}, nil
})

client := &APIClient{
HTTPCli: httpClient,
}

_, err := client.ContainerLogs(context.Background(), "container_id", opts)
if err != nil {
t.Fatal(err)
}
}
6 changes: 5 additions & 1 deletion daemon/logger/jsonfile/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,19 @@ func seekOffsetByTailLines(rs io.ReadSeeker, n int) (int64, error) {
cnt = 0
left = int64(0)
b []byte

readN = int64(0)
)

for {
readN = int64(blockSize)
left = size + int64(block*blockSize)
if left < 0 {
readN = int64(blockSize) + left
left = 0
}

b = make([]byte, blockSize)
b = make([]byte, readN)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the content don't end with \n like 1\n2\n3\n4, seekOffsetByTailLines(, 2) will return the index of 2. The tail content would be 2\n3\n4. Compared with command tail, echo "1\n2\n3\n4" > a.txt; tail -n2 a.txt is 3\n4.
Does this function works as expected?😁 @fuweid

if _, err := rs.Seek(left, os.SEEK_SET); err != nil {
return 0, err
}
Expand Down
86 changes: 86 additions & 0 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,92 @@ func FormatTimeInterval(input int64) (formattedTime string, err error) {
return formattedTime, nil
}

// GetUnixTimestamp will parse the value into time and get the nano-timestamp
// in string.
//
// NOTE: if the value is not relative time, GetUnixTimestamp will use RFC3339
// format to parse the value.
func GetUnixTimestamp(value string, base time.Time) (string, error) {
// time.ParseDuration will handle the 5h, 7d relative time.
if d, err := time.ParseDuration(value); value != "0" && err == nil {
return strconv.FormatInt(base.Add(-d).Unix(), 10), nil
}

var (
// rfc3399
layoutDate = "2006-01-02"
layoutDateWithH = "2006-01-02T15"
layoutDateWithHM = "2006-01-02T15:04"
layoutDateWithHMS = "2006-01-02T15:04:05"
layoutDateWithHMSNano = "2006-01-02T15:04:05.999999999"

layout string
)

// if the value doesn't contain any z, Z, +, T, : and -, it maybe
// timestamp and we should return value.
if !strings.ContainsAny(value, "zZ+.:T-") {
return value, nil
}

// if the value containns any z, Z or +, we should parse it with timezone
isLocal := !(strings.ContainsAny(value, "zZ+") || strings.Count(value, "-") == 3)

if strings.Contains(value, ".") {
// if the value contains ., we should parse it with nano
if isLocal {
layout = layoutDateWithHMSNano
} else {
layout = layoutDateWithHMSNano + "Z07:00"
}
} else if strings.Contains(value, "T") {
// if the value contains T, we should parse it with h:m:s
numColons := strings.Count(value, ":")

// NOTE:
// from https://tools.ietf.org/html/rfc3339
//
// time-numoffset = ("+" / "-") time-hour [[":"] time-minute]
//
// if the value has zero with +/-, it may contains the extra
// colon like +08:00, which we should remove the extra colon.
if !isLocal && !strings.ContainsAny(value, "zZ") && numColons > 0 {
numColons--
}

switch numColons {
case 0:
layout = layoutDateWithH
case 1:
layout = layoutDateWithHM
default:
layout = layoutDateWithHMS
}

if !isLocal {
layout += "Z07:00"
}
} else if isLocal {
layout = layoutDate
} else {
layout = layoutDate + "Z07:00"
}

var t time.Time
var err error

if isLocal {
t, err = time.ParseInLocation(layout, value, time.FixedZone(base.Zone()))
} else {
t, err = time.Parse(layout, value)
}

if err != nil {
return "", err
}
return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
}

// ParseTimestamp returns seconds and nanoseconds.
//
// 1. If the value is empty, it will return default second, the second arg.
Expand Down
Loading