Files
memby/server/internal/timing/transport.go
T

81 lines
2.3 KiB
Go
Raw Normal View History

2026-08-19 18:08:00 +12:00
package timing
import (
"io"
"net/http"
"time"
)
// Transport records every upstream HTTP call against a stage.
//
// It is a RoundTripper rather than a helper each client calls because there are eight
// upstream clients here and the one thing they all genuinely share is that they make
// HTTP requests. Wrapping the transport means a new integration is instrumented by
// being constructed rather than by somebody remembering to measure it.
//
// The context it reads is the request's, so a call made outside a traced request —
// a scheduled sync, a health probe — records nothing and costs one nil check.
type Transport struct {
Stage string
Base http.RoundTripper
}
func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
base := t.Base
if base == nil {
base = http.DefaultTransport
}
trace := From(r.Context())
if trace == nil {
return base.RoundTrip(r)
}
stage := LabelFrom(r.Context(), t.Stage)
began := time.Now()
resp, err := base.RoundTrip(r)
trace.Record(stage, time.Since(began))
if err != nil || resp == nil || resp.Body == nil {
return resp, err
}
// RoundTrip returns once the headers are in, and a home row is several hundred
// kilobytes of JSON still on the wire at that point. Reading the body is part of
// what the upstream cost, so the stage keeps accruing until the caller closes it —
// otherwise the largest responses are the ones the breakdown under-reports.
resp.Body = &timedBody{ReadCloser: resp.Body, trace: trace, stage: stage}
return resp, nil
}
type timedBody struct {
io.ReadCloser
trace *Trace
stage string
spent time.Duration
done bool
}
func (b *timedBody) Read(p []byte) (int, error) {
began := time.Now()
n, err := b.ReadCloser.Read(p)
b.spent += time.Since(began)
return n, err
}
func (b *timedBody) Close() error {
err := b.ReadCloser.Close()
if !b.done {
b.done = true
b.trace.Record(b.stage, b.spent)
}
return err
}
// Instrument wraps a client's transport in place. Called during construction, before
// anything is serving, so it needs no synchronisation — the stance the Emby client's
// other setters take.
func Instrument(client *http.Client, stage string) *http.Client {
if client == nil {
return nil
}
client.Transport = &Transport{Stage: stage, Base: client.Transport}
return client
}