Skip to content

Commit 954e275

Browse files
committed
feat: Keep track of seen containers to set "Since" time more accurately
Before, logs where always truncated to latest container start. To get rid of this restriction containers are kept track of, not just if their active, but ever (as long as they fit in cache and are not removed) Log streams end when containers terminate and if the container starts again using the --since time will print logs that were already printed on earlier starts of the container. If the container has been seen earlier logs will be printed from latest start/finish time. Unless --since is later than the start time which is unlikely unless retrying a failed log stream is done (which is not merged yet).
1 parent 5bc0b58 commit 954e275

File tree

7 files changed

+75
-40
lines changed

7 files changed

+75
-40
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The `container query` is a regular expression of the container name; you could p
5252
`--no-follow` | `false` | Exit when all logs have been shown.
5353
`--only-log-lines` | `false` | Print only log lines
5454
`--output`, `-o` | `default` | Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]
55-
`--since`, `-s` | `48h0m0s` | Return logs newer than a relative duration like 5s, 2m, or 3h. The duration is truncated at container start time.
55+
`--since`, `-s` | `48h0m0s` | Return logs newer than a relative duration like 5s, 2m, or 3h.
5656
`--stdin` | `false` | Parse logs from stdin. All Docker related flags are ignored when it is set.
5757
`--tail` | `-1` | The number of lines from the end of the logs to show. Defaults to -1, showing all logs.
5858
`--template` | | Template to use for log lines, leave empty to use --output flag.

cmd/tailfincmd/cmd.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
348348
fs.StringArrayVarP(&o.label, "label", "l", o.label, "Label query to filter on. One `key` or `key=value` per flag instance.")
349349
fs.IntVar(&o.maxLogRequests, "max-log-requests", o.maxLogRequests, "Maximum number of concurrent logs to request. Defaults to 50, but 5 when specifying --no-follow")
350350
fs.StringVarP(&o.output, "output", "o", o.output, "Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]")
351-
fs.DurationVarP(&o.since, "since", "s", o.since, "Return logs newer than a relative duration like 5s, 2m, or 3h. The duration is truncated at container start time.")
351+
fs.DurationVarP(&o.since, "since", "s", o.since, "Return logs newer than a relative duration like 5s, 2m, or 3h.")
352352
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.")
353353
fs.StringVar(&o.template, "template", o.template, "Template to use for log lines, leave empty to use --output flag.")
354354
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.")

stern/docker_stern.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func RunDocker(ctx context.Context, client *dockerclient.Client, config *DockerC
3838
target.Tty,
3939
target.StartedAt,
4040
target.FinishedAt,
41+
target.SeenPreviously,
4142
config.Template,
4243
config.Out,
4344
config.ErrOut,
@@ -55,7 +56,9 @@ func RunDocker(ctx context.Context, client *dockerclient.Client, config *DockerC
5556
containerExcludeFilter: config.ExcludeContainerQuery,
5657
composeProjectFilter: config.ComposeProjectQuery,
5758
imageFilter: config.ImageQuery,
58-
})
59+
},
60+
max(config.MaxLogRequests*2, 100),
61+
)
5962

