mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-29 15:02:30 +00:00
5e827fe36f
Currently we trace everything we are given; sometimes in rare but valid cases that can be multi-MiB strings. That gets logged and stays in memory for tracing, which can blow up quite a lot and be a source of problems and unexpected excessive resource usage. To mitigate this and reduce the chances of issues, this patch truncates very long messages to 8k utf8 characters. That's long enough to be useful for debugging, without running the risk of causing problems on extremely long messages. We may adjust this issue in the future if this limit becomes a problem.
190 lines
4.9 KiB
Go
190 lines
4.9 KiB
Go
package trace
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/nettrace"
|
|
"blitiri.com.ar/go/log"
|
|
)
|
|
|
|
type nopCloser struct{ io.Writer }
|
|
|
|
func (nopCloser) Close() error { return nil }
|
|
|
|
// setupLog replaces the default logger with one that writes to the returned
|
|
// buffer, at the given level. It is restored when the test finishes.
|
|
func setupLog(t *testing.T, level log.Level) *bytes.Buffer {
|
|
t.Helper()
|
|
buf := &bytes.Buffer{}
|
|
orig := log.Default
|
|
log.Default = log.New(nopCloser{buf})
|
|
log.Default.Level = level
|
|
log.Default.LogTime = false
|
|
log.Default.LogCaller = false
|
|
t.Cleanup(func() { log.Default = orig })
|
|
return buf
|
|
}
|
|
|
|
// traceEvents returns the rendered (HTML) active traces for the family.
|
|
func traceEvents(t *testing.T, family string) string {
|
|
t.Helper()
|
|
req := httptest.NewRequest("GET", "/debug/traces?fam="+family+"&b=-1", nil)
|
|
w := httptest.NewRecorder()
|
|
nettrace.RenderTraces(w, req)
|
|
return w.Body.String()
|
|
}
|
|
|
|
// countingStringer counts how many times it was formatted.
|
|
type countingStringer struct{ n *int }
|
|
|
|
func (c countingStringer) String() string {
|
|
*c.n++
|
|
return "stringer"
|
|
}
|
|
|
|
func TestFormatOnce(t *testing.T) {
|
|
for _, level := range []log.Level{log.Info, log.Debug} {
|
|
setupLog(t, level)
|
|
tr := New("TestFormatOnce", "title")
|
|
|
|
n := 0
|
|
tr.Debugf("%v", countingStringer{&n})
|
|
if n != 1 {
|
|
t.Errorf("level %d: Debugf formatted %d times, expected 1",
|
|
level, n)
|
|
}
|
|
|
|
n = 0
|
|
tr.Printf("%v", countingStringer{&n})
|
|
if n != 1 {
|
|
t.Errorf("level %d: Printf formatted %d times, expected 1",
|
|
level, n)
|
|
}
|
|
|
|
tr.Finish()
|
|
}
|
|
}
|
|
|
|
func TestLog(t *testing.T) {
|
|
// At info level, debug messages are not logged.
|
|
buf := setupLog(t, log.Info)
|
|
tr := New("TestLog", "title")
|
|
tr.Printf("info %d", 1)
|
|
tr.Debugf("debug %d", 2)
|
|
tr.Errorf("error %d", 3)
|
|
tr.Error(errors.New("error 4"))
|
|
tr.Finish()
|
|
|
|
expected := "_ TestLog title: info 1\n" +
|
|
"_ TestLog title: error: error 3\n" +
|
|
"_ TestLog title: error: error 4\n"
|
|
if got := buf.String(); got != expected {
|
|
t.Errorf("unexpected log at info level:\n got: %q\nwant: %q",
|
|
got, expected)
|
|
}
|
|
|
|
// At debug level, they are. Messages are quoted.
|
|
buf = setupLog(t, log.Debug)
|
|
tr = New("TestLog", "title")
|
|
tr.Debugf("debug %q\n", "x")
|
|
tr.Finish()
|
|
|
|
expected = `. TestLog title: debug \"x\"\n` + "\n"
|
|
if got := buf.String(); got != expected {
|
|
t.Errorf("unexpected log at debug level:\n got: %q\nwant: %q",
|
|
got, expected)
|
|
}
|
|
}
|
|
|
|
func TestTruncate(t *testing.T) {
|
|
rep := strings.Repeat
|
|
cases := []struct {
|
|
in, want string
|
|
}{
|
|
{"", ""},
|
|
{"short", "short"},
|
|
{rep("a", maxMsgLen), rep("a", maxMsgLen)},
|
|
{rep("a", maxMsgLen+1),
|
|
rep("a", maxMsgLen) + "… [truncated, 8193 bytes total]"},
|
|
|
|
// The limit is in characters, not bytes ("ñ" is 2 bytes).
|
|
{rep("ñ", maxMsgLen), rep("ñ", maxMsgLen)},
|
|
{rep("ñ", maxMsgLen+1),
|
|
rep("ñ", maxMsgLen) + "… [truncated, 16386 bytes total]"},
|
|
|
|
// Invalid UTF-8 bytes count as one character each, and are kept.
|
|
{rep("\xff", maxMsgLen+1),
|
|
rep("\xff", maxMsgLen) + "… [truncated, 8193 bytes total]"},
|
|
}
|
|
for i, c := range cases {
|
|
if got := truncate(c.in); got != c.want {
|
|
t.Errorf("%d: truncate(%d bytes) = %q (%d bytes), want %q",
|
|
i, len(c.in), got[max(0, len(got)-40):], len(got),
|
|
c.want[max(0, len(c.want)-40):])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTruncateMessages(t *testing.T) {
|
|
buf := setupLog(t, log.Debug)
|
|
tr := New("TestTruncateMessages", "title")
|
|
|
|
long := strings.Repeat("x", 3*maxMsgLen)
|
|
tr.Printf("printf %s", long)
|
|
tr.Debugf("debugf %s", long)
|
|
err := tr.Errorf("errorf %s", long)
|
|
tr.Error(errors.New("error " + long))
|
|
|
|
// The error returned by Errorf must not be truncated.
|
|
if len(err.Error()) != len("errorf ")+len(long) {
|
|
t.Errorf("Errorf returned a truncated error (%d bytes)",
|
|
len(err.Error()))
|
|
}
|
|
|
|
// All messages are truncated in the log...
|
|
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
|
if len(lines) != 4 {
|
|
t.Fatalf("expected 4 log lines, got %d", len(lines))
|
|
}
|
|
for _, l := range lines {
|
|
if len(l) > maxMsgLen+100 ||
|
|
!strings.HasSuffix(l, " bytes total]") {
|
|
t.Errorf("log line not truncated (%d bytes): ...%s",
|
|
len(l), l[max(0, len(l)-60):])
|
|
}
|
|
}
|
|
|
|
// ... and in the trace.
|
|
body := traceEvents(t, "TestTruncateMessages")
|
|
if n := strings.Count(body, " bytes total]"); n != 4 {
|
|
t.Errorf("expected 4 truncated events in the trace, got %d", n)
|
|
}
|
|
if len(body) > 4*maxMsgLen+10*1024 {
|
|
t.Errorf("trace too large (%d bytes), events not truncated?",
|
|
len(body))
|
|
}
|
|
tr.Finish()
|
|
}
|
|
|
|
func TestTraceEvents(t *testing.T) {
|
|
// Debug messages are added to the trace even if they are not logged.
|
|
setupLog(t, log.Info)
|
|
tr := New("TestTraceEvents", "title")
|
|
tr.Printf("info %d", 1)
|
|
tr.Debugf("debug %d", 2)
|
|
tr.Errorf("error %d", 3)
|
|
|
|
body := traceEvents(t, "TestTraceEvents")
|
|
for _, s := range []string{"info 1", "debug 2", "error: error 3"} {
|
|
if !strings.Contains(body, s) {
|
|
t.Errorf("trace does not contain %q:\n%s", s, body)
|
|
}
|
|
}
|
|
tr.Finish()
|
|
}
|