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 := `` switch depth { case 0: case 1: s += "· " case 2: s += "· · " case 3: s += "· · · " case 4: s += "· · · · " default: s += fmt.Sprintf("· (%d) · ", depth) } s += `` 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]) }