-
Notifications
You must be signed in to change notification settings - Fork 0
Filter and color emulator log lines #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package container | ||
|
|
||
| import ( | ||
| "strings" | ||
|
|
||
| "github.com/localstack/lstk/internal/output" | ||
| ) | ||
|
|
||
| func shouldFilter(line string) bool { | ||
| if strings.Contains(line, "Docker not available") { | ||
| return true | ||
| } | ||
| _, logger := parseLogLine(line) | ||
| switch { | ||
| case logger == "localstack.request.http": | ||
| return true | ||
| case logger == "l.aws.handlers.internal": | ||
| return true | ||
| case strings.HasSuffix(logger, ".provider"): | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // Expected format: 2026-03-16T17:56:00.810 INFO --- [ MainThread] l.p.c.extensions.plugins : message | ||
| func parseLogLine(line string) (output.LogLevel, string) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should push for introducing structured logs again. This seems quite brittle, but it's the best we can do at the moment
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💯 log format could be configurable, plain text is better for humans but json would make it easier to parse them. However I think the biggest issue with the logs at the moment is the fact that are not consistent (some are formatted, others not)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Go PRO-236! 🤖 |
||
| sepIdx := strings.Index(line, " --- [") | ||
| if sepIdx < 0 { | ||
| return output.LogLevelUnknown, "" | ||
| } | ||
|
|
||
| prefix := strings.TrimSpace(line[:sepIdx]) | ||
| level := output.LogLevelUnknown | ||
| if spIdx := strings.LastIndex(prefix, " "); spIdx >= 0 { | ||
| switch prefix[spIdx+1:] { | ||
| case "DEBUG": | ||
| level = output.LogLevelDebug | ||
| case "INFO": | ||
| level = output.LogLevelInfo | ||
| case "WARN": | ||
| level = output.LogLevelWarn | ||
| case "ERROR": | ||
| level = output.LogLevelError | ||
| } | ||
| } | ||
|
|
||
| rest := line[sepIdx+6:] // skip " --- [" | ||
| bracketEnd := strings.Index(rest, "]") | ||
| if bracketEnd < 0 { | ||
| return level, "" | ||
| } | ||
| afterBracket := strings.TrimSpace(rest[bracketEnd+1:]) | ||
| colonIdx := strings.Index(afterBracket, " : ") | ||
| if colonIdx < 0 { | ||
| return level, "" | ||
| } | ||
| logger := strings.TrimSpace(afterBracket[:colonIdx]) | ||
| return level, logger | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| package container | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/localstack/lstk/internal/output" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestParseLogLine(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| line string | ||
| wantLevel output.LogLevel | ||
| wantLogger string | ||
| }{ | ||
| { | ||
| name: "INFO aws request", | ||
| line: "2026-03-16T17:56:43.472 INFO --- [et.reactor-0] localstack.request.aws : AWS iam.GetUser => 200", | ||
| wantLevel: output.LogLevelInfo, | ||
| wantLogger: "localstack.request.aws", | ||
| }, | ||
| { | ||
| name: "INFO http request", | ||
| line: "2026-03-16T17:58:36.596 INFO --- [et.reactor-1] localstack.request.http : GET /_localstack/resources => 200", | ||
| wantLevel: output.LogLevelInfo, | ||
| wantLogger: "localstack.request.http", | ||
| }, | ||
| { | ||
| name: "WARN internal handler", | ||
| line: "2026-03-16T18:10:35.149 WARN --- [et.reactor-1] l.aws.handlers.internal : Unable to find resource handler for path: /_localstack/appinspector/v1/traces/*/spans", | ||
| wantLevel: output.LogLevelWarn, | ||
| wantLogger: "l.aws.handlers.internal", | ||
| }, | ||
| { | ||
| name: "INFO provider log", | ||
| line: "2026-03-16T17:56:51.985 INFO --- [et.reactor-0] l.p.c.services.s3.provider : Using /tmp/localstack/state/s3 as storage path for s3 assets", | ||
| wantLevel: output.LogLevelInfo, | ||
| wantLogger: "l.p.c.services.s3.provider", | ||
| }, | ||
| { | ||
| name: "INFO extensions plugin", | ||
| line: "2026-03-16T17:56:00.810 INFO --- [ MainThread] l.p.c.extensions.plugins : loaded 0 extensions", | ||
| wantLevel: output.LogLevelInfo, | ||
| wantLogger: "l.p.c.extensions.plugins", | ||
| }, | ||
| { | ||
| name: "ERROR level", | ||
| line: "2026-03-16T17:56:00.810 ERROR --- [ MainThread] localstack.core : something failed", | ||
| wantLevel: output.LogLevelError, | ||
| wantLogger: "localstack.core", | ||
| }, | ||
| { | ||
| name: "non-standard line", | ||
| line: "Docker not available", | ||
| wantLevel: output.LogLevelUnknown, | ||
| wantLogger: "", | ||
| }, | ||
| { | ||
| name: "empty line", | ||
| line: "", | ||
| wantLevel: output.LogLevelUnknown, | ||
| wantLogger: "", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| level, logger := parseLogLine(tt.line) | ||
| assert.Equal(t, tt.wantLevel, level) | ||
| assert.Equal(t, tt.wantLogger, logger) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestShouldFilter(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| line string | ||
| filter bool | ||
| }{ | ||
| { | ||
| name: "HTTP request log", | ||
| line: "2026-03-16T17:58:36.596 INFO --- [et.reactor-1] localstack.request.http : GET /_localstack/resources => 200", | ||
| filter: true, | ||
| }, | ||
| { | ||
| name: "HTTP OPTIONS request", | ||
| line: "2026-03-16T18:10:35.142 INFO --- [et.reactor-4] localstack.request.http : OPTIONS /_localstack/appinspector/status => 204", | ||
| filter: true, | ||
| }, | ||
| { | ||
| name: "internal handler warning", | ||
| line: "2026-03-16T18:10:35.149 WARN --- [et.reactor-1] l.aws.handlers.internal : Unable to find resource handler for path: /_localstack/appinspector/v1/traces/*/spans", | ||
| filter: true, | ||
| }, | ||
| { | ||
| name: "provider debug log", | ||
| line: "2026-03-16T17:56:51.985 INFO --- [et.reactor-0] l.p.c.services.s3.provider : Using /tmp/localstack/state/s3 as storage path for s3 assets", | ||
| filter: true, | ||
| }, | ||
| { | ||
| name: "Docker not available", | ||
| line: "Docker not available", | ||
| filter: true, | ||
| }, | ||
| { | ||
| name: "AWS request log - kept", | ||
| line: "2026-03-16T17:56:43.472 INFO --- [et.reactor-0] localstack.request.aws : AWS iam.GetUser => 200", | ||
| filter: false, | ||
| }, | ||
| { | ||
| name: "extensions plugin log - kept", | ||
| line: "2026-03-16T17:56:00.810 INFO --- [ MainThread] l.p.c.extensions.plugins : loaded 0 extensions", | ||
| filter: false, | ||
| }, | ||
| { | ||
| name: "IAM plugin log - kept", | ||
| line: "2026-03-16T17:56:43.344 INFO --- [et.reactor-0] l.p.c.services.iam.plugins : Configured IAM provider to use Advance Policy Simulator", | ||
| filter: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| assert.Equal(t, tt.filter, shouldFilter(tt.line)) | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package ui | ||
|
|
||
| import ( | ||
| "strings" | ||
|
|
||
| "github.com/localstack/lstk/internal/output" | ||
| "github.com/localstack/lstk/internal/ui/styles" | ||
| ) | ||
|
|
||
| func renderLogLine(line string, level output.LogLevel) string { | ||
| idx := strings.Index(line, " : ") | ||
| if idx < 0 { | ||
| return renderLogMessage(line, level) | ||
| } | ||
| meta := line[:idx+3] // up to and including " : " | ||
| msg := line[idx+3:] | ||
| return styles.Secondary.Render(meta) + renderLogMessage(msg, level) | ||
| } | ||
|
|
||
| func renderLogMessage(s string, level output.LogLevel) string { | ||
| switch level { | ||
| case output.LogLevelWarn: | ||
| return styles.Warning.Render(s) | ||
| case output.LogLevelError: | ||
| return styles.LogError.Render(s) | ||
| default: | ||
| return s | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package ui | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "os" | ||
|
|
||
| tea "github.com/charmbracelet/bubbletea" | ||
| "github.com/localstack/lstk/internal/config" | ||
| "github.com/localstack/lstk/internal/container" | ||
| "github.com/localstack/lstk/internal/output" | ||
| "github.com/localstack/lstk/internal/runtime" | ||
| ) | ||
|
|
||
| func RunLogs(parentCtx context.Context, rt runtime.Runtime, containers []config.ContainerConfig, follow bool) error { | ||
| ctx, cancel := context.WithCancel(parentCtx) | ||
| defer cancel() | ||
|
|
||
| app := NewApp("", "", "", cancel, withoutHeader()) | ||
| p := tea.NewProgram(app, tea.WithInput(os.Stdin), tea.WithOutput(os.Stdout)) | ||
| runErrCh := make(chan error, 1) | ||
|
|
||
| go func() { | ||
| err := container.Logs(ctx, rt, output.NewTUISink(programSender{p: p}), containers, follow) | ||
| runErrCh <- err | ||
| if err != nil && !errors.Is(err, context.Canceled) { | ||
| p.Send(runErrMsg{err: err}) | ||
| return | ||
| } | ||
| p.Send(runDoneMsg{}) | ||
| }() | ||
|
|
||
| model, err := p.Run() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if app, ok := model.(App); ok && app.Err() != nil { | ||
| return output.NewSilentError(app.Err()) | ||
| } | ||
|
|
||
| runErr := <-runErrCh | ||
| if runErr != nil && !errors.Is(runErr, context.Canceled) { | ||
| return runErr | ||
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This one has a lot of comments in it :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks 😆
Removed.