Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.

Commit 474682e

Browse files
authored
Merge pull request #1920 from njhale/events/filter-time
Enable filtering acorn events output to a given time span
2 parents 0a30e5c + 139d7c8 commit 474682e

File tree

6 files changed

+156
-11
lines changed

6 files changed

+156
-11
lines changed

docs/docs/100-reference/01-command-line/acorn_events.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ acorn events [flags] [PREFIX]
4444
# Get a single event by name
4545
acorn events 4b2ba097badf2031c4718609b9179fb5
4646
47+
# Filtering by Time
48+
# The --since and --until options can be Unix timestamps, date formatted timestamps, or Go duration strings (relative to system time).
49+
# List events observed within the last 15 minutes
50+
acorn events --since 15m
51+
52+
# List events observed between 2023-05-08T15:04:05 and 2023-05-08T15:05:05 (inclusive)
53+
acorn events --since '2023-05-08T15:04:05' --until '2023-05-08T15:05:05'
54+
4755
```
4856

4957
### Options
@@ -52,7 +60,9 @@ acorn events [flags] [PREFIX]
5260
-f, --follow Follow the event log
5361
-h, --help help for events
5462
-o, --output string Output format (json, yaml, {{gotemplate}})
63+
-s, --since string Show all events created since timestamp
5564
-t, --tail int Return this number of latest events
65+
-u, --until string Stream events until this timestamp
5666
```
5767

5868
### Options inherited from parent commands

