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
128 lines
2.8 KiB
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
|
|
}
|