Skip to content

Commit 43ab3f1

Browse files
author
Takashi Kusumi
authored
Add a short format for timestamps (#249)
1 parent 079d158 commit 43ab3f1

File tree

8 files changed

+98
-25
lines changed

8 files changed

+98
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
9797
`--tail` | `-1` | The number of lines from the end of the logs to show. Defaults to -1, showing all logs.
9898
`--template` | | Template to use for log lines, leave empty to use --output flag.
9999
`--template-file`, `-T` | | Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.
100-
`--timestamps`, `-t` | `false` | Print timestamps.
100+
`--timestamps`, `-t` | | Print timestamps with the specified format. One of 'default' or 'short'. If specified but without value, 'default' is used.
101101
`--timezone` | `Local` | Set timestamps to specific timezone.
102102
`--verbosity` | `0` | Number of the log level verbosity
103103
`--version`, `-v` | `false` | Print the version and exit.

cmd/cmd.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type options struct {
4646
container string
4747
excludeContainer []string
4848
containerStates []string
49-
timestamps bool
49+
timestamps string
5050
timezone string
5151
since time.Duration
5252
context string
@@ -90,7 +90,7 @@ func NewOptions(streams genericclioptions.IOStreams) *options {
9090
tail: -1,
9191
template: "",
9292
templateFile: "",
93-
timestamps: false,
93+
timestamps: "",
9494
timezone: "Local",
9595
prompt: false,
9696
noFollow: false,
@@ -218,6 +218,17 @@ func (o *options) sternConfig() (*stern.Config, error) {
218218

219219
namespaces := makeUnique(o.namespaces)
220220

221+
var timestampFormat string
222+
switch o.timestamps {
223+
case "default":
224+
timestampFormat = stern.TimestampFormatDefault
225+
case "short":
226+
timestampFormat = stern.TimestampFormatShort
227+
case "":
228+
default:
229+
return nil, errors.New("timestamps should be one of 'default', or 'short'")
230+
}
231+
221232
// --timezone
222233
location, err := time.LoadLocation(o.timezone)
223234
if err != nil {
@@ -239,7 +250,8 @@ func (o *options) sternConfig() (*stern.Config, error) {
239250
Namespaces: namespaces,
240251
PodQuery: pod,
241252
ExcludePodQuery: excludePod,
242-
Timestamps: o.timestamps,
253+
Timestamps: timestampFormat != "",
254+
TimestampFormat: timestampFormat,
243255
Location: location,
244256
ContainerQuery: container,
245257
ExcludeContainerQuery: excludeContainer,
@@ -306,11 +318,13 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
306318
fs.Int64Var(&o.tail, "tail", o.tail, "The number of lines from the end of the logs to show. Defaults to -1, showing all logs.")
307319
fs.StringVar(&o.template, "template", o.template, "Template to use for log lines, leave empty to use --output flag.")
308320
fs.StringVarP(&o.templateFile, "template-file", "T", o.templateFile, "Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.")
309-
fs.BoolVarP(&o.timestamps, "timestamps", "t", o.timestamps, "Print timestamps.")
321+
fs.StringVarP(&o.timestamps, "timestamps", "t", o.timestamps, "Print timestamps with the specified format. One of 'default' or 'short'. If specified but without value, 'default' is used.")
310322
fs.StringVar(&o.timezone, "timezone", o.timezone, "Set timestamps to specific timezone.")
311323
fs.BoolVar(&o.onlyLogLines, "only-log-lines", o.onlyLogLines, "Print only log lines")
312324
fs.IntVar(&o.verbosity, "verbosity", o.verbosity, "Number of the log level verbosity")
313325
fs.BoolVarP(&o.version, "version", "v", o.version, "Print the version and exit.")
326+
327+
fs.Lookup("timestamps").NoOptDefVal = "default"
314328
}
315329

316330
func (o *options) generateTemplate() (*template.Template, error) {

cmd/cmd_test.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ func TestOptionsSternConfig(t *testing.T) {
388388
PodQuery: re(""),
389389
ExcludePodQuery: nil,
390390
Timestamps: false,
391+
TimestampFormat: "",
391392
Location: local,
392393
ContainerQuery: re(".*"),
393394
ExcludeContainerQuery: nil,
@@ -433,7 +434,7 @@ func TestOptionsSternConfig(t *testing.T) {
433434
o.namespaces = []string{"ns1", "ns2"}
434435
o.podQuery = "query1"
435436
o.excludePod = []string{"exp1", "exp2"}
436-
o.timestamps = true
437+
o.timestamps = "default"
437438
o.timezone = "UTC" // Location
438439
o.container = "container1"
439440
o.excludeContainer = []string{"exc1", "exc2"}
@@ -463,6 +464,7 @@ func TestOptionsSternConfig(t *testing.T) {
463464
c.PodQuery = re("query1")
464465
c.ExcludePodQuery = []*regexp.Regexp{re("exp1"), re("exp2")}
465466
c.Timestamps = true
467+
c.TimestampFormat = stern.TimestampFormatDefault
466468
c.Location = utc
467469
c.ContainerQuery = re("container1")
468470
c.ExcludeContainerQuery = []*regexp.Regexp{re("exc1"), re("exc2")}
@@ -519,6 +521,23 @@ func TestOptionsSternConfig(t *testing.T) {
519521
}(),
520522
false,
521523
},
524+
{
525+
"timestamp=short",
526+
func() *options {
527+
o := NewOptions(streams)
528+
o.timestamps = "short"
529+
530+
return o
531+
}(),
532+
func() *stern.Config {
533+
c := defaultConfig()
534+
c.Timestamps = true
535+
c.TimestampFormat = stern.TimestampFormatShort
536+
537+
return c
538+
}(),
539+
false,
540+
},
522541
{
523542
"noFollow has the different default",
524543
func() *options {
@@ -689,6 +708,17 @@ func TestOptionsSternConfig(t *testing.T) {
689708
nil,
690709
true,
691710
},
711+
{
712+
"error timestamps",
713+
func() *options {
714+
o := NewOptions(streams)
715+
o.timestamps = "invalid"
716+
717+
return o
718+
}(),
719+
nil,
720+
true,
721+
},
692722
}
693723

694724
for _, tt := range tests {

cmd/flag_completion.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var flagChoices = map[string][]string{
3434
"completion": []string{"bash", "zsh", "fish"},
3535
"container-state": []string{stern.RUNNING, stern.WAITING, stern.TERMINATED, stern.ALL_STATES},
3636
"output": []string{"default", "raw", "json", "extjson", "ppextjson"},
37+
"timestamps": []string{"default", "short"},
3738
}
3839

3940
func runCompletion(shell string, cmd *cobra.Command, out io.Writer) error {

stern/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Config struct {
3232
PodQuery *regexp.Regexp
3333
ExcludePodQuery []*regexp.Regexp
3434
Timestamps bool
35+
TimestampFormat string
3536
Location *time.Location
3637
ContainerQuery *regexp.Regexp
3738
ExcludeContainerQuery []*regexp.Regexp

stern/stern.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,16 @@ func Run(ctx context.Context, config *Config) error {
9696
})
9797
newTail := func(t *Target) *Tail {
9898
return NewTail(client.CoreV1(), t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, &TailOptions{
99-
Timestamps: config.Timestamps,
100-
Location: config.Location,
101-
SinceSeconds: pointer.Int64(int64(config.Since.Seconds())),
102-
Exclude: config.Exclude,
103-
Include: config.Include,
104-
Namespace: config.AllNamespaces || len(namespaces) > 1,
105-
TailLines: config.TailLines,
106-
Follow: config.Follow,
107-
OnlyLogLines: config.OnlyLogLines,
99+
Timestamps: config.Timestamps,
100+
TimestampFormat: config.TimestampFormat,
101+
Location: config.Location,
102+
SinceSeconds: pointer.Int64(int64(config.Since.Seconds())),
103+
Exclude: config.Exclude,
104+
Include: config.Include,
105+
Namespace: config.AllNamespaces || len(namespaces) > 1,
106+
TailLines: config.TailLines,
107+
Follow: config.Follow,
108+
OnlyLogLines: config.OnlyLogLines,
108109
})
109110
}
110111

stern/tail.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ import (
3636
"k8s.io/client-go/rest"
3737
)
3838

39+
// RFC3339Nano with trailing zeros
40+
const TimestampFormatDefault = "2006-01-02T15:04:05.000000000Z07:00"
41+
42+
// time.DateTime without year
43+
const TimestampFormatShort = "01-02 15:04:05"
44+
3945
type Tail struct {
4046
clientset corev1client.CoreV1Interface
4147

@@ -58,8 +64,9 @@ type Tail struct {
5864
}
5965

6066
type TailOptions struct {
61-
Timestamps bool
62-
Location *time.Location
67+
Timestamps bool
68+
TimestampFormat string
69+
Location *time.Location
6370

6471
SinceSeconds *int64
6572
SinceTime *metav1.Time
@@ -131,12 +138,16 @@ func (o TailOptions) HighlightMatchedString(msg string) string {
131138
return msg
132139
}
133140

134-
func (o TailOptions) UpdateTimezone(timestamp string) (string, error) {
141+
func (o TailOptions) UpdateTimezoneAndFormat(timestamp string) (string, error) {
135142
t, err := time.ParseInLocation(time.RFC3339Nano, timestamp, time.UTC)
136143
if err != nil {
137144
return "", errors.New("missing timestamp")
138145
}
139-
return t.In(o.Location).Format("2006-01-02T15:04:05.000000000Z07:00"), nil
146+
format := TimestampFormatDefault
147+
if o.TimestampFormat != "" {
148+
format = o.TimestampFormat
149+
}
150+
return t.In(o.Location).Format(format), nil
140151
}
141152

142153
// NewTail returns a new tail for a Kubernetes container inside a pod
@@ -327,7 +338,7 @@ func (t *Tail) consumeLine(line string) {
327338
msg := t.Options.HighlightMatchedString(content)
328339

329340
if t.Options.Timestamps {
330-
updatedTs, err := t.Options.UpdateTimezone(rfc3339Nano)
341+
updatedTs, err := t.Options.UpdateTimezoneAndFormat(rfc3339Nano)
331342
if err != nil {
332343
t.Print(fmt.Sprintf("[%v] %s", err, line))
333344
return

stern/tail_test.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,26 @@ func TestIsIncludeTestOptions(t *testing.T) {
6363
}
6464
}
6565

66-
func TestUpdateTimezone(t *testing.T) {
66+
func TestUpdateTimezoneAndFormat(t *testing.T) {
6767
location, _ := time.LoadLocation("Asia/Tokyo")
6868

6969
tests := []struct {
7070
name string
71+
format string
7172
message string
7273
expected string
7374
err string
7475
}{
7576
{
7677
"normal case",
78+
"", // default format is used if empty
7779
"2021-04-18T03:54:44.764981564Z",
7880
"2021-04-18T12:54:44.764981564+09:00",
7981
"",
8082
},
8183
{
8284
"padding",
85+
"",
8386
"2021-04-18T03:54:44.764981500Z",
8487
"2021-04-18T12:54:44.764981500+09:00",
8588
"",
@@ -88,28 +91,40 @@ func TestUpdateTimezone(t *testing.T) {
8891
"timestamp required on non timestamp message",
8992
"",
9093
"",
94+
"",
9195
"missing timestamp",
9296
},
9397
{
9498
"not UTC",
99+
"",
95100
"2021-08-03T01:26:29.953994922+02:00",
96101
"2021-08-03T08:26:29.953994922+09:00",
97102
"",
98103
},
99104
{
100105
"RFC3339Nano format removed trailing zeros",
106+
"",
101107
"2021-06-20T08:20:30.331385Z",
102108
"2021-06-20T17:20:30.331385000+09:00",
103109
"",
104110
},
111+
{
112+
"Specified the short format",
113+
TimestampFormatShort,
114+
"2021-06-20T08:20:30.331385Z",
115+
"06-20 17:20:30",
116+
"",
117+
},
105118
}
106119

107-
tailOptions := &TailOptions{
108-
Location: location,
109-
}
110120
for _, tt := range tests {
111121
t.Run(tt.name, func(t *testing.T) {
112-
message, err := tailOptions.UpdateTimezone(tt.message)
122+
tailOptions := &TailOptions{
123+
Location: location,
124+
TimestampFormat: tt.format,
125+
}
126+
127+
message, err := tailOptions.UpdateTimezoneAndFormat(tt.message)
113128
if tt.expected != message {
114129
t.Errorf("expected %q, but actual %q", tt.expected, message)
115130
}

0 commit comments

Comments
 (0)