pkg/apis/api.acorn.io/v1/scheme.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func AddToSchemeWithGV(scheme *runtime.Scheme, schemeGroupVersion schema.GroupVe
9191
gvk := schemeGroupVersion.WithKind("Event")
9292
flcf := func(label, value string) (string, string, error) {
9393
switch label {
94-
case "prefix", "details", "metadata.name", "metadata.namespace":
94+
case "prefix", "since", "until", "details", "metadata.name", "metadata.namespace":
9595
return label, value, nil
9696
}
9797
return "", "", fmt.Errorf("unsupported field selection [%s]", label)

pkg/apis/internal.acorn.io/v1/event.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ func (e EventInstance) GetObserved() MicroTime {
6565
// It extends metav1.MicroTime to allow unmarshaling from RFC3339.
6666
type MicroTime metav1.MicroTime
6767

68+
func NewMicroTime(t time.Time) MicroTime {
69+
return MicroTime(metav1.NewMicroTime(t))
70+
}
71+
72+
func NowMicro() MicroTime {
73+
return NewMicroTime(time.Now())
74+
}
75+
6876
// DeepCopyInto returns a deep-copy of the MicroTime value. The underlying time.Time
6977
// type is effectively immutable in the time API, so it is safe to
7078
// copy-by-assign, despite the presence of (unexported) Pointer fields.

pkg/cli/events.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,23 @@ func NewEvent(c CommandContext) *cobra.Command {
4646
4747
# Get a single event by name
4848
acorn events 4b2ba097badf2031c4718609b9179fb5
49+
50+
# Filtering by Time
51+
# The --since and --until options can be Unix timestamps, date formatted timestamps, or Go duration strings (relative to system time).
52+
# List events observed within the last 15 minutes
53+
acorn events --since 15m
54+
55+
# List events observed between 2023-05-08T15:04:05 and 2023-05-08T15:05:05 (inclusive)
56+
acorn events --since '2023-05-08T15:04:05' --until '2023-05-08T15:05:05'
4957
`})
5058
return cmd
5159
}
5260

5361
type Events struct {
5462
Tail int `usage:"Return this number of latest events" short:"t"`
5563
Follow bool `usage:"Follow the event log" short:"f"`
64+
Since string `usage:"Show all events created since timestamp" short:"s"`
65+
Until string `usage:"Stream events until this timestamp" short:"u"`
5666
Output string `usage:"Output format (json, yaml, {{gotemplate}})" short:"o"`
5767
client ClientFactory
5868
}
@@ -66,6 +76,8 @@ func (e *Events) Run(cmd *cobra.Command, args []string) error {
6676
opts := &client.EventStreamOptions{
6777
Tail: e.Tail,
6878
Follow: e.Follow,
79+
Since: e.Since,
80+
Until: e.Until,
6981
}
7082

7183
if len(args) > 0 {

pkg/client/client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@ type EventStreamOptions struct {
338338
Tail int `json:"tail,omitempty"`
339339
Follow bool `json:"follow,omitempty"`
340340
Prefix string `json:"prefix,omitempty"`
341+
Since string `json:"since,omitempty"`
342+
Until string `json:"until,omitempty"`
341343
ResourceVersion string `json:"resourceVersion,omitempty"`
342344
}
343345

@@ -346,6 +348,12 @@ func (o EventStreamOptions) ListOptions() *kclient.ListOptions {
346348
if o.Prefix != "" {
347349
fieldSet["prefix"] = o.Prefix
348350
}
351+
if o.Since != "" {
352+
fieldSet["since"] = o.Since
353+
}
354+
if o.Until != "" {
355+
fieldSet["until"] = o.Until
356+
}
349357

350358
// Set details selector to get details from older runtime APIs that don't return details by default.
351359
fieldSet["details"] = strconv.FormatBool(true)

pkg/server/registry/apigroups/acorn/events/strategy.go

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ package events
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"sort"
8+
"strconv"
79
"strings"
10+
"time"
811

912
"github.com/acorn-io/mink/pkg/strategy"
1013
"github.com/acorn-io/mink/pkg/types"
1114
apiv1 "github.com/acorn-io/runtime/pkg/apis/api.acorn.io/v1"
12-
v1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
15+
internalv1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
1316
"github.com/acorn-io/runtime/pkg/channels"
17+
"github.com/acorn-io/z"
1418
"github.com/sirupsen/logrus"
1519
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1620
"k8s.io/apimachinery/pkg/watch"
@@ -83,7 +87,7 @@ func setDefaults(ctx context.Context, e *apiv1.Event) *apiv1.Event {
8387
}
8488

8589
if e.Observed.IsZero() {
86-
e.Observed = v1.MicroTime(metav1.NowMicro())
90+
e.Observed = internalv1.NowMicro()
8791
}
8892

8993
return e
@@ -97,6 +101,12 @@ type query struct {
97101
// Only events with matching names or source strings are included in query results.
98102
// As a special case, the empty string "" matches all events.
99103
prefix prefix
104+
105+
// since excludes events observed before it when not nil.
106+
since *internalv1.MicroTime
107+
108+
// until excludes events observed after it when not nil.
109+
until *internalv1.MicroTime
100110
}
101111

102112
// filterChannel applies the query to every event received from unfiltered and forwards the result to filtered, if any.
@@ -131,7 +141,7 @@ func (q query) filterEvent(e watch.Event) *watch.Event {
131141
return &e
132142
}
133143

134-
// Attempt to filter
144+
// Filter
135145
obj := e.Object.(*apiv1.Event)
136146
filtered := q.filter(*obj)
137147
if len(filtered) < 1 {
@@ -144,6 +154,24 @@ func (q query) filterEvent(e watch.Event) *watch.Event {
144154
return &e
145155
}
146156

157+
func (q query) afterWindow(observation internalv1.MicroTime) bool {
158+
if q.until == nil {
159+
// Window includes all future events
160+
return false
161+
}
162+
163+
return observation.After(q.until.Time)
164+
}
165+
166+
func (q query) beforeWindow(observation internalv1.MicroTime) bool {
167+
if q.since == nil {
168+
// Window includes all existing events
169+
return false
170+
}
171+
172+
return observation.Before(q.since.Time)
173+
}
174+
147175
// filter returns the result of applying the query to a slice of events.
148176
func (q query) filter(events ...apiv1.Event) []apiv1.Event {
149177
if len(events) < 1 {
@@ -161,15 +189,19 @@ func (q query) filter(events ...apiv1.Event) []apiv1.Event {
161189
tail = int(q.tail)
162190
}
163191

164-
if q.prefix.all() {
165-
// Query selects all remaining events
166-
return events[len(events)-tail:]
167-
}
168-
169192
results := make([]apiv1.Event, 0, tail)
170193
for _, event := range events {
171-
if !q.prefix.matches(event) {
172-
// Exclude from results
194+
observed := event.Observed
195+
if q.afterWindow(observed) {
196+
// Exclude all events observed after the observation window ends.
197+
// Since the slice is sorted chronologically, we can stop filtering here.
198+
break
199+
}
200+
201+
if q.beforeWindow(observed) || !q.prefix.matches(event) {
202+
// Exclude events:
203+
// - observed before the observation window starts
204+
// - that don't match the given prefix
173205
continue
174206
}
175207

@@ -187,13 +219,18 @@ func (q query) filter(events ...apiv1.Event) []apiv1.Event {
187219
func stripQuery(opts storage.ListOptions) (q query, stripped storage.ListOptions, err error) {
188220
stripped = opts
189221

222+
now := internalv1.NowMicro()
190223
stripped.Predicate.Field, err = stripped.Predicate.Field.Transform(func(f, v string) (string, string, error) {
191224
var err error
192225
switch f {
193226
case "details":
194227
// Detail elision is deprecated, so clients should always get details.
195228
// We still strip it from the selector here in order to maintain limited backwards compatibility with old
196229
// clients that still specify it.
230+
case "since":
231+
q.since, err = parseTimeBound(v, now, true)
232+
case "until":
233+
q.until, err = parseTimeBound(v, now, false)
197234
case "prefix":
198235
q.prefix = prefix(v)
199236
default:
@@ -211,6 +248,76 @@ func stripQuery(opts storage.ListOptions) (q query, stripped storage.ListOptions
211248
return
212249
}
213250

251+
// parseTimeBound parses a time bound from a string.
252+
//
253+
// It attempts to parse raw as one of the following formats, in order, returning the result of the first successful parse:
254+
// 1. Go duration; e.g. "5m"
255+
// - time is calculated relative to now
256+
// - if since is true, then the duration is subtracted from now, otherwise it is added
257+
//
258+
// 2. RFC3339; e.g. "2006-01-02T15:04:05Z07:00"
259+
// 3. RFC3339Micro; e.g. "2006-01-02T15:04:05.999999Z07:00"
260+
// 4. Unix timestamp; e.g. "1136239445"
261+
func parseTimeBound(raw string, now internalv1.MicroTime, since bool) (*internalv1.MicroTime, error) {
262+
// Try to parse raw as a duration string
263+
var errs []error
264+
duration, err := time.ParseDuration(raw)
265+
if err == nil {
266+
if since {
267+
duration *= -1
268+
}
269+
270+
return z.P(internalv1.NewMicroTime(now.Add(duration))), nil
271+
}
272+
errs = append(errs, fmt.Errorf("%s is not a valid duration: %w", raw, err))
273+
274+
// Try to parse raw as a time string
275+
t, err := parseTime(raw)
276+
if err == nil {
277+
return t, nil
278+
}
279+
errs = append(errs, fmt.Errorf("%s is not a valid time: %w", raw, err))
280+
281+
// Try to parse raw as a unix timestamp
282+
unix, err := parseUnix(raw)
283+
if err == nil {
284+
return unix, nil
285+
}
286+
errs = append(errs, fmt.Errorf("%s is not a valid unix timestamp: %w", raw, err))
287+
288+
return nil, errors.Join(errs...)
289+
}
290+
291+
var (
292+
supportedLayouts = []string{
293+
time.RFC3339,
294+
metav1.RFC3339Micro,
295+
}
296+
)
297+
298+
func parseTime(raw string) (*internalv1.MicroTime, error) {
299+
var errs []error
300+
for _, layout := range supportedLayouts {
301+
t, err := time.Parse(layout, raw)
302+
if err == nil {
303+
return z.P(internalv1.NewMicroTime(t)), nil
304+
}
305+
306+
errs = append(errs, err)
307+
}
308+
309+
return nil, errors.Join(errs...)
310+
}
311+
312+
func parseUnix(raw string) (*internalv1.MicroTime, error) {
313+
sec, err := strconv.ParseInt(raw, 10, 64)
314+
if err != nil {
315+
return nil, err
316+
}
317+
318+
return z.P(internalv1.NewMicroTime(time.Unix(sec, 0))), nil
319+
}
320+
214321
type prefix string
215322

216323
func (p prefix) matches(e apiv1.Event) bool {

0 commit comments

Comments
 (0)