git » chasquid » main » tree

[main] / internal / nettrace / http.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])
}