telemetry

package
v1.0.442 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package telemetry provides http.Handler middleware for request logging and request metrics, plus the ResponseCapture writer they rely on to observe the status code and body size of a response.

h := telemetry.NewRequestLogger(next, time.Millisecond, logger,
	telemetry.WithLoggerSkipPaths([]telemetry.LoggerSkipPath{{Path: "/healthz", Agent: "*"}}))
h = telemetry.NewRequestMetrics(h) // reports the caller role
h = identity.NewContextHandler(h, mapper)
h = telemetry.NewRequestMetrics(h) // records every response

A NewRequestMetrics nested inside another records nothing and passes the role of its request's identity to the outer one, so the outer handler also counts the responses of the identity handler (as guest). The outer handler also records a request whose handler panics, with the status it had sent or 500, and lets the panic continue.

Metrics are emitted through porto/metricskey (HTTPReqPerf, HTTPReqByRole) keyed by method, status and route template. The restserver Router records the template of the route it dispatches to with SetRoute; requests without one are labelled UnknownRoute, and non-standard methods OtherMethod, so no label value is copied from the request. LoggerSkipPath entries are also reused by restserver/authz to suppress access logs; see ShouldSkip.

Index

Constants

View Source
const (
	// UnknownRoute is the uri label of requests without a recorded route:
	// unmatched paths and requests answered before or instead of a route
	// handler (identity rejections, rate limits, CORS preflights, authz
	// denials, readiness, the router's own 405, OPTIONS and redirect
	// responses) or by middleware that does not call SetRoute.
	UnknownRoute = "unknown"
	// OtherMethod is the verb label of requests whose method is not one of
	// the standard net/http methods, as in the OpenTelemetry HTTP semantic
	// conventions.
	OtherMethod = "_OTHER"
)

Variables

This section is empty.

Functions

func NewRequestLogger

func NewRequestLogger(
	handler http.Handler,
	granularity time.Duration,
	logger xlog.KeyValueLogger,
	opts ...Option) http.Handler

NewRequestLogger creates a RequestLogger that chains to handler. The logged duration is expressed in units of granularity (e.g. time.Millisecond); a zero or negative granularity logs nanoseconds. It panics if handler is nil and returns handler unchanged (no logging) if logger is nil. The remote address logged is identity.ClientIPFromRequest, which accepts forwarding headers only from configured trusted proxies.

func NewRequestMetrics

func NewRequestMetrics(h http.Handler) http.Handler

NewRequestMetrics wraps h so that every request records metricskey.HTTPReqPerf (latency) and metricskey.HTTPReqByRole (count) labelled by method, status code, route and caller role. No label value is copied from the request, so callers cannot create unbounded series: the uri label is the route template recorded with SetRoute (the restserver Router records its registered path, for example "/v1/users/:id"), or UnknownRoute when no route was recorded; the verb label is the request method when it is a standard net/http method, or OtherMethod; the role label is whatever the identity mapper returned, so it is bounded only if the mapper's roles are.

A NewRequestMetrics nested inside another records nothing: it reports the role of the identity in its request (see identity.FromContext) to the enclosing one, which records the request once. Place one outside identity.NewContextHandler, so that its rejections are counted, and one inside it for the role:

h = telemetry.NewRequestMetrics(h) // reports the role
h = identity.NewContextHandler(h, mapper)
h = telemetry.NewRequestMetrics(h) // records every response

Without a nested handler, or when the request never reached it, the role is the one of the identity in the outer request, which is the guest role when none was stored.

A request whose handler panics is recorded too, with the status already sent (see ResponseCapture) or 500 when none was; the panic, including http.ErrAbortHandler, is not recovered and continues to the server. net/http then aborts the response, so the client of a handler that panicked before sending a status sees no 500; gserver TLS listeners answer it with 500.

func SetRoute added in v1.0.438

func SetRoute(ctx context.Context, pattern string)

SetRoute records pattern, the registered route template that matched the request (for example "/v1/users/:id"), as the uri label of the metrics recorded by NewRequestMetrics. The restserver Router calls it for every matched route; custom routers behind NewRequestMetrics call it with ctx from the request they dispatch. The pattern must come from the route registration, never from the request, so that the label stays bounded. The last call wins; without an enclosing NewRequestMetrics it is a no-op.

func ShouldSkip added in v0.24.0

func ShouldSkip(cfg []LoggerSkipPath, path, userAgent string) bool

ShouldSkip reports whether a request for path with the given User-Agent matches any entry in cfg and should therefore not be logged.

Types

type LoggerSkipPath

type LoggerSkipPath struct {
	Path  string `json:"path,omitempty" yaml:"path,omitempty"`
	Agent string `json:"agent,omitempty" yaml:"agent,omitempty"`
}

LoggerSkipPath describes requests to exclude from logging by Path and User-Agent. Path is compared exactly ("*" matches every path); Agent is a substring match ("*" matches every agent). Both must match.

