mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
fix: expand grouped log lines when copying to clipboard (#4771)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,18 +2,20 @@ package container
|
||||
|
||||
import "strings"
|
||||
|
||||
// stripControlBytes removes C0 control characters except tab (\x09), newline
|
||||
// (\x0a) and carriage return (\x0d). A NUL byte terminates the clipboard string
|
||||
// on Windows, dropping everything after it.
|
||||
func stripControlBytes(r rune) rune {
|
||||
if (r >= 0x00 && r <= 0x08) || r == 0x0b || r == 0x0c || (r >= 0x0e && r <= 0x1f) {
|
||||
return -1
|
||||
// PlainText renders a log event as plain text for clipboard copy, stripping
|
||||
// ANSI escape sequences. Grouped events carry their lines in fragments and
|
||||
// have an empty RawMessage, so they are expanded one line per fragment;
|
||||
// otherwise every grouped multi-line entry collapses to a single blank line on
|
||||
// copy.
|
||||
func (e *LogEvent) PlainText() string {
|
||||
if e.Type == LogTypeGroup {
|
||||
if fragments, ok := e.Message.([]LogFragment); ok {
|
||||
lines := make([]string, len(fragments))
|
||||
for i, fragment := range fragments {
|
||||
lines[i] = StripANSI(fragment.Message)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SanitizeForPlainText strips ANSI escape sequences and control bytes so log
|
||||
// text is safe to copy to the clipboard or open in a text editor.
|
||||
func SanitizeForPlainText(str string) string {
|
||||
return strings.Map(stripControlBytes, StripANSI(str))
|
||||
return StripANSI(e.RawMessage)
|
||||
}
|
||||
|
||||
@@ -2,23 +2,48 @@ package container
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSanitizeForPlainText(t *testing.T) {
|
||||
func TestLogEventPlainText(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
event *LogEvent
|
||||
expected string
|
||||
}{
|
||||
{"strips NUL bytes that truncate clipboard text on Windows", "before\x00after", "beforeafter"},
|
||||
{"strips other C0 control bytes", "a\x01b\x07c\x1fd", "abcd"},
|
||||
{"preserves tab, newline and carriage return", "a\tb\nc\r\nd", "a\tb\nc\r\nd"},
|
||||
{"removes ANSI escape sequences", "\x1b[31mred\x1b[0m text", "red text"},
|
||||
{"leaves plain text untouched", "plain log line", "plain log line"},
|
||||
{
|
||||
name: "single event returns ANSI-stripped raw message",
|
||||
event: &LogEvent{Type: LogTypeSingle, RawMessage: "\x1b[31mred\x1b[0m text"},
|
||||
expected: "red text",
|
||||
},
|
||||
{
|
||||
name: "complex event returns raw message",
|
||||
event: &LogEvent{Type: LogTypeComplex, RawMessage: `{"level":"info"}`},
|
||||
expected: `{"level":"info"}`,
|
||||
},
|
||||
{
|
||||
name: "grouped event expands every fragment line (otherwise lost on copy)",
|
||||
event: &LogEvent{
|
||||
Type: LogTypeGroup,
|
||||
Message: []LogFragment{
|
||||
{Message: "Job (17) starting"},
|
||||
{Message: "Job (17) done in 0.006s"},
|
||||
{Message: "Job (17) completed"},
|
||||
},
|
||||
},
|
||||
expected: "Job (17) starting\nJob (17) done in 0.006s\nJob (17) completed",
|
||||
},
|
||||
{
|
||||
name: "grouped fragments are ANSI-stripped",
|
||||
event: &LogEvent{
|
||||
Type: LogTypeGroup,
|
||||
Message: []LogFragment{{Message: "\x1b[31mred\x1b[0m"}, {Message: "plain"}},
|
||||
},
|
||||
expected: "red\nplain",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := SanitizeForPlainText(tt.input); got != tt.expected {
|
||||
t.Errorf("SanitizeForPlainText(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||
if got := tt.event.PlainText(); got != tt.expected {
|
||||
t.Errorf("PlainText() = %q, want %q", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,8 +197,10 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
if plainText {
|
||||
// Strip ANSI and control bytes; a NUL byte truncates clipboard text on Windows
|
||||
fmt.Fprintf(writer, "%s\n", container.SanitizeForPlainText(event.RawMessage))
|
||||
// Expand grouped events into their fragment lines; grouped
|
||||
// events store their lines in Message and have an empty
|
||||
// RawMessage, so writing RawMessage alone drops every group.
|
||||
fmt.Fprintf(writer, "%s\n", event.PlainText())
|
||||
} else if err := encoder.Encode(event); err != nil {
|
||||
log.Error().Err(err).Msg("error encoding log event")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user