You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

128 lines
2.8 KiB

5 months ago
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
}