From 394c476f2aa7bb282bad4f7ebb43d4f29581618d Mon Sep 17 00:00:00 2001 From: henrygd Date: Sun, 30 Nov 2025 14:36:00 -0500 Subject: [PATCH] strip ansi escape sequences from docker logs (#1478) --- agent/docker.go | 12 +++++++++- agent/docker_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/agent/docker.go b/agent/docker.go index 929aed6f..b01e828a 100644 --- a/agent/docker.go +++ b/agent/docker.go @@ -14,6 +14,7 @@ import ( "net/url" "os" "path" + "regexp" "strings" "sync" "time" @@ -24,6 +25,10 @@ import ( "github.com/blang/semver" ) +// ansiEscapePattern matches ANSI escape sequences (colors, cursor movement, etc.) +// This includes CSI sequences like \x1b[...m and simple escapes like \x1b[K +var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]`) + const ( // Docker API timeout in milliseconds dockerTimeoutMs = 2100 @@ -692,7 +697,12 @@ func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (strin return "", err } - return builder.String(), nil + // Strip ANSI escape sequences from logs for clean display in web UI + logs := builder.String() + if strings.Contains(logs, "\x1b") { + logs = ansiEscapePattern.ReplaceAllString(logs, "") + } + return logs, nil } func decodeDockerLogStream(reader io.Reader, builder *strings.Builder) error { diff --git a/agent/docker_test.go b/agent/docker_test.go index b07d80c9..e6f9326c 100644 --- a/agent/docker_test.go +++ b/agent/docker_test.go @@ -1203,3 +1203,60 @@ func TestShouldExcludeContainer(t *testing.T) { }) } } + +func TestAnsiEscapePattern(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no ANSI codes", + input: "Hello, World!", + expected: "Hello, World!", + }, + { + name: "simple color code", + input: "\x1b[34mINFO\x1b[0m client mode", + expected: "INFO client mode", + }, + { + name: "multiple color codes", + input: "\x1b[31mERROR\x1b[0m: \x1b[33mWarning\x1b[0m message", + expected: "ERROR: Warning message", + }, + { + name: "bold and color", + input: "\x1b[1;32mSUCCESS\x1b[0m", + expected: "SUCCESS", + }, + { + name: "cursor movement codes", + input: "Line 1\x1b[KLine 2", + expected: "Line 1Line 2", + }, + { + name: "256 color code", + input: "\x1b[38;5;196mRed text\x1b[0m", + expected: "Red text", + }, + { + name: "RGB/truecolor code", + input: "\x1b[38;2;255;0;0mRed text\x1b[0m", + expected: "Red text", + }, + { + name: "mixed content with newlines", + input: "\x1b[34m2024-01-01 12:00:00\x1b[0m INFO Starting\n\x1b[31m2024-01-01 12:00:01\x1b[0m ERROR Failed", + expected: "2024-01-01 12:00:00 INFO Starting\n2024-01-01 12:00:01 ERROR Failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ansiEscapePattern.ReplaceAllString(tt.input, "") + assert.Equal(t, tt.expected, result) + }) + } +} +