1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-10-28 15:02:28 +00:00
Files
go-chasquid-smtp/internal/trace/trace.go
T
Alberto Bertogli 5e827fe36f trace: Truncate long messages
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.
2026-10-12 19:29:40 +01:00

116 lines
3.1 KiB
Go

// Package trace extends nettrace with logging.
package trace
import (
"fmt"
"strconv"
"unicode/utf8"
"blitiri.com.ar/go/chasquid/internal/nettrace"
"blitiri.com.ar/go/log"
)
func init() {
}
// A Trace represents an active request.
type Trace struct {
family string
title string
t nettrace.Trace
}
// New trace.
func New(family, title string) *Trace {
t := &Trace{family, title, nettrace.New(family, title)}
// The default for max events is 10, which is a bit short for a normal
// SMTP exchange. Expand it to 100 which should be large enough to keep
// most of the traces.
t.t.SetMaxEvents(100)
return t
}
// NewChild creates a new child trace.
func (t *Trace) NewChild(family, title string) *Trace {
n := &Trace{family, title, t.t.NewChild(family, title)}
n.t.SetMaxEvents(100)
return n
}
// Printf adds this message to the trace's log.
func (t *Trace) Printf(format string, a ...any) {
t.printf(log.Info, format, a...)
}
// Debugf adds this message to the trace's log, with a debugging level.
func (t *Trace) Debugf(format string, a ...any) {
t.printf(log.Debug, format, a...)
}
func (t *Trace) printf(level log.Level, format string, a ...any) {
// Format the message only once, as it can be expensive.
msg := truncate(fmt.Sprintf(format, a...))
t.t.Printf("%s", msg)
// Only quote and log the message if it's going to be logged.
if log.V(level) {
log.Log(level, 2, "%s %s: %s", t.family, t.title, quote(msg))
}
}
// Errorf adds this message to the trace's log, with an error level.
func (t *Trace) Errorf(format string, a ...any) error {
// Note we can't just call t.Error here, as it breaks caller logging.
err := fmt.Errorf(format, a...)
msg := truncate(err.Error())
t.t.SetError()
t.t.Printf("error: %s", msg)
log.Log(log.Info, 1, "%s %s: error: %s", t.family, t.title,
quote(msg))
return err
}
// Error marks the trace as having seen an error, and also logs it to the
// trace's log.
func (t *Trace) Error(err error) error {
msg := truncate(err.Error())
t.t.SetError()
t.t.Printf("error: %s", msg)
log.Log(log.Info, 1, "%s %s: error: %s", t.family, t.title,
quote(msg))
return err
}
// Finish the trace. It should not be changed after this is called.
func (t *Trace) Finish() {
t.t.Finish()
}
// Maximum length of a message in the trace or the log, in characters (so up
// to 4x that in bytes); longer ones get truncated. Messages can contain
// attacker-controlled data (e.g. headers, DNS records, remote server
// responses), and traces are kept in memory after they finish, so we need a
// cap. It is set well above the normal message sizes, so in practice it only
// affects pathological cases.
const maxMsgLen = 8 * 1024
// truncate s to maxMsgLen characters, noting its original length.
func truncate(s string) string {
// Check the length in bytes first, as it's much cheaper, and it can't
// have more characters than bytes.
if len(s) <= maxMsgLen || utf8.RuneCountInString(s) <= maxMsgLen {
return s
}
return fmt.Sprintf("%.*s… [truncated, %d bytes total]",
maxMsgLen, s, len(s))
}
func quote(s string) string {
qs := strconv.Quote(s)
return qs[1 : len(qs)-1]
}