// Package tracelog renders request-grouped logs as traces. // // A trace is the set of log lines sharing a request ID. Render prints one // block per trace: a header summarizing the request, then each line with its // offset from the start of the trace: // // 2026-07-06 12:29:20 req-06FKH... POST /Identity_Login 500 1ms // +0s INFO http request agent=curl/8.7.1 remote=[::1]:52161 // +638µs ERROR invalid credentials // +638µs ERROR http response error=invalid credentials written=34 // // Collector renders traces live as requests complete, from any stream of // slog JSON lines (ParseLine). The same code serves dev (cmd/dev tails the // server it starts) and production (the traces CLI reads server log files), // so both environments see identical output. package tracelog import ( "fmt" "io" "strings" "time" ) type Attr struct { Key string Value string } type Entry struct { Time time.Time Level string Msg string Attrs []Attr } // Render writes one trace block: a header line summarizing the request, // then each entry with its offset from the first entry. The method, path, // status, and duration attributes of the "http request" and "http response" // entries are hoisted into the header. func Render(w io.Writer, id string, entries []Entry) { render(w, id, entries, false) } func render(w io.Writer, id string, entries []Entry, color bool) { var method, path, status, dur string rows := make([]Entry, len(entries)) copy(rows, entries) for i := range rows { switch rows[i].Msg { case "http request": method = pop(&rows[i], "method") path = pop(&rows[i], "path") case "http response": status = pop(&rows[i], "status") dur = pop(&rows[i], "duration") } } start := rows[0].Time fmt.Fprintf(w, "%s %s", start.Format("2006-01-02 15:04:05"), id) if method != "" { fmt.Fprintf(w, " %s %s", method, path) } if status != "" { fmt.Fprintf(w, " %s %s", status, dur) } if a := Anomalies(entries); len(a) > 0 { callout := "!! " + strings.Join(a, ", ") if color { callout = "\x1b[33m" + callout + "\x1b[0m" } fmt.Fprintf(w, " %s", callout) } fmt.Fprintln(w) for _, row := range rows { offset := "+" + FormatDuration(row.Time.Sub(start)) fmt.Fprintf(w, " %-8s %-5s %s%s\n", offset, row.Level, row.Msg, formatAttrs(row.Attrs)) } fmt.Fprintln(w) } // pop removes the named attribute from the entry and returns its value. func pop(e *Entry, key string) string { for i, a := range e.Attrs { if a.Key == key { e.Attrs = append(append([]Attr{}, e.Attrs[:i]...), e.Attrs[i+1:]...) return a.Value } } return "" } func formatAttrs(attrs []Attr) string { var b strings.Builder for _, a := range attrs { fmt.Fprintf(&b, " %s=%s", a.Key, a.Value) } return b.String() } // FormatDuration trims a duration to a readable precision for log output, // e.g. "12.35ms" rather than "12.345678ms". func FormatDuration(d time.Duration) string { switch { case d >= time.Second: d = d.Round(10 * time.Millisecond) case d >= time.Millisecond: d = d.Round(10 * time.Microsecond) default: d = d.Round(time.Microsecond) } return d.String() }