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.
260 lines
6.1 KiB
Go
260 lines
6.1 KiB
Go
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])
|
|
}
|