126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
|
package servertiming
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// Metric represents a single metric for the Server-Timing header.
|
||
|
//
|
||
|
// The easiest way to use the Metric is to use NewMetric and chain it. This
|
||
|
// results in a single line defer at the top of a function time a function.
|
||
|
//
|
||
|
// timing := FromContext(r.Context())
|
||
|
// defer timing.NewMetric("sql").Start().Stop()
|
||
|
//
|
||
|
// For timing around specific blocks of code:
|
||
|
//
|
||
|
// m := timing.NewMetric("sql").Start()
|
||
|
// // ... run your code being timed here
|
||
|
// m.Stop()
|
||
|
//
|
||
|
// A metric is expected to represent a single timing event. Therefore,
|
||
|
// no functions on the struct are safe for concurrency by default. If a single
|
||
|
// Metric is shared by multiple concurrenty goroutines, you must lock access
|
||
|
// manually.
|
||
|
type Metric struct {
|
||
|
// Name is the name of the metric. This must be a valid RFC7230 "token"
|
||
|
// format. In a gist, this is an alphanumeric string that may contain
|
||
|
// most common symbols but may not contain any whitespace. The exact
|
||
|
// syntax can be found in RFC7230.
|
||
|
//
|
||
|
// It is common for Name to be a unique identifier (such as "sql-1") and
|
||
|
// for a more human-friendly name to be used in the "desc" field.
|
||
|
Name string
|
||
|
|
||
|
// Duration is the duration of this Metric.
|
||
|
Duration time.Duration
|
||
|
|
||
|
// Desc is any string describing this metric. For example: "SQL Primary".
|
||
|
// The specific format of this is `token | quoted-string` according to
|
||
|
// RFC7230.
|
||
|
Desc string
|
||
|
|
||
|
// Extra is a set of extra parameters and values to send with the
|
||
|
// metric. The specification states that unrecognized parameters are
|
||
|
// to be ignored so it should be safe to add additional data here. The
|
||
|
// key must be a valid "token" (same syntax as Name) and the value can
|
||
|
// be any "token | quoted-string" (same as Desc field).
|
||
|
//
|
||
|
// If this map contains a key that would be sent by another field in this
|
||
|
// struct (such as "desc"), then this value is prioritized over the
|
||
|
// struct value.
|
||
|
Extra map[string]string
|
||
|
|
||
|
// startTime is the time that this metric recording was started if
|
||
|
// Start() was called.
|
||
|
startTime time.Time
|
||
|
}
|
||
|
|
||
|
// WithDesc is a chaining-friendly helper to set the Desc field on the Metric.
|
||
|
func (m *Metric) WithDesc(desc string) *Metric {
|
||
|
m.Desc = desc
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// Start starts a timer for recording the duration of some task. This must
|
||
|
// be paired with a Stop call to set the duration. Calling this again will
|
||
|
// reset the start time for a subsequent Stop call.
|
||
|
func (m *Metric) Start() *Metric {
|
||
|
m.startTime = time.Now()
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// Stop ends the timer started with Start and records the duration in the
|
||
|
// Duration field. Calling this multiple times will modify the Duration based
|
||
|
// on the last time Start was called.
|
||
|
//
|
||
|
// If Start was never called, this function has zero effect.
|
||
|
func (m *Metric) Stop() *Metric {
|
||
|
// Only record if we have a start time set with Start()
|
||
|
if !m.startTime.IsZero() {
|
||
|
m.Duration = time.Since(m.startTime)
|
||
|
}
|
||
|
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// String returns the valid Server-Timing metric entry value.
|
||
|
func (m *Metric) String() string {
|
||
|
// Begin building parts, expected capacity is length of extra
|
||
|
// fields plus id, desc, dur.
|
||
|
parts := make([]string, 1, len(m.Extra)+3)
|
||
|
parts[0] = m.Name
|
||
|
|
||
|
// Description
|
||
|
if _, ok := m.Extra[paramNameDesc]; !ok && m.Desc != "" {
|
||
|
parts = append(parts, headerEncodeParam(paramNameDesc, m.Desc))
|
||
|
}
|
||
|
|
||
|
// Duration
|
||
|
if _, ok := m.Extra[paramNameDur]; !ok && m.Duration > 0 {
|
||
|
parts = append(parts, headerEncodeParam(
|
||
|
paramNameDur,
|
||
|
strconv.FormatFloat(float64(m.Duration)/float64(time.Millisecond), 'f', -1, 64),
|
||
|
))
|
||
|
}
|
||
|
|
||
|
// All remaining extra params
|
||
|
for k, v := range m.Extra {
|
||
|
parts = append(parts, headerEncodeParam(k, v))
|
||
|
}
|
||
|
|
||
|
return strings.Join(parts, ";")
|
||
|
}
|
||
|
|
||
|
// GoString is needed for fmt.GoStringer so %v works on pointer value.
|
||
|
func (m *Metric) GoString() string {
|
||
|
if m == nil {
|
||
|
return "nil"
|
||
|
}
|
||
|
|
||
|
return fmt.Sprintf("*%#v", *m)
|
||
|
}
|