type Option

type Option option

Option configures NewRequestLogger; see WithLoggerSkipPaths.

func WithLoggerSkipPaths

func WithLoggerSkipPaths(value []LoggerSkipPath) Option

WithLoggerSkipPaths returns an Option that suppresses log lines for requests matching any of the given LoggerSkipPath entries.

type RequestLogger

type RequestLogger struct {
	// contains filtered or unexported fields
}

RequestLogger is a http.Handler that forwards requests to the wrapped handler and then logs one INFO line per request (method, path, status, bytes, duration, remote IP, agent) using the request context for correlation fields.

func (*RequestLogger) ServeHTTP

func (l *RequestLogger) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface. We wrap the call to the real handler to collect info about the response, and then write out the log line

type ResponseCapture

type ResponseCapture struct {
	// contains filtered or unexported fields
}

ResponseCapture is a net/http.ResponseWriter that delegates everything to the contained delegate, but captures the status code and number of bytes written. The status is the one the response was sent with: the first final status passed to WriteHeader, or 200 when the handler wrote the body, copied a byte of it or flushed first, or wrote nothing. Like net/http, it ignores informational 1xx statuses other than 101 and every WriteHeader after the status was sent; an invalid code (outside 100-999), which net/http rejects with a panic, is not captured.

Unwrap exposes the delegate, so http.ResponseController reaches its optional features (read and write deadlines, full duplex, flush, hijack). ResponseCapture also implements http.Flusher, FlushError and http.Hijacker for handlers that assert them directly (WebSocket upgrades); they reach the delegate through http.ResponseController. When the delegate chain lacks the feature, Flush is a no-op, and FlushError and Hijack return an error wrapping http.ErrNotSupported (Hijack does on HTTP/2), so asserting http.Hijacker does not prove that hijacking works. Status and size cover only what is written through the ResponseCapture, not what a handler writes to a hijacked connection. ResponseCapture implements io.ReaderFrom, so io.Copy into it (http.ServeContent, http.FileServer) keeps the delegate's ReadFrom, which uses sendfile for files on plain TCP connections.

func NewResponseCapture

func NewResponseCapture(w http.ResponseWriter) *ResponseCapture

NewResponseCapture returns a new ResponseCapture instance that delegates writes to the supplied ResponseWriter

func (*ResponseCapture) BodySize

func (r *ResponseCapture) BodySize() uint64

BodySize returns in bytes the total number of bytes written to the response body so far.

func (*ResponseCapture) Flush

func (r *ResponseCapture) Flush()

Flush sends any buffered data to the client when the delegate chain supports flushing. It implements http.Flusher, which cannot report errors, so a failed or unsupported flush is ignored; use FlushError, or http.ResponseController.Flush, which calls it, to see the error.

func (*ResponseCapture) FlushError added in v1.0.438

func (r *ResponseCapture) FlushError() error

FlushError flushes like Flush and returns the delegate chain's error: an error wrapping http.ErrNotSupported when no writer in the chain can flush, or the write error after the client went away. http.ResponseController.Flush calls it, so streaming handlers see the error through the ResponseCapture.

func (*ResponseCapture) Header

func (r *ResponseCapture) Header() http.Header

Header returns the underlying writers Header instance

func (*ResponseCapture) Hijack added in v1.0.438

func (r *ResponseCapture) Hijack() (net.Conn, *bufio.ReadWriter, error)

Hijack lets the caller take over the connection, as http.Hijacker, when the delegate chain supports it (HTTP/1.x). Otherwise, for example on HTTP/2, it returns an error wrapping http.ErrNotSupported.

func (*ResponseCapture) ReadFrom added in v1.0.438

func (r *ResponseCapture) ReadFrom(src io.Reader) (int64, error)

ReadFrom copies src to the response, as io.ReaderFrom, and counts the bytes copied. It calls the delegate's ReadFrom when the delegate implements io.ReaderFrom, and copies through its Write otherwise; like Write, it returns the delegate's error as is.

func (*ResponseCapture) StatusCode

func (r *ResponseCapture) StatusCode() int

StatusCode returns the HTTP status the response was sent with, or 200 when nothing was sent yet.

func (*ResponseCapture) Unwrap added in v1.0.438

func (r *ResponseCapture) Unwrap() http.ResponseWriter

Unwrap returns the delegate ResponseWriter; http.ResponseController uses it to reach the delegate's optional interfaces.

func (*ResponseCapture) Write

func (r *ResponseCapture) Write(data []byte) (int, error)

Write the supplied data to the response (tracking the number of bytes written as we go)

func (*ResponseCapture) WriteHeader

func (r *ResponseCapture) WriteHeader(sc int)

WriteHeader sets the HTTP status code of the response. The captured status changes only on the first final status, as net/http sends only that one.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL