package servertiming import ( "fmt" "net/http" "strconv" "strings" "time" ) type ServerTiming struct { Name string Dur time.Duration Desc string // Used when formatting. The default behavior is to output time as an int // representing milliseconds. Increasing the precision will add decimals to // the output, down to nanoseconds. DecimalPrecision int } func (t ServerTiming) String() string { sb := strings.Builder{} sb.WriteString(t.Name) if t.Dur != 0 { sb.WriteString(";dur=") // precision is clamped between 0 and 6 inclusively. 0 represents // milliseconds as an integer, 6 decimal positions represent nanoseconds precision := min(max(0, t.DecimalPrecision), 6) sb.WriteString(fmt.Sprintf("%.*f", precision, float64(t.Dur.Nanoseconds())/1_000_000)) } if t.Desc != "" { sb.WriteString(";desc=") sb.WriteString(t.Desc) } return sb.String() } func max(a, b int) int { if a > b { return a } return b } func min(a, b int) int { if a < b { return a } return b } func FromString(s string) ServerTiming { st := ServerTiming{} part, rest, more := strings.Cut(s, ";") st.Name = strings.TrimSpace(part) for more { part, rest, more = strings.Cut(strings.TrimSpace(rest), ";") key, val, _ := strings.Cut(part, "=") val = strings.TrimSpace(val) switch key { case "desc": st.Desc = val case "dur": // From the spec: // Since duration is a DOMHighResTimeStamp, it usually represents a // duration in milliseconds. Since this is not enforcable in // practice, duration can represent any unit of time, and having it // represent a duration in milliseconds is a recommendation. // The happy path is an int, in which case milliseconds are assumed if i, err := strconv.Atoi(val); err == nil { st.Dur = time.Duration(i * int(time.Millisecond)) break } // Otherwise we try to parse as a float, and multiply by 1,000,000 // to get nanoseconds and truncate the rest if f, err := strconv.ParseFloat(val, 64); err == nil { st.Dur = time.Duration(int(f*1_000_000) * int(time.Nanosecond)) } default: // ignore any unknown token } } return st } func Append(r *http.Response, t ServerTiming) { r.Header.Add("Server-Timing", t.String()) } func Trailer(r *http.Response, t ServerTiming) { r.Trailer.Add("Server-Timing", t.String()) } func Parse(r *http.Response) []ServerTiming { res := []ServerTiming{} for k, v := range r.Header { if strings.ToLower(k) == "server-timing" { for _, s := range v { for _, single := range strings.Split(s, ",") { res = append(res, FromString(single)) } } } } for k, v := range r.Trailer { if strings.ToLower(k) == "server-timing" { for _, s := range v { for _, single := range strings.Split(s, ",") { res = append(res, FromString(single)) } } } } return res }