Skip to content

Commit acd5e01

Browse files
committed
feat(cli): add --since, --tail, --json flags to tilt logs
Implements #6652 - adds filtering and JSON output capabilities to tilt logs: - --since: Filter logs by time (e.g., "5m", "1h", "30s") - --tail: Limit output to last N lines (applies only to initial history when combined with -f/--follow) - --json: Output logs as JSON Lines (JSONL) format - --json-fields: Configure JSON output fields ("minimal", "full", or comma-separated field list) Implementation details: - Extends LogFilter with Since/Tail fields and time-based filtering - Adds JSONPrinter for structured JSONL output with configurable fields - Uses pointer types in JSONLogLine to distinguish "not included" from "empty value" for --json-fields=full - LogStreamer tracks isFirstBatch to apply tail only to initial history - Validates --since (positive duration), --tail (>= -1), and --json-fields (rejects unknown field names) Closes #6652 Signed-off-by: Big Boss <bigboss@metalrodeo.xyz>
1 parent 41cc177 commit acd5e01

File tree

15 files changed

+1119
-54
lines changed

15 files changed

+1119
-54
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,9 @@ yarn-error.log*
6363
# http caches from running kubectl in integration tests
6464
integration/.kube
6565
.aider*
66+
67+
# Claude Code state files
68+
.claude/
69+
70+
# Built binary at repo root
71+
/tilt

internal/cli/flags.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ func addLogFilterFlags(cmd *cobra.Command, prefix string) {
122122
)
123123
}
124124

125+
func addLogOutputFlags(cmd *cobra.Command) {
126+
cmd.Flags().StringVar(&logSinceFlag, "since", "", `Only show logs since duration ago (e.g., "5m", "1h", "30s")`)
127+
cmd.Flags().IntVar(&logTailFlag, "tail", -1, `Number of lines to show from the end of logs (-1 for all)`)
128+
cmd.Flags().BoolVar(&logJSONFlag, "json", false, `Output logs in JSON Lines format`)
129+
cmd.Flags().StringVar(&logJSONFieldsFlag, "json-fields", "", `Fields to include in JSON output. Presets: "minimal" (default), "full". Or comma-separated: "time,resource,level,message"`)
130+
}
131+
125132
var kubeContextOverride string
126133

127134
func ProvideKubeContextOverride() k8s.KubeContextOverride {

internal/cli/logs.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ By default, looks for a running Tilt instance on localhost:10350
3535

3636
addConnectServerFlags(cmd)
3737
addLogFilterFlags(cmd, "")
38+
addLogOutputFlags(cmd)
3839
return cmd
3940
}
4041

@@ -56,5 +57,5 @@ func (c *logsCmd) run(ctx context.Context, args []string) error {
5657
return err
5758
}
5859

59-
return server.StreamLogs(ctx, c.follow, logDeps.url, logDeps.filter, logDeps.printer)
60+
return server.StreamLogs(ctx, c.follow, logDeps.url, logDeps.filter, logDeps.stdout)
6061
}

internal/cli/logs_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestProvideLogSinceValidation(t *testing.T) {
11+
testCases := []struct {
12+
name string
13+
flag string
14+
expectError bool
15+
errorMsg string
16+
}{
17+
{
18+
name: "empty is valid",
19+
flag: "",
20+
expectError: false,
21+
},
22+
{
23+
name: "positive duration is valid",
24+
flag: "5m",
25+
expectError: false,
26+
},
27+
{
28+
name: "negative duration is invalid",
29+
flag: "-5m",
30+
expectError: true,
31+
errorMsg: "must be positive",
32+
},
33+
{
34+
name: "invalid format returns parse error",
35+
flag: "notaduration",
36+
expectError: true,
37+
errorMsg: "invalid duration",
38+
},
39+
}
40+
41+
for _, tc := range testCases {
42+
t.Run(tc.name, func(t *testing.T) {
43+
// Save and restore the global flag
44+
oldFlag := logSinceFlag
45+
defer func() { logSinceFlag = oldFlag }()
46+
47+
logSinceFlag = tc.flag
48+
_, err := provideLogSince()
49+
50+
if tc.expectError {
51+
require.Error(t, err)
52+
assert.Contains(t, err.Error(), tc.errorMsg)
53+
} else {
54+
require.NoError(t, err)
55+
}
56+
})
57+
}
58+
}
59+
60+
func TestProvideLogTailValidation(t *testing.T) {
61+
testCases := []struct {
62+
name string
63+
flag int
64+
expectError bool
65+
errorMsg string
66+
}{
67+
{
68+
name: "-1 (no limit) is valid",
69+
flag: -1,
70+
expectError: false,
71+
},
72+
{
73+
name: "0 is valid",
74+
flag: 0,
75+
expectError: false,
76+
},
77+
{
78+
name: "positive is valid",
79+
flag: 100,
80+
expectError: false,
81+
},
82+
{
83+
name: "-2 is invalid",
84+
flag: -2,
85+
expectError: true,
86+
errorMsg: "must be -1 (no limit) or >= 0",
87+
},
88+
{
89+
name: "-100 is invalid",
90+
flag: -100,
91+
expectError: true,
92+
errorMsg: "must be -1 (no limit) or >= 0",
93+
},
94+
}
95+
96+
for _, tc := range testCases {
97+
t.Run(tc.name, func(t *testing.T) {
98+
// Save and restore the global flag
99+
oldFlag := logTailFlag
100+
defer func() { logTailFlag = oldFlag }()
101+
102+
logTailFlag = tc.flag
103+
_, err := provideLogTail()
104+
105+
if tc.expectError {
106+
require.Error(t, err)
107+
assert.Contains(t, err.Error(), tc.errorMsg)
108+
} else {
109+
require.NoError(t, err)
110+
}
111+
})
112+
}
113+
}