6063
tailTarget := func(target *DockerTarget) error {
6164
tail := newTail(target)

stern/docker_tail.go

+13-11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type DockerTail struct {
2525
Tty bool
2626
StartedAt time.Time
2727
FinishedAt string
28+
SeenPreviously bool
2829
composeColor *color.Color
2930
containerColor *color.Color
3031
Options *TailOptions
@@ -42,6 +43,7 @@ func NewDockerTail(
4243
tty bool,
4344
startedAt time.Time,
4445
finishedAt string,
46+
seenPreviously bool,
4547
tmpl *template.Template,
4648
out, errOut io.Writer,
4749
options *TailOptions,
@@ -56,6 +58,7 @@ func NewDockerTail(
5658
Tty: tty,
5759
StartedAt: startedAt,
5860
FinishedAt: finishedAt,
61+
SeenPreviously: seenPreviously,
5962
Options: options,
6063
composeColor: composeColor,
6164
containerColor: containerColor,
@@ -99,22 +102,21 @@ func (t *DockerTail) Close() {
99102
close(t.closed)
100103
}
101104

102-
// The maximum "since time" is the time the container was last started. Because logs are sometimes missing when using
103-
// the start time, the finished time is used instead (because there are no logs between the finish and start time). If
104-
// the Options.SinceTime is after the finished time it'll be used with the same reasoning in addition to it might be
105-
// after the start time, which in that case is the desired "since time".
105+
// Since log streams end when containers terminate tailfin must keep track of when a container has been previously
106+
// tailed and use a "since"-time from last finish/start. Otherwise it will include logs from previous starts thus
107+
// printing logs already printed.
106108
func (t *DockerTail) getSinceTime() time.Time {
107-
finished, err := time.Parse(time.RFC3339, t.FinishedAt)
108-
// If there's no finish time it should mean the container only started once so it's safe to use the options time. The
109-
// same applies if the finish time is earlier than the option time.
110-
if err != nil ||
111-
finished.Before(t.Options.DockerSinceTime) ||
109+
if !t.SeenPreviously ||
112110
t.StartedAt.Before(t.Options.DockerSinceTime) {
113111
return t.Options.DockerSinceTime
114112
}
115113

116-
// Sometimes early logs are missing if StartedAt is used
117-
if finished.Before(t.StartedAt) {
114+
// If there's no finish time it should mean the container only started once so it's safe to use the options time.
115+
finished, err := time.Parse(time.RFC3339, t.FinishedAt)
116+
if err != nil {
117+
return t.Options.DockerSinceTime
118+
} else if finished.Before(t.StartedAt) {
119+
// Sometimes early logs are missing if StartedAt is used
118120
return finished
119121
}
120122
return t.StartedAt

stern/docker_tail_test.go

+31-22
Original file line numberDiff line numberDiff line change
@@ -15,71 +15,80 @@ func TestGetSinceTime(t *testing.T) {
1515
startTime time.Time
1616
finishTime string
1717
optionsTime time.Time
18+
seen bool
1819
expected time.Time
1920
}{
2021
{
21-
desc: "invalid finish, earlier start",
22+
desc: "invalid finish, seen",
2223
startTime: t1,
2324
finishTime: "",
2425
optionsTime: t2,
26+
seen: true,
2527
expected: t2,
2628
},
2729
{
28-
desc: "invalid finish, later start",
29-
startTime: t2,
30-
finishTime: "",
31-
optionsTime: t1,
32-
expected: t1,
33-
},
34-
{
35-
desc: "finish earlier than option",
30+
desc: "finish earlier than option, seen",
3631
startTime: t3,
3732
finishTime: t1.Format(time.RFC3339),
3833
optionsTime: t2,
39-
expected: t2,
34+
seen: true,
35+
expected: t1,
4036
},
4137
{
42-
desc: "start earlier than option",
38+
desc: "start earlier than option, seen",
4339
startTime: t1,
4440
finishTime: t3.Format(time.RFC3339),
4541
optionsTime: t2,
42+
seen: true,
4643
expected: t2,
4744
},
4845
{
49-
desc: "option later than both 1",
46+
desc: "option later than both, seen",
5047
startTime: t2,
5148
finishTime: t1.Format(time.RFC3339),
5249
optionsTime: t3,
50+
seen: true,
5351
expected: t3,
5452
},
5553
{
56-
desc: "option later than both 2",
57-
startTime: t1,
58-
finishTime: t2.Format(time.RFC3339),
59-
optionsTime: t3,
60-
expected: t3,
61-
},
62-
{
63-
desc: "truncated to start",
54+
desc: "truncated to start, seen",
6455
startTime: t2,
6556
finishTime: t3.Format(time.RFC3339),
6657
optionsTime: t1,
58+
seen: true,
6759
expected: t2,
6860
},
6961
{
70-
desc: "truncated to finish",
62+
desc: "truncated to finish, seen",
7163
startTime: t3,
7264
finishTime: t2.Format(time.RFC3339),
7365
optionsTime: t1,
66+
seen: true,
7467
expected: t2,
7568
},
69+
{
70+
desc: "start earlier than option, not seen",
71+
startTime: t1,
72+
finishTime: t2.Format(time.RFC3339),
73+
optionsTime: t3,
74+
seen: false,
75+
expected: t3,
76+
},
77+
{
78+
desc: "start later than option, not seen",
79+
startTime: t2,
80+
finishTime: t3.Format(time.RFC3339),
81+
optionsTime: t1,
82+
seen: false,
83+
expected: t1,
84+
},
7685
}
7786
for _, tt := range tests {
7887
t.Run(tt.desc, func(t *testing.T) {
7988
options := &TailOptions{
8089
DockerSinceTime: tt.optionsTime,
8190
}
82-
tail := NewDockerTail(nil, "", "", "", false, tt.startTime, tt.finishTime, nil, io.Discard, io.Discard, options)
91+
tail := NewDockerTail(nil, "", "", "", false, tt.startTime, tt.finishTime, tt.seen, nil, io.Discard, io.Discard, options)
8392

8493
actual := tail.getSinceTime()
8594
if tt.expected != actual {

stern/docker_target.go

+21-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/docker/docker/api/types"
10+
"github.com/hashicorp/golang-lru/v2"
1011
"k8s.io/klog/v2"
1112
)
1213

@@ -17,6 +18,7 @@ type DockerTarget struct {
1718
Tty bool
1819
StartedAt time.Time
1920
FinishedAt string
21+
SeenPreviously bool
2022
}
2123

2224
type dockerTargetFilterConfig struct {
@@ -29,13 +31,20 @@ type dockerTargetFilterConfig struct {
2931
type dockerTargetFilter struct {
3032
config dockerTargetFilterConfig
3133
activeContainers map[string]time.Time
34+
seenContainers *lru.Cache[string, bool]
3235
mu sync.RWMutex
3336
}
3437

35-
func newDockerTargetFilter(filterConfig dockerTargetFilterConfig) *dockerTargetFilter {
38+
func newDockerTargetFilter(filterConfig dockerTargetFilterConfig, lruCacheSize int) *dockerTargetFilter {
39+
lru, err := lru.New[string, bool](lruCacheSize)
40+
if err != nil {
41+
panic(err)
42+
}
43+
3644
return &dockerTargetFilter{
3745
config: filterConfig,
3846
activeContainers: make(map[string]time.Time),
47+
seenContainers: lru,
3948
}
4049
}
4150

@@ -73,6 +82,7 @@ func (f *dockerTargetFilter) visit(container types.ContainerJSON, visitor func(t
7382
Tty: container.Config.Tty,
7483
StartedAt: startedAt,
7584
FinishedAt: container.State.FinishedAt,
85+
SeenPreviously: f.seenContainers.Contains(container.ID),
7686
}
7787

7888
if f.shouldAdd(target) {
@@ -95,6 +105,7 @@ func (f *dockerTargetFilter) shouldAdd(t *DockerTarget) bool {
95105
return false
96106
}
97107

108+
f.seenContainers.Add(t.Id, true)
98109
return true
99110
}
100111

@@ -151,14 +162,21 @@ func (f *dockerTargetFilter) matchingImageFilter(containerImage string) bool {
151162
return false
152163
}
153164

154-
func (f *dockerTargetFilter) forget(containerId string) {
165+
func (f *dockerTargetFilter) inactive(containerId string) {
166+
f.seenContainers.Add(containerId, true)
155167
f.mu.Lock()
156168
defer f.mu.Unlock()
157169

158-
klog.V(7).InfoS("Forget container", "target", containerId)
170+
klog.V(7).InfoS("Inactive container", "target", containerId)
159171
delete(f.activeContainers, containerId)
160172
}
161173

174+
func (f *dockerTargetFilter) forget(containerId string) {
175+
klog.V(7).InfoS("Forget container", "target", containerId)
176+
// Actively remove container from LRU cache to minimize the risk of old (but not removed) containers getting evicted
177+
f.seenContainers.Remove(containerId)
178+
}
179+
162180
func (f *dockerTargetFilter) isActive(t *DockerTarget) bool {
163181
f.mu.RLock()
164182
defer f.mu.RUnlock()

stern/docker_watch.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func WatchDockers(ctx context.Context, config *DockerConfig, filter *dockerTarge
1414
added := make(chan *DockerTarget)
1515
go func() {
1616
visitor := func(t *DockerTarget) {
17-
klog.V(7).InfoS("New container", "id", t.Id, "name", t.Name)
17+
klog.V(7).InfoS("Active container", "id", t.Id, "name", t.Name)
1818
added <- t
1919
}
2020
ctx, cancel := context.WithCancel(ctx)
@@ -24,6 +24,7 @@ func WatchDockers(ctx context.Context, config *DockerConfig, filter *dockerTarge
2424
args := filters.NewArgs()
2525
args.Add("event", string(events.ActionDie))
2626
args.Add("event", string(events.ActionStart))
27+
args.Add("event", string(events.ActionDestroy))
2728
for _, label := range config.Label {
2829
args.Add("label", label)
2930
}
@@ -52,6 +53,8 @@ func WatchDockers(ctx context.Context, config *DockerConfig, filter *dockerTarge
5253
klog.V(8).InfoS("Container inspect", "id", e.ID, "container JSON", container)
5354
filter.visit(container, visitor)
5455
case events.ActionDie:
56+
filter.inactive(e.ID)
57+
case events.ActionDestroy:
5558
filter.forget(e.ID)
5659
}
5760
case <-ctx.Done():

0 commit comments

Comments
 (0)