Skip to content

Commit 578caa9

Browse files
authored
Merge pull request #1472 from fuweid/feature_finish_logs_client_part
feature: finish the CLI logs part
2 parents 8545de4 + aca9f33 commit 578caa9

File tree

8 files changed

+427
-29
lines changed

8 files changed

+427
-29
lines changed

cli/logs.go

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package main
22

33
import (
4-
"bytes"
54
"context"
6-
"fmt"
5+
"io"
6+
"os"
77

88
"github.com/alibaba/pouch/apis/types"
9+
"github.com/docker/docker/pkg/stdcopy"
910

1011
"github.com/spf13/cobra"
1112
)
@@ -25,6 +26,7 @@ type LogsCommand struct {
2526
follow bool
2627
since string
2728
tail string
29+
until string
2830
timestamps bool
2931
}
3032

@@ -47,17 +49,17 @@ func (lc *LogsCommand) Init(c *Cli) {
4749
// addFlags adds flags for specific command.
4850
func (lc *LogsCommand) addFlags() {
4951
flagSet := lc.cmd.Flags()
50-
flagSet.BoolVarP(&lc.details, "details", "", false, "Show extra provided to logs")
5152
flagSet.BoolVarP(&lc.follow, "follow", "f", false, "Follow log output")
52-
flagSet.StringVarP(&lc.since, "since", "", "", "Show logs since timestamp")
53+
flagSet.StringVarP(&lc.since, "since", "", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
54+
flagSet.StringVarP(&lc.until, "until", "", "", "Show logs before timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
5355
flagSet.StringVarP(&lc.tail, "tail", "", "all", "Number of lines to show from the end of the logs default \"all\"")
5456
flagSet.BoolVarP(&lc.timestamps, "timestamps", "t", false, "Show timestamps")
57+
58+
// TODO(fuwei): support the detail functionality
5559
}
5660

5761
// runLogs is the entry of LogsCommand command.
5862
func (lc *LogsCommand) runLogs(args []string) error {
59-
// TODO
60-
6163
containerName := args[0]
6264

6365
ctx := context.Background()
@@ -67,25 +69,30 @@ func (lc *LogsCommand) runLogs(args []string) error {
6769
ShowStdout: true,
6870
ShowStderr: true,
6971
Since: lc.since,
72+
Until: lc.until,
7073
Timestamps: lc.timestamps,
7174
Follow: lc.follow,
7275
Tail: lc.tail,
73-
Details: lc.details,
7476
}
7577

76-
resp, err := apiClient.ContainerLogs(ctx, containerName, opts)
78+
body, err := apiClient.ContainerLogs(ctx, containerName, opts)
7779
if err != nil {
7880
return err
7981
}
8082

81-
defer resp.Close()
83+
defer body.Close()
8284

83-
buf := new(bytes.Buffer)
84-
buf.ReadFrom(resp)
85-
86-
fmt.Printf(buf.String())
85+
c, err := apiClient.ContainerGet(ctx, containerName)
86+
if err != nil {
87+
return err
88+
}
8789

88-
return nil
90+
if c.Config.Tty {
91+
_, err = io.Copy(os.Stdout, body)
92+
} else {
93+
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, body)
94+
}
95+
return err
8996
}
9097

9198
// logsExample shows examples in logs command, and is used in auto-generated cli docs.

client/container_logs.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import (
44
"context"
55
"io"
66
"net/url"
7+
"time"
78

89
"github.com/alibaba/pouch/apis/types"
10+
"github.com/alibaba/pouch/pkg/utils"
911
)
1012

1113
// ContainerLogs return the logs generated by a container in an io.ReadCloser.
1214
func (client *APIClient) ContainerLogs(ctx context.Context, name string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
15+
now := time.Now()
16+
1317
query := url.Values{}
1418
if options.ShowStdout {
1519
query.Set("stdout", "1")
@@ -20,21 +24,25 @@ func (client *APIClient) ContainerLogs(ctx context.Context, name string, options
2024
}
2125

2226
if options.Since != "" {
23-
// TODO
27+
sinceTs, err := utils.GetUnixTimestamp(options.Since, now)
28+
if err != nil {
29+
return nil, err
30+
}
31+
query.Set("since", sinceTs)
2432
}
2533

2634
if options.Until != "" {
27-
// TODO
35+
untilTs, err := utils.GetUnixTimestamp(options.Until, now)
36+
if err != nil {
37+
return nil, err
38+
}
39+
query.Set("until", untilTs)
2840
}
2941

3042
if options.Timestamps {
3143
query.Set("timestamps", "1")
3244
}
3345

34-
if options.Details {
35-
query.Set("details", "1")
36-
}
37-
3846
if options.Follow {
3947
query.Set("follow", "1")
4048
}
@@ -44,6 +52,5 @@ func (client *APIClient) ContainerLogs(ctx context.Context, name string, options
4452
if err != nil {
4553
return nil, err
4654
}
47-
ensureCloseReader(resp)
4855
return resp.Body, nil
4956
}

client/container_logs_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"strings"
10+
"testing"
11+
12+
"github.com/alibaba/pouch/apis/types"
13+
)
14+
15+
func TestContainerLogsServerError(t *testing.T) {
16+
client := &APIClient{
17+
HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")),
18+
}
19+
20+
_, err := client.ContainerLogs(context.Background(), "nothing", types.ContainerLogsOptions{})
21+
if err == nil || !strings.Contains(err.Error(), "Server error") {
22+
t.Fatalf("expected a Server Error, got %v", err)
23+
}
24+
}
25+
26+
func TestContainerLogsOK(t *testing.T) {
27+
expectedURL := "/containers/container_id/logs"
28+
expectedSinceTS := "1531728000.000000000" // 2018-07-16T08:00Z
29+
expectedUntilTS := "1531728300.000000000" // 2018-07-16T08:05Z
30+
31+
opts := types.ContainerLogsOptions{
32+
Follow: true,
33+
ShowStdout: true,
34+
ShowStderr: false,
35+
Timestamps: true,
36+
37+
Since: "2018-07-16T08:00Z",
38+
Until: "2018-07-16T08:05Z",
39+
Tail: "10",
40+
}
41+
42+
httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
43+
if !strings.HasPrefix(req.URL.Path, expectedURL) {
44+
return nil, fmt.Errorf("expected URL %s, got %s", expectedURL, req.URL)
45+
}
46+
47+
if req.Method != http.MethodGet {
48+
return nil, fmt.Errorf("expected HTTP Method = %s, got %s", http.MethodGet, req.Method)
49+
}
50+
51+
query := req.URL.Query()
52+
if got := query.Get("follow"); got != "1" {
53+
return nil, fmt.Errorf("expected follow mode (1), got %v", got)
54+
}
55+
56+
if got := query.Get("stdout"); got != "1" {
57+
return nil, fmt.Errorf("expected stdout mode (1), got %v", got)
58+
}
59+
60+
if got := query.Get("stderr"); got != "" {
61+
return nil, fmt.Errorf("expected without stderr mode, got %v", got)
62+
}
63+
64+
if got := query.Get("timestamps"); got != "1" {
65+
return nil, fmt.Errorf("expected timestamps mode, got %v", got)
66+
}
67+
68+
if got := query.Get("tail"); got != "10" {
69+
return nil, fmt.Errorf("expected tail = %v, got %v", opts.Tail, got)
70+
}
71+
72+
if got := query.Get("since"); got != expectedSinceTS {
73+
return nil, fmt.Errorf("expected since = %v, got %v", expectedSinceTS, got)
74+
}
75+
76+
if got := query.Get("until"); got != expectedUntilTS {
77+
return nil, fmt.Errorf("expected since = %v, got %v", expectedUntilTS, got)
78+
}
79+
80+
return &http.Response{
81+
StatusCode: http.StatusOK,
82+
Body: ioutil.NopCloser(bytes.NewReader([]byte(""))),
83+
}, nil
84+
})
85+
86+
client := &APIClient{
87+
HTTPCli: httpClient,
88+
}
89+
90+
_, err := client.ContainerLogs(context.Background(), "container_id", opts)
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
}

daemon/logger/jsonfile/utils.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,19 @@ func seekOffsetByTailLines(rs io.ReadSeeker, n int) (int64, error) {
193193
cnt = 0
194194
left = int64(0)
195195
b []byte
196+
197+
readN = int64(0)
196198
)
197199

198200
for {
201+
readN = int64(blockSize)
199202
left = size + int64(block*blockSize)
200203
if left < 0 {
204+
readN = int64(blockSize) + left
201205
left = 0
202206
}
203207

204-
b = make([]byte, blockSize)
208+
b = make([]byte, readN)
205209
if _, err := rs.Seek(left, os.SEEK_SET); err != nil {
206210
return 0, err
207211
}

pkg/utils/utils.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,92 @@ func FormatTimeInterval(input int64) (formattedTime string, err error) {
8888
return formattedTime, nil
8989
}
9090

91+
// GetUnixTimestamp will parse the value into time and get the nano-timestamp
92+
// in string.
93+
//
94+
// NOTE: if the value is not relative time, GetUnixTimestamp will use RFC3339
95+
// format to parse the value.
96+
func GetUnixTimestamp(value string, base time.Time) (string, error) {
97+
// time.ParseDuration will handle the 5h, 7d relative time.
98+
if d, err := time.ParseDuration(value); value != "0" && err == nil {
99+
return strconv.FormatInt(base.Add(-d).Unix(), 10), nil
100+
}
101+
102+
var (
103+
// rfc3399
104+
layoutDate = "2006-01-02"
105+
layoutDateWithH = "2006-01-02T15"
106+
layoutDateWithHM = "2006-01-02T15:04"
107+
layoutDateWithHMS = "2006-01-02T15:04:05"
108+
layoutDateWithHMSNano = "2006-01-02T15:04:05.999999999"
109+
110+
layout string
111+
)
112+
113+
// if the value doesn't contain any z, Z, +, T, : and -, it maybe
114+
// timestamp and we should return value.
115+
if !strings.ContainsAny(value, "zZ+.:T-") {
116+
return value, nil
117+
}
118+
119+
// if the value containns any z, Z or +, we should parse it with timezone
120+
isLocal := !(strings.ContainsAny(value, "zZ+") || strings.Count(value, "-") == 3)
121+
122+
if strings.Contains(value, ".") {
123+
// if the value contains ., we should parse it with nano
124+
if isLocal {
125+
layout = layoutDateWithHMSNano
126+
} else {
127+
layout = layoutDateWithHMSNano + "Z07:00"
128+
}
129+
} else if strings.Contains(value, "T") {
130+
// if the value contains T, we should parse it with h:m:s
131+
numColons := strings.Count(value, ":")
132+
133+
// NOTE:
134+
// from https://tools.ietf.org/html/rfc3339
135+
//
136+
// time-numoffset = ("+" / "-") time-hour [[":"] time-minute]
137+
//
138+
// if the value has zero with +/-, it may contains the extra
139+
// colon like +08:00, which we should remove the extra colon.
140+
if !isLocal && !strings.ContainsAny(value, "zZ") && numColons > 0 {
141+
numColons--
142+
}
143+
144+
switch numColons {
145+
case 0:
146+
layout = layoutDateWithH
147+
case 1:
148+
layout = layoutDateWithHM
149+
default:
150+
layout = layoutDateWithHMS
151+
}
152+
153+
if !isLocal {
154+
layout += "Z07:00"
155+
}
156+
} else if isLocal {
157+
layout = layoutDate
158+
} else {
159+
layout = layoutDate + "Z07:00"
160+
}
161+
162+
var t time.Time
163+
var err error
164+
165+
if isLocal {
166+
t, err = time.ParseInLocation(layout, value, time.FixedZone(base.Zone()))
167+
} else {
168+
t, err = time.Parse(layout, value)
169+
}
170+
171+
if err != nil {
172+
return "", err
173+
}
174+
return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil
175+
}
176+
91177
// ParseTimestamp returns seconds and nanoseconds.
92178
//
93179
// 1. If the value is empty, it will return default second, the second arg.

0 commit comments

Comments
 (0)