mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-19 14:57:04 +00:00
nettrace: Add a new tracing library
This commit introduces a new tracing library, that replaces golang.org/x/net/trace, and supports (amongts other thing) nested traces. This is a minimal change, future patches will make use of the new functionality.
This commit is contained in:
259
internal/nettrace/http.go
Normal file
259
internal/nettrace/http.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package nettrace
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed "templates/*.tmpl" "templates/*.css"
|
||||
var templatesFS embed.FS
|
||||
var top *template.Template
|
||||
|
||||
func init() {
|
||||
top = template.Must(
|
||||
template.New("_top").Funcs(template.FuncMap{
|
||||
"stripZeros": stripZeros,
|
||||
"roundSeconds": roundSeconds,
|
||||
"roundDuration": roundDuration,
|
||||
"colorize": colorize,
|
||||
"depthspan": depthspan,
|
||||
"shorttitle": shorttitle,
|
||||
"traceemoji": traceemoji,
|
||||
}).ParseFS(templatesFS, "templates/*"))
|
||||
}
|
||||
|
||||
// RegisterHandler registers a the trace handler in the given ServeMux, on
|
||||
// `/debug/traces`.
|
||||
func RegisterHandler(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/debug/traces", RenderTraces)
|
||||
}
|
||||
|
||||
// RenderTraces is an http.Handler that renders the tracing information.
|
||||
func RenderTraces(w http.ResponseWriter, req *http.Request) {
|
||||
data := &struct {
|
||||
Buckets *[]time.Duration
|
||||
FamTraces map[string]*familyTraces
|
||||
|
||||
// When displaying traces for a specific family.
|
||||
Family string
|
||||
Bucket int
|
||||
BucketStr string
|
||||
AllGT bool
|
||||
Traces []*trace
|
||||
|
||||
// When displaying latencies for a specific family.
|
||||
Latencies *histSnapshot
|
||||
|
||||
// When displaying a specific trace.
|
||||
Trace *trace
|
||||
AllEvents []traceAndEvent
|
||||
|
||||
// Error to show to the user.
|
||||
Error string
|
||||
}{}
|
||||
|
||||
// Reference the common buckets, no need to copy them.
|
||||
data.Buckets = &buckets
|
||||
|
||||
// Copy the family traces map, so we don't have to keep it locked for too
|
||||
// long. We'll still need to lock individual entries.
|
||||
data.FamTraces = copyFamilies()
|
||||
|
||||
// Default to showing greater-than.
|
||||
data.AllGT = true
|
||||
if all := req.FormValue("all"); all != "" {
|
||||
data.AllGT, _ = strconv.ParseBool(all)
|
||||
}
|
||||
|
||||
// Fill in the family related parameters.
|
||||
if fam := req.FormValue("fam"); fam != "" {
|
||||
if _, ok := data.FamTraces[fam]; !ok {
|
||||
data.Family = ""
|
||||
data.Error = "Unknown family"
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
goto render
|
||||
}
|
||||
data.Family = fam
|
||||
|
||||
if bs := req.FormValue("b"); bs != "" {
|
||||
i, err := strconv.Atoi(bs)
|
||||
if err != nil {
|
||||
data.Error = "Invalid bucket (not a number)"
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
goto render
|
||||
} else if i < -2 || i >= nBuckets {
|
||||
data.Error = "Invalid bucket number"
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
goto render
|
||||
}
|
||||
data.Bucket = i
|
||||
data.Traces = data.FamTraces[data.Family].TracesFor(i, data.AllGT)
|
||||
|
||||
switch i {
|
||||
case -2:
|
||||
data.BucketStr = "errors"
|
||||
case -1:
|
||||
data.BucketStr = "active"
|
||||
default:
|
||||
data.BucketStr = buckets[i].String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lat := req.FormValue("lat"); data.Family != "" && lat != "" {
|
||||
data.Latencies = data.FamTraces[data.Family].Latencies()
|
||||
}
|
||||
|
||||
if traceID := req.FormValue("trace"); traceID != "" {
|
||||
refID := req.FormValue("ref")
|
||||
tr := findInFamilies(id(traceID), id(refID))
|
||||
if tr == nil {
|
||||
data.Error = "Trace not found"
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
goto render
|
||||
}
|
||||
data.Trace = tr
|
||||
data.Family = tr.Family
|
||||
data.AllEvents = allEvents(tr)
|
||||
}
|
||||
|
||||
render:
|
||||
|
||||
// Write into a buffer, to avoid accidentally holding a lock on http
|
||||
// writes. It shouldn't happen, but just to be extra safe.
|
||||
bw := &bytes.Buffer{}
|
||||
bw.Grow(16 * 1024)
|
||||
err := top.ExecuteTemplate(bw, "index.html.tmpl", data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
w.Write(bw.Bytes())
|
||||
}
|
||||
|
||||
type traceAndEvent struct {
|
||||
Trace *trace
|
||||
Event event
|
||||
Depth uint
|
||||
}
|
||||
|
||||
// allEvents gets all the events for the trace and its children/linked traces;
|
||||
// and returns them sorted by timestamp.
|
||||
func allEvents(tr *trace) []traceAndEvent {
|
||||
// Map tracking all traces we've seen, to avoid loops.
|
||||
seen := map[id]bool{}
|
||||
|
||||
// Recursively gather all events.
|
||||
evts := appendAllEvents(tr, []traceAndEvent{}, seen, 0)
|
||||
|
||||
// Sort them by time.
|
||||
sort.Slice(evts, func(i, j int) bool {
|
||||
return evts[i].Event.When.Before(evts[j].Event.When)
|
||||
})
|
||||
|
||||
return evts
|
||||
}
|
||||
|
||||
func appendAllEvents(tr *trace, evts []traceAndEvent, seen map[id]bool, depth uint) []traceAndEvent {
|
||||
if seen[tr.ID] {
|
||||
return evts
|
||||
}
|
||||
seen[tr.ID] = true
|
||||
|
||||
subTraces := []*trace{}
|
||||
|
||||
// Append all events of this trace.
|
||||
trevts := tr.Events()
|
||||
for _, e := range trevts {
|
||||
evts = append(evts, traceAndEvent{tr, e, depth})
|
||||
if e.Ref != nil {
|
||||
subTraces = append(subTraces, e.Ref)
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range subTraces {
|
||||
evts = appendAllEvents(t, evts, seen, depth+1)
|
||||
}
|
||||
|
||||
return evts
|
||||
}
|
||||
|
||||
func stripZeros(d time.Duration) string {
|
||||
if d < time.Second {
|
||||
_, frac := math.Modf(d.Seconds())
|
||||
return fmt.Sprintf(" .%6d", int(frac*1000000))
|
||||
}
|
||||
return fmt.Sprintf("%.6f", d.Seconds())
|
||||
}
|
||||
|
||||
func roundSeconds(d time.Duration) string {
|
||||
return fmt.Sprintf("%.6f", d.Seconds())
|
||||
}
|
||||
|
||||
func roundDuration(d time.Duration) time.Duration {
|
||||
return d.Round(time.Millisecond)
|
||||
}
|
||||
|
||||
func colorize(depth uint, id id) template.CSS {
|
||||
if depth == 0 {
|
||||
return template.CSS("rgba(var(--text-color))")
|
||||
}
|
||||
|
||||
if depth > 3 {
|
||||
depth = 3
|
||||
}
|
||||
|
||||
// Must match the number of nested color variables in the CSS.
|
||||
colori := crc32.ChecksumIEEE([]byte(id)) % 6
|
||||
return template.CSS(
|
||||
fmt.Sprintf("var(--nested-d%02d-c%02d)", depth, colori))
|
||||
}
|
||||
|
||||
func depthspan(depth uint) template.HTML {
|
||||
s := `<span class="depth">`
|
||||
switch depth {
|
||||
case 0:
|
||||
case 1:
|
||||
s += "· "
|
||||
case 2:
|
||||
s += "· · "
|
||||
case 3:
|
||||
s += "· · · "
|
||||
case 4:
|
||||
s += "· · · · "
|
||||
default:
|
||||
s += fmt.Sprintf("· (%d) · ", depth)
|
||||
}
|
||||
|
||||
s += `</span>`
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
// Hand-picked emojis that have enough visual differences in most common
|
||||
// renderings, and are common enough to be able to easily describe them.
|
||||
var emojids = []rune(`😀🤣😇🥰🤧😈🤡👻👽🤖👋✊🦴👅` +
|
||||
`🐒🐕🦊🐱🐯🐎🐄🐷🐑🐐🐪🦒🐘🐀🦇🐓🦆🦚🦜🐢🐍🦖🐋🐟🦈🐙` +
|
||||
`🦋🐜🐝🪲🌻🌲🍉🍌🍍🍎🍑🥕🍄` +
|
||||
`🧀🍦🍰🧉🚂🚗🚜🛵🚲🛼🪂🚀🌞🌈🌊⚽`)
|
||||
|
||||
func shorttitle(tr *trace) string {
|
||||
all := tr.Family + " - " + tr.Title
|
||||
if len(all) > 20 {
|
||||
all = "..." + all[len(all)-17:]
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
func traceemoji(id id) string {
|
||||
i := crc32.ChecksumIEEE([]byte(id)) % uint32(len(emojids))
|
||||
return string(emojids[i])
|
||||
}
|
||||
Reference in New Issue
Block a user