Skip to content

Commit

Permalink
Merge pull request #1472 from fuweid/feature_finish_logs_client_part
Browse files Browse the repository at this point in the history
feature: finish the CLI logs part
  • Loading branch information
allencloud authored Jun 12, 2018
2 parents 8545de4 + aca9f33 commit 578caa9
Show file tree
Hide file tree
Showing 8 changed files with 427 additions and 29 deletions.
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)
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

0 comments on commit 578caa9

Please sign in to comment.