internal/cli/up.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ var webModeFlag model.WebMode = model.DefaultWebMode
3131
const DefaultWebDevPort = 46764
3232

3333
var (
34-
updateModeFlag string = string(liveupdates.UpdateModeAuto)
35-
webDevPort = 0
36-
logActionsFlag bool = false
37-
logSourceFlag string = ""
38-
logResourcesFlag []string = nil
39-
logLevelFlag string = ""
34+
updateModeFlag string = string(liveupdates.UpdateModeAuto)
35+
webDevPort = 0
36+
logActionsFlag bool = false
37+
logSourceFlag string = ""
38+
logResourcesFlag []string = nil
39+
logLevelFlag string = ""
40+
logSinceFlag string = ""
41+
logTailFlag int = -1 // -1 means no limit
42+
logJSONFlag bool = false
43+
logJSONFieldsFlag string = ""
4044
)
4145

4246
var userExitError = errors.New("user requested Tilt exit")

internal/cli/wire.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package cli
77

88
import (
99
"context"
10+
"fmt"
1011
"time"
1112

1213
"github.com/spf13/afero"
@@ -113,6 +114,10 @@ var BaseWireSet = wire.NewSet(
113114
provideLogSource,
114115
provideLogResources,
115116
provideLogLevel,
117+
provideLogSince,
118+
provideLogTail,
119+
provideLogJSON,
120+
provideLogJSONFields,
116121
hud.WireSet,
117122
prompt.WireSet,
118123
wire.Value(openurl.OpenURL(openurl.BrowserOpen)),
@@ -304,9 +309,9 @@ func wireLogsDeps(ctx context.Context, tiltAnalytics *analytics.TiltAnalytics, s
304309
}
305310

306311
type LogsDeps struct {
307-
url model.WebURL
308-
printer *hud.IncrementalPrinter
309-
filter hud.LogFilter
312+
url model.WebURL
313+
stdout hud.Stdout
314+
filter hud.LogFilter
310315
}
311316

312317
func provideClock() func() time.Time {
@@ -366,3 +371,32 @@ func provideLogLevel() hud.FilterLevel {
366371
return hud.FilterLevel(logger.NoneLvl)
367372
}
368373
}
374+
375+
func provideLogSince() (hud.FilterSince, error) {
376+
if logSinceFlag == "" {
377+
return hud.FilterSince(0), nil
378+
}
379+
d, err := time.ParseDuration(logSinceFlag)
380+
if err != nil {
381+
return 0, err
382+
}
383+
if d < 0 {
384+
return 0, fmt.Errorf("--since duration must be positive, got %v", d)
385+
}
386+
return hud.FilterSince(d), nil
387+
}
388+
389+
func provideLogTail() (hud.FilterTail, error) {
390+
if logTailFlag < -1 {
391+
return 0, fmt.Errorf("--tail must be -1 (no limit) or >= 0, got %d", logTailFlag)
392+
}
393+
return hud.FilterTail(logTailFlag), nil
394+
}
395+
396+
func provideLogJSON() hud.FilterJSON {
397+
return hud.FilterJSON(logJSONFlag)
398+
}
399+
400+
func provideLogJSONFields() hud.FilterJSONFields {
401+
return hud.FilterJSONFields(logJSONFieldsFlag)
402+
}

0 commit comments

Comments
 (0)