recorder

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 37 Imported by: 0

README

recorder

CI Go Reference

recorder is an http.RoundTripper that records the full life cycle of net/http client exchanges as HAR 1.2 documents. It records successful responses and, just as thoroughly, calls that fail before or during the response: DNS resolution, TCP connect, TLS handshake, proxy dialing, context cancellation, and request/response body stream errors — each classified into a structured _error extension.

  • Core package is standard library only. Optional integrations (the OpenTelemetry adapter, the HAR inspector UI) live in separate modules/directories and never pull dependencies into the core.
  • Caller-visible HTTP behavior never changes. The original request is not mutated, response bytes/EOF/errors pass through untouched, and recorder failures never break the HTTP call.
  • Redaction applies to the recording only. The live request and response are never modified; secrets are replaced in the recorded copy.
  • Nothing is invented. Values that cannot be observed (wire header sizes, compressed sizes after transparent gzip, unmeasured timing phases, the origin IP behind a proxy) are recorded as -1 or omitted, per HAR 1.2.

Motivation

I often needed a reliable way to record outbound API calls, especially for financial workflows where preserving an accurate account of an exchange can be essential for debugging, reconciliation, and incident analysis.

Most HTTP recorders focus on successful request/response pairs. In practice, the failures are often more important: DNS errors, connection failures, TLS handshake problems, context cancellation, truncated bodies, and stream errors.

recorder captures the complete client exchange lifecycle as HAR 1.2 without reimplementing HTTP transport behavior. It wraps an existing http.RoundTripper, preserves caller-visible request and response semantics, and records only what can actually be observed.

The result is a portable debugging artifact combining HTTP data, network timings, structured failures, TLS details, and configurable redaction. Body capture remains opt-in by default, which is particularly important when API traffic may contain financial or other sensitive data.

Requirements

  • Core module: Go 1.24 or newer.
  • otelrecorder module: Go 1.25 or newer, matching its OpenTelemetry dependencies.

CI tests each module on its minimum supported Go version and the current stable Go release.

Quick start

package main

import (
	"io"
	"net/http"
	"os"

	"github.com/mgurevin/recorder"
)

func main() {
	rec := recorder.NewMemoryRecorder()
	client := &http.Client{
		Transport: recorder.NewTransport(http.DefaultTransport, rec),
	}

	resp, err := client.Get("https://example.com/")
	if err == nil {
		io.Copy(io.Discard, resp.Body) // the entry finalizes on body EOF/Close
		resp.Body.Close()
	}

	rec.WriteHAR(os.Stdout) // or: har := rec.HAR()
}

An entry is not complete when RoundTrip returns — the response body has not been read yet. It reaches the recorder when the body hits EOF, is closed early, or fails; transport errors finalize immediately. A body that is never read and never closed produces no entry (a caller bug that also leaks the connection in plain net/http).

What gets recorded

Area What is recorded
HAR request/response method, URL, status, headers (wire-order when observable), cookies, body metadata
timings blocked, dns, connect, ssl, send, wait, receive
_error transport/body failure: phase, type, message, timeout/context flags, unwrap chain
_network DNS results + coalescing, local/remote address, IP version, reuse/idle, proxy, HTTP/2, idle-pool return
_tls TLS version, cipher suite, ALPN, SNI, resumption, OCSP/SCT, certificate chain
_requestBody / _responseBody completion, early close, truncation, total/captured bytes, hash, store reference
trailers / transfer encoding _requestTrailers, _responseTrailers, _*TransferEncoding
_trace raw httptrace event timeline (when enabled); details pass through RedactErrorMessage
_expect100 Expect: 100-continue handshake (waited, received, wait time)
_informational 1xx interim responses (100, 103 Early Hints) with redacted headers
correlation _traceId, _exchangeId, _redirectIndex

Stripping every _-prefixed field leaves a valid plain HAR 1.2 document (verified by test); the files open in standard HAR viewers.

Security issues should be reported privately as described in SECURITY.md. Release history is maintained in CHANGELOG.md.

Default configuration

NewTransport starts from DefaultOptions() and applies your Option values on top:

Option field Default Notes
CaptureRequestBody false lifecycle and byte counts tracked; content capture is opt-in
CaptureResponseBody false lifecycle and byte counts tracked; content capture is opt-in
EmbedBodies false body text is not embedded by default
MaxRequestBodyBytes 1 MiB content capture limit; <= 0 means unlimited
MaxResponseBodyBytes 1 MiB content capture limit; <= 0 means unlimited
CaptureTLS true _tls extension enabled
CaptureCertificates true peer certificate metadata enabled
CaptureRawCertificates false raw DER not embedded by default
CaptureHeaders true headers and trailers recorded
CaptureCookies true parsed cookies recorded
RedactHeaders Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-API-Key case-insensitive
RedactQueryParameters empty opt-in
RedactCookies empty opt-in; cookies are also redacted when their carrier header is
RedactJSONFields empty opt-in
RedactXMLElements empty opt-in (SOAP bodies)
HashBodies false full-stream hashing is opt-in
BodyHashAlgorithm sha256 sha1/md5 supported; unknown values fall back to sha256
CaptureRawTrace false raw httptrace event list disabled by default
ContentDecoders gzip, x-gzip, deflate stdlib decoders for record-time decoding
BodyStore MemoryBodyStore used when nil
InternalErrorMode InternalErrorIgnore reports through OnInternalError if set
OnInternalError nil optional callback for recorder-internal errors
Logf nil used by InternalErrorLog; nil falls back to the standard log package
OnEntryCompleted nil optional per-entry callback (context + entry)
RedactErrorMessage nil optional error message redactor

Also note:

  • WithOptions(Options{}) replaces the whole struct: zero-value options disable all capturing (and body embedding).
  • Options must not be mutated after the transport served its first request.

Options reference

Option Effect
WithOptions(o) Replace the entire Options value; apply first when combining
WithCaptureRequestBody(v) Toggle request body content capture (size/state always tracked)
WithCaptureResponseBody(v) Toggle response body content capture
WithEmbedBodies(v) Off: keep sizes/hashes/store refs but embed no body text — the production setting with FileBodyStore
WithMaxRequestBodyBytes(n) Request capture limit; counting continues past it
WithMaxResponseBodyBytes(n) Response capture limit; also bounds record-time decoding
WithCaptureTLS(v) Toggle the _tls extension
WithCaptureCertificates(v, raw) Toggle certificate details; raw embeds Base64 DER
WithCaptureHeaders(v) Toggle header/trailer recording
WithCaptureCookies(v) Toggle parsed cookie recording
WithRedactHeaders(names...) Append case-insensitive header names to redact
WithRedactQueryParameters(names...) Redact query params in url and queryString (and form fields)
WithRedactCookies(names...) Redact cookies by name
WithRedactJSONFields(names...) Recursively redact JSON object fields in captured bodies
WithRedactXMLElements(names...) Redact XML element subtrees by local name (namespace prefixes ignored)
WithHashBodies(enabled, alg) Toggle body hashing / choose algorithm
WithBodyStore(s) Storage backend for captured bytes (MemoryBodyStore, FileBodyStore, custom)
WithCaptureRawTrace(v) Record every raw httptrace event under _trace
WithContentDecoder(enc, dec) Register a record-time decoder (e.g. brotli, zstd) for a Content-Encoding
WithInternalErrorMode(m) Ignore (default) or Log; recorder failures never alter the HTTP result
WithOnInternalError(fn) Callback for recorder-internal errors
WithLogf(fn) Logger used by InternalErrorLog
WithOnEntryCompleted(fn) Per-entry completion callback — the integration hook (OTel adapter uses it)
WithErrorRedactor(fn) Filter applied to every recorded error message

Production recipes

Full forensic capture

Explicitly enable body capture and add the redaction your payloads need:

tr := recorder.NewTransport(base, rec,
	recorder.WithCaptureRequestBody(true),
	recorder.WithCaptureResponseBody(true),
	recorder.WithEmbedBodies(true),
	recorder.WithHashBodies(true, "sha256"),
	recorder.WithRedactQueryParameters("token", "api_key"),
	recorder.WithRedactJSONFields("password", "secret"),
	recorder.WithMaxResponseBodyBytes(4<<20),
)
High-volume telemetry
tr := recorder.NewTransport(base, rec,
	recorder.WithCaptureRequestBody(true),
	recorder.WithCaptureResponseBody(true),
	recorder.WithEmbedBodies(false),                      // no body text in the HAR
	recorder.WithBodyStore(recorder.FileBodyStore{Dir: "/var/spool/recorder"}),
	recorder.WithMaxResponseBodyBytes(64<<10),            // small capture budget
	recorder.WithHashBodies(false, ""),                   // hashing caps large streams at ~SHA-256 speed
)

Hashing covers every streamed byte and is the throughput ceiling on large bodies (the 100 MB streaming benchmark runs ~4x faster without it — compare Benchmark100MBStreamingBody and Benchmark100MBStreamingBodyNoHash).

Default header-only capture
tr := recorder.NewTransport(base, rec)
SOAP/XML redaction
tr := recorder.NewTransport(base, rec,
	recorder.WithRedactXMLElements("Username", "Password"), // covers <wsse:Password> etc.
)
Per-request export with a trace ID
ctx, traceID := recorder.TraceContext(context.Background())
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req) // redirect hops share the trace ID
if err == nil {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}

entries := rec.TakeTrace(traceID)   // atomically remove + return this call's entries
recorder.NewHAR(entries).Write(f)   // standalone HAR for just this call
Streaming NDJSON export

JSONStreamRecorder writes each entry as one JSON line the moment it completes — no buffering, and the top-level HAR wrapper is intentionally not emitted so no JSON document is ever left half-written:

stream := recorder.NewJSONStreamRecorder(w)
tr := recorder.NewTransport(base, stream)
OpenTelemetry adapter

See OpenTelemetry export below — the adapter lives in the separate otelrecorder submodule and plugs into WithOnEntryCompleted.

Body capture, storage, and hashing

Three independent concerns:

  • Counting always runs, even with capture off: totalBytes and the HAR size fields reflect the real stream, beyond any limit.
  • Capture writes content to the BodyStore until Max*BodyBytes is reached; past the limit only counting (and hashing) continues, and the record is marked truncated. MemoryBodyStore (default) buffers in memory; FileBodyStore spools to temp files so large bodies never live in memory — cleaning up its files is the caller's responsibility (paths are exposed via _requestBody/_responseBody.store).
  • Embedding (EmbedBodies) decides whether captured content becomes postData.text / content.text in the HAR. With WithEmbedBodies(false) the document stays small while sizes, hashes, truncation state and the store reference remain.

Hashes cover the entire stream (truncation does not affect them) and are only emitted for complete streams — a partial-stream hash would be misleading.

Redaction

  • Headers, query parameters, cookies are redacted by case-insensitive name; a cookie is also redacted when its carrier header (Cookie / Set-Cookie) is in the header list.
  • JSON field redaction recursively replaces matching object-field values while preserving every unredacted byte (including whitespace, key order, duplicate keys, number spelling, and escapes).
  • XML element redaction replaces the text content of matching elements (by local name, namespace prefixes ignored — "Password" covers <wsse:Password>), preserving the rest of the document byte-for-byte. XML attribute values are not redacted.
  • Fully captured, well-formed JSON/XML is also recognized when a server sends it under a generic or incorrect content type such as text/plain.

Rules that hold everywhere:

  • Redaction applies only to the recorded copy — the live HTTP request and response are never modified.
  • Structured body redaction (JSON/XML) runs only on fully captured, non-truncated bodies; a partial document cannot be parsed safely.
  • Body hashes are computed over the real wire/caller bytes, never over redacted bytes.
  • Error messages can carry secrets too: WithErrorRedactor filters every recorded error string.

Failures and error classification

HTTP 4xx/5xx are not transport errors — they are recorded as normal responses. Transport and body-stream failures produce a _error extension:

{
  "_error": {
    "phase": "dns",
    "type": "*net.DNSError",
    "message": "lookup api.example.com: no such host",
    "timeout": false,
    "temporary": false,
    "contextCanceled": false,
    "contextDeadlineExceeded": false,
    "unwrapChain": ["*url.Error", "*net.OpError", "*net.DNSError"]
  }
}

Phases: request_setup, dns, connect, proxy, tls, write_request, write_request_body, wait_response, read_response_headers, read_response_body, redirect, context, unknown.

Classification is type-first (errors.Is/errors.As over the unwrap chain), with httptrace progress as fallback; string matching is a last resort for untyped errors only. Two distinctions worth knowing:

  • The context phase and the contextCanceled/contextDeadlineExceeded flags require the request's own context to have fired. Transport- internal timeouts (e.g. ResponseHeaderTimeout) merely match context errors via errors.Is and are attributed to the phase the exchange stood in (wait_response), with timeout: true.
  • A response body read error records _error with phase read_response_body; a body closed early by the caller is not an error — it records state closed_early and _responseBody.closedEarly.

Timings and observability limits

HAR timings are milliseconds; -1 means "not observed / not applicable", never zero:

  • Reused connections (including HTTP/2 multiplexing) report -1 for dns/connect/ssl — those phases did not happen for the exchange; blocked covers the wait for a pooled connection.
  • headersSize is always -1: the exact bytes written to the wire (including transport-added headers, HPACK on HTTP/2) are not observable at the RoundTripper layer.
  • With transparent gzip, bodySize is -1 and content.size is the decoded size (content._decoded: true).
  • Behind a proxy the TCP peer is the proxy: serverIPAddress is omitted and _network's addresses describe the proxy connection. With a standard *http.Transport, _network.proxy contains the selected proxy URL after password and configured query-parameter redaction. A custom RoundTripper may expose only the dialed proxy address as a host:port fallback.
  • A response body that is neither read nor closed never finalizes — no entry is produced (no finalizers by design).
  • _network.putIdle is best-effort: the connection's return to the idle pool can race with entry finalization, so absence means "not observed".

Compression and content decoding

  1. Transparent gzip — when you don't set Accept-Encoding, http.Transport negotiates and decompresses gzip itself. The record stores decoded bytes (_decoded: true), bodySize is -1.
  2. Record-time decoding — when you negotiated compression yourself, the wire bytes are captured as-is and, if a decoder is registered for the Content-Encoding, the HAR stores the decoded text while bodySize, the hash and the stream counters keep the wire view. JSON/XML redaction runs on the decoded form.

gzip, x-gzip and deflate (zlib-wrapped or raw, sniffed like browsers) ship by default using only the standard library. Brotli/zstd are deliberately not bundled; wiring them is a few lines with the de-facto standard pure-Go implementations (zero transitive deps, no cgo):

recorder.WithContentDecoder("br", func(r io.Reader) (io.ReadCloser, error) {
	return io.NopCloser(brotli.NewReader(r)), nil // github.com/andybalholm/brotli
})

recorder.WithContentDecoder("zstd", func(r io.Reader) (io.ReadCloser, error) {
	zr, err := zstd.NewReader(r) // github.com/klauspost/compress/zstd
	if err != nil {
		return nil, err
	}
	return zr.IOReadCloser(), nil
})

Safety rails: multi-step encodings (br, gzip) and partial/truncated captures are not decoded; a failing decoder falls back to the raw bytes (reported via OnInternalError); decoded output larger than MaxResponseBodyBytes is refused, so a compression bomb cannot blow the capture budget.

Recorder implementations

Recorder Stores entries? TraceStore? Best for
MemoryRecorder yes yes tests, short-lived capture, HAR()/WriteHAR
HARFileRecorder yes, until flush yes complete HAR file written atomically on Flush/Close
JSONStreamRecorder no no streaming NDJSON, one line per entry
RecorderFunc caller-defined no custom callbacks

All built-in recorders are safe for concurrent use; entries are immutable snapshots. Only finalized entries ever reach a recorder — in-flight exchanges are absent from every export.

Trace correlation

http.Client follows redirects above the transport, so each hop is a separate entry. Correlate them through the context:

  • recorder.WithTraceID(ctx, id) — install your correlation ID; every hop records it as _traceId with an incrementing _redirectIndex.
  • recorder.TraceContext(ctx) — same, with a generated random ID.

MemoryRecorder and HARFileRecorder implement the optional TraceStore capability (discovered via type assertion on Recorder):

  • EntriesByTrace(id) — return the trace's entries, keep them stored
  • RemoveTrace(id) — delete the trace's entries
  • TakeTrace(id)atomically remove and return them; an entry finalized concurrently can never fall between a query and a delete

MemoryRecorder additionally provides HARForTrace(id) as a convenience helper that builds a HAR document for one trace; it is not part of the TraceStore interface. For other recorders, combine TakeTrace/EntriesByTrace with recorder.NewHAR(entries).

HAR inspector

The repo ships a local React + TypeScript viewer under inspector/ for the HAR files this library produces, including every _ extension field:

HAR Inspector screenshot

  • Entry list with filtering (method, status class, state, error phase, host/path search, traceId, failed/truncated/closed-early), sorting, and trace-chain grouping by _traceId.
  • Detail tabs: overview with a timing waterfall, timings (-1 shown as "not observed"), request/response with JSON/XML pretty printing and binary indicators, _error, _network, _tls with the certificate chain, raw _trace timeline, raw JSON with unknown extensions preserved, and a Replay tab that reconstructs a cURL command (including --proxy from the redacted _network.proxy URL when present).
  • Virtualized list for large HAR files; responsive down to mobile widths.
cd inspector
npm install
npm run dev     # http://localhost:5173
npm run build
npm test

Workflow:

1. produce a HAR with recorder (WriteHAR / HARFileRecorder)
2. open the inspector
3. drag & drop the .har file (or use the file picker / built-in sample)
4. inspect entry details tab by tab

Security note: HAR files routinely contain sensitive data. The inspector parses files locally in your browser — nothing is uploaded anywhere. See inspector/README.md.

OpenTelemetry export (otelrecorder)

The otelrecorder submodule (its own Go module — the core stays dependency-free) exports finished entries as OTel span events and metrics through the OnEntryCompleted hook:

import "github.com/mgurevin/recorder/otelrecorder"

exporter, err := otelrecorder.NewExporter() // uses the global OTel providers
if err != nil {
	// handle
}
tr := recorder.NewTransport(base, rec,
	recorder.WithOnEntryCompleted(exporter.OnEntryCompleted))

With an active span in the request context, each exchange becomes a recorder.http.exchange span event; metrics (duration/body-size histograms, failure/closed-early/truncation counters) are always recorded.

Cardinality guidance: the adapter never exports full URLs, paths, query strings, header/cookie values, body content or raw HAR JSON. server.address is host-only, metric labels use the status class (2xx, 0), correlation IDs are span-event-only and opt-in, string values are clamped. Custom attributes are for user-controlled low-cardinality dimensions such as a route template — never raw paths or IDs. When you need the full HAR entry, write it to a dedicated sink (HARFileRecorder, JSONStreamRecorder); an OTel attribute is the wrong place for a document.

Development

go test ./...
go test -race ./...
go vet ./...
go test -bench . -run '^$'          # benchmarks (in-memory network, no OS sockets)
go test -fuzz FuzzRedactJSON        # fuzz targets: FuzzRedactJSON, FuzzRedactXML,
                                    # FuzzRedactURL, FuzzQueryPairs, FuzzHeaderPairs,
                                    # FuzzContentClassification, FuzzUnwrapChain,
                                    # FuzzHARSerialization

Architecture decisions, invariants and known limitations are documented in DESIGN.md.

Documentation

Overview

Package recorder records the full life cycle of net/http client calls — including calls that fail at the DNS, TCP, TLS or HTTP layer — and exports them as standard HAR 1.2 documents enriched with "_"-prefixed extension fields ("_error", "_network", "_tls", "_requestBody", "_responseBody", "_trace", ...). Stripping every extension field leaves a valid plain HAR 1.2 document.

The guiding principle: record what was actually observable during the call, as accurately and structurally as possible, without ever changing the behavior the caller sees. Values that cannot be measured reliably (wire header sizes, compressed body sizes after transparent gzip decoding, timing phases that did not happen) are reported as -1 or omitted — never invented.

Usage:

rec := recorder.NewMemoryRecorder()
client := &http.Client{
	Transport: recorder.NewTransport(http.DefaultTransport, rec),
}
resp, err := client.Get("https://example.com/")
// ... consume resp.Body; the entry is finalized on EOF/Close ...
_ = rec.WriteHAR(os.Stdout)

A response entry is not complete when RoundTrip returns: the body has not been read yet. The entry reaches the Recorder when the body hits EOF, is closed early, or fails — or immediately, when RoundTrip itself returns an error. A response body that is neither fully read nor closed (a caller bug) never finalizes; the library deliberately uses no finalizers.

Index

Constants

View Source
const (
	PhaseRequestSetup        = "request_setup"
	PhaseDNS                 = "dns"
	PhaseConnect             = "connect"
	PhaseProxy               = "proxy"
	PhaseTLS                 = "tls"
	PhaseWriteRequest        = "write_request"
	PhaseWriteRequestBody    = "write_request_body"
	PhaseWaitResponse        = "wait_response"
	PhaseReadResponseHeaders = "read_response_headers"
	PhaseReadResponseBody    = "read_response_body"
	PhaseRedirect            = "redirect"
	PhaseContext             = "context"
	PhaseUnknown             = "unknown"
)

Error phases. The phase names the step of the HTTP exchange in which the failure was observed.

View Source
const (
	StateCreated                 = "created"
	StateRequestStarted          = "request_started"
	StateRequestHeadersWritten   = "request_headers_written"
	StateRequestBodyStreaming    = "request_body_streaming"
	StateResponseHeadersReceived = "response_headers_received"
	StateResponseBodyStreaming   = "response_body_streaming"
	StateCompleted               = "completed"
	StateFailed                  = "failed"
	StateClosedEarly             = "closed_early"
)

Exchange life cycle states, recorded under "_state". Only terminal states (completed, failed, closed_early) ever appear in exported entries, because entries are emitted exclusively at finalization.

Variables

This section is empty.

Functions

func DefaultRedactedHeaders

func DefaultRedactedHeaders() []string

DefaultRedactedHeaders returns the headers redacted by default.

func DeflateDecoder

func DeflateDecoder(r io.Reader) (io.ReadCloser, error)

DeflateDecoder decodes "deflate" content using the standard library. Registered by default. HTTP's deflate is formally zlib-wrapped (RFC 9110), but a number of servers send raw DEFLATE streams; the decoder sniffs the zlib header and handles both, like browsers do.

func GzipDecoder

func GzipDecoder(r io.Reader) (io.ReadCloser, error)

GzipDecoder decodes gzip content using the standard library. Registered by default for "gzip" and "x-gzip".

func TraceContext

func TraceContext(ctx context.Context) (context.Context, string)

TraceContext is a convenience wrapper around WithTraceID that generates a fresh random trace ID and returns it alongside the derived context.

func TraceIDFromContext

func TraceIDFromContext(ctx context.Context) (string, bool)

TraceIDFromContext returns the correlation ID installed by WithTraceID.

func WithTraceID

func WithTraceID(ctx context.Context, id string) context.Context

WithTraceID returns a context carrying the given correlation ID. Every exchange started under this context records the ID as "_traceId" and an incrementing "_redirectIndex", which links redirect chains followed by http.Client into one logical trace.

Types

type BodyInfo

type BodyInfo struct {
	Present     bool `json:"present"`
	Complete    bool `json:"complete"`
	ClosedEarly bool `json:"closedEarly,omitempty"`
	Truncated   bool `json:"truncated,omitempty"`
	// CapturedBytes counts bytes stored for the HAR document.
	CapturedBytes int64 `json:"capturedBytes"`
	// TotalBytes counts every byte that actually flowed through the stream,
	// including bytes past the capture limit.
	TotalBytes int64 `json:"totalBytes"`
	// Hash covers TotalBytes worth of data and is only emitted when the
	// stream completed (a partial-stream hash would be misleading).
	Hash          string `json:"hash,omitempty"`
	HashAlgorithm string `json:"hashAlgorithm,omitempty"`
	ReadError     string `json:"readError,omitempty"`
	CloseError    string `json:"closeError,omitempty"`
	// Store is an external reference (e.g. a file path) when the BodyStore
	// keeps content out of memory.
	Store string `json:"store,omitempty"`
}

BodyInfo is the "_requestBody" / "_responseBody" extension describing what happened to a body stream.

type BodyMetadata

type BodyMetadata struct {
	ExchangeID  string
	Direction   string // "request" or "response"
	ContentType string
	// SizeHint is the expected number of bytes this writer will receive
	// (derived from Content-Length, already clamped to the capture limit),
	// or 0 when unknown. Stores may use it to pre-allocate, but must treat
	// it as untrusted: the actual stream may be shorter or longer, and a
	// hostile peer can announce an absurd Content-Length.
	SizeHint int64
}

BodyMetadata describes the body stream a BodyWriter is created for.

type BodyStore

type BodyStore interface {
	NewWriter(ctx context.Context, metadata BodyMetadata) (BodyWriter, error)
}

BodyStore creates BodyWriter instances. Implementations must be safe for concurrent use.

type BodyWriter

type BodyWriter interface {
	io.Writer
	Close() error
	// Bytes returns the bytes captured so far (used when embedding content
	// into the HAR document).
	Bytes() ([]byte, error)
	// Ref returns an external reference to the stored content (e.g. a file
	// path), or "" when the content lives inline in memory.
	Ref() string
}

BodyWriter receives captured body bytes for one stream. Implementations must be safe for concurrent use of Write with Bytes.

type Cache

type Cache struct{}

Cache is the HAR cache record. This library performs no caching, so the object is intentionally empty (allowed by HAR 1.2).

type CertInfo

type CertInfo struct {
	Subject            string   `json:"subject"`
	Issuer             string   `json:"issuer"`
	SerialNumber       string   `json:"serialNumber"`
	DNSNames           []string `json:"dnsNames,omitempty"`
	IPAddresses        []string `json:"ipAddresses,omitempty"`
	NotBefore          string   `json:"notBefore"`
	NotAfter           string   `json:"notAfter"`
	PublicKeyAlgorithm string   `json:"publicKeyAlgorithm"`
	SignatureAlgorithm string   `json:"signatureAlgorithm"`
	SHA256Fingerprint  string   `json:"sha256Fingerprint"`
	RawDER             string   `json:"rawDER,omitempty"`
}

CertInfo describes one peer certificate.

type Content

type Content struct {
	Size        int64  `json:"size"`
	Compression int64  `json:"compression,omitempty"`
	MimeType    string `json:"mimeType"`
	Text        string `json:"text,omitempty"`
	Encoding    string `json:"encoding,omitempty"`
	Comment     string `json:"comment,omitempty"`
	Decoded     bool   `json:"_decoded,omitempty"`
}

Content is the HAR content record. Size is the decoded content length when the decoded form is known (transparent gzip by http.Transport, or a configured ContentDecoder), otherwise the bytes the caller actually read. "_decoded" marks that Text/Size describe the decoded form rather than the raw wire bytes.

type ContentDecoder

type ContentDecoder func(io.Reader) (io.ReadCloser, error)

ContentDecoder turns a compressed body stream into its decoded form. It is used only while building the HAR record: when a captured response body carries a Content-Encoding with a registered decoder, content.text/size are stored in decoded form (marked "_decoded": true) while bodySize, the body hash and the "_responseBody" counters keep describing the real wire bytes. The bytes handed to the caller are never touched.

Decoders for encodings outside the standard library (brotli, zstd) are deliberately not bundled — the module stays dependency-free. Registering one is a few lines with the de-facto standard implementations:

import "github.com/andybalholm/brotli"

recorder.WithContentDecoder("br", func(r io.Reader) (io.ReadCloser, error) {
	return io.NopCloser(brotli.NewReader(r)), nil
})

import "github.com/klauspost/compress/zstd"

recorder.WithContentDecoder("zstd", func(r io.Reader) (io.ReadCloser, error) {
	zr, err := zstd.NewReader(r)
	if err != nil {
		return nil, err
	}
	return zr.IOReadCloser(), nil
})
type Cookie struct {
	Name     string `json:"name"`
	Value    string `json:"value"`
	Path     string `json:"path,omitempty"`
	Domain   string `json:"domain,omitempty"`
	Expires  string `json:"expires,omitempty"`
	HTTPOnly bool   `json:"httpOnly,omitempty"`
	Secure   bool   `json:"secure,omitempty"`
}

Cookie is the HAR cookie record.

type Creator

type Creator struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

Creator identifies the producing application.

type Entry

type Entry struct {
	StartedDateTime string    `json:"startedDateTime"`
	Time            float64   `json:"time"`
	Request         *Request  `json:"request"`
	Response        *Response `json:"response"`
	Cache           *Cache    `json:"cache"`
	Timings         *Timings  `json:"timings"`
	ServerIPAddress string    `json:"serverIPAddress,omitempty"`
	Connection      string    `json:"connection,omitempty"`
	Comment         string    `json:"comment,omitempty"`

	// Extensions.
	TraceID                  string                  `json:"_traceId,omitempty"`
	ExchangeID               string                  `json:"_exchangeId,omitempty"`
	RedirectIndex            *int                    `json:"_redirectIndex,omitempty"`
	State                    string                  `json:"_state,omitempty"`
	Error                    *ErrorInfo              `json:"_error,omitempty"`
	Network                  *NetworkInfo            `json:"_network,omitempty"`
	TLS                      *TLSInfo                `json:"_tls,omitempty"`
	Expect100                *Expect100Info          `json:"_expect100,omitempty"`
	Informational            []InformationalResponse `json:"_informational,omitempty"`
	RequestBody              *BodyInfo               `json:"_requestBody,omitempty"`
	ResponseBody             *BodyInfo               `json:"_responseBody,omitempty"`
	RequestTrailers          []NameValuePair         `json:"_requestTrailers,omitempty"`
	ResponseTrailers         []NameValuePair         `json:"_responseTrailers,omitempty"`
	RequestTransferEncoding  []string                `json:"_requestTransferEncoding,omitempty"`
	ResponseTransferEncoding []string                `json:"_responseTransferEncoding,omitempty"`
	RawTrace                 []TraceEvent            `json:"_trace,omitempty"`
	// contains filtered or unexported fields
}

Entry is a single HAR entry: one physical HTTP exchange. Fields whose JSON name starts with "_" are application extensions per HAR 1.2; removing every "_" field leaves a valid plain HAR 1.2 entry.

Entries produced by Transport are immutable snapshots: neither the Transport nor the built-in recorders mutate an Entry after it has been handed to Recorder.Record.

func (*Entry) StartTime

func (e *Entry) StartTime() time.Time

StartTime returns the request start time used for ordering entries.

type ErrorInfo

type ErrorInfo struct {
	Phase                   string   `json:"phase"`
	Type                    string   `json:"type"`
	Message                 string   `json:"message"`
	Timeout                 bool     `json:"timeout"`
	Temporary               bool     `json:"temporary"`
	ContextCanceled         bool     `json:"contextCanceled"`
	ContextDeadlineExceeded bool     `json:"contextDeadlineExceeded"`
	Cause                   string   `json:"cause,omitempty"`
	UnwrapChain             []string `json:"unwrapChain,omitempty"`
}

ErrorInfo is the structured "_error" extension describing a transport or body-stream failure.

type Expect100Info

type Expect100Info struct {
	// Waited reports that the transport paused before sending the body.
	Waited bool `json:"waited"`
	// ContinueReceived reports that the server sent 100 Continue.
	ContinueReceived bool `json:"continueReceived"`
	// WaitMS is the pause between waiting and the 100 arriving; omitted when
	// either side of the interval was not observed.
	WaitMS float64 `json:"waitMs,omitempty"`
}

Expect100Info describes an "Expect: 100-continue" handshake observed on this exchange.

type FileBodyStore

type FileBodyStore struct {
	// Dir is the directory temp files are created in; empty means the system
	// temp directory.
	Dir string
}

FileBodyStore spools captured bodies to temporary files so large bodies do not have to live in memory. Files are NOT deleted automatically; their paths are exposed via BodyInfo.Store and cleanup is the caller's responsibility.

func (FileBodyStore) NewWriter

func (s FileBodyStore) NewWriter(_ context.Context, meta BodyMetadata) (BodyWriter, error)

NewWriter implements BodyStore.

type HAR

type HAR struct {
	Log *Log `json:"log"`
}

HAR is the top-level HAR 1.2 document.

func NewHAR

func NewHAR(entries []*Entry) *HAR

NewHAR builds a HAR document from finished entries. Entries are ordered by request start time (stable, so equal timestamps keep recording order). Only finalized entries ever reach a Recorder, so in-flight exchanges are by definition absent from the export.

func (*HAR) Write

func (h *HAR) Write(w io.Writer) error

Write serializes the document as indented JSON. Field order follows struct declaration order and header lists are sorted, so output is deterministic for identical entries.

type HARFileRecorder

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

HARFileRecorder buffers finalized entries and writes a complete HAR document to a file on Flush (atomically, via a temp file + rename). It is safe for concurrent use.

func NewHARFileRecorder

func NewHARFileRecorder(path string) *HARFileRecorder

NewHARFileRecorder creates a recorder that will write to path on Flush.

func (*HARFileRecorder) Close

func (r *HARFileRecorder) Close() error

Close flushes the document; it satisfies io.Closer for defer-friendly use.

func (*HARFileRecorder) EntriesByTrace

func (r *HARFileRecorder) EntriesByTrace(traceID string) []*Entry

EntriesByTrace implements TraceStore: entries are buffered until Flush, so they remain queryable by trace ID.

func (*HARFileRecorder) Flush

func (r *HARFileRecorder) Flush() error

Flush writes the full HAR document collected so far. It can be called any number of times; each call rewrites the file atomically.

func (*HARFileRecorder) Record

func (r *HARFileRecorder) Record(e *Entry)

Record implements Recorder.

func (*HARFileRecorder) RemoveTrace

func (r *HARFileRecorder) RemoveTrace(traceID string) int

RemoveTrace implements TraceStore. Removed entries are excluded from every subsequent Flush.

func (*HARFileRecorder) TakeTrace

func (r *HARFileRecorder) TakeTrace(traceID string) []*Entry

TakeTrace implements TraceStore: it atomically removes and returns the trace's entries. The taken entries are excluded from every subsequent Flush — use this to split one shared recording into per-call HAR files (recorder.NewHAR(taken).Write(...)).

type InformationalResponse

type InformationalResponse struct {
	Status  int             `json:"status"`
	Headers []NameValuePair `json:"headers,omitempty"`
}

InformationalResponse is one 1xx interim response (100 Continue, 103 Early Hints, ...) observed before the final response. Headers are redacted with the same rules as final response headers.

type InternalErrorMode

type InternalErrorMode int

InternalErrorMode controls how recorder-internal failures (body store errors, recorder panics) are reported. The wrapped HTTP call is never retried or altered by an internal error.

const (
	// InternalErrorIgnore drops internal errors after reporting them through
	// Options.OnInternalError (when set). The HTTP call is never affected.
	// This is the default.
	InternalErrorIgnore InternalErrorMode = iota

	// InternalErrorLog behaves like InternalErrorIgnore but additionally
	// writes the error using Options.Logf (or the standard log package when
	// Logf is nil).
	InternalErrorLog
)

type JSONStreamRecorder

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

JSONStreamRecorder writes every finalized entry immediately as one JSON document per line (NDJSON) to the underlying writer. This is the streaming export path: it deliberately does not emit the enclosing HAR wrapper, so the top-level HAR JSON structure is never left half-written; consumers can wrap the lines into a log object themselves. Safe for concurrent use.

JSONStreamRecorder intentionally does not implement TraceStore: entries leave the process the moment they are recorded, so there is nothing left to query or remove. Group downstream by each line's "_traceId" field, or use a retaining recorder (MemoryRecorder, HARFileRecorder) when per-trace access is needed.

func NewJSONStreamRecorder

func NewJSONStreamRecorder(w io.Writer) *JSONStreamRecorder

NewJSONStreamRecorder creates a streaming recorder writing to w.

func (*JSONStreamRecorder) Err

func (r *JSONStreamRecorder) Err() error

Err returns the first encoding error encountered, if any.

func (*JSONStreamRecorder) Record

func (r *JSONStreamRecorder) Record(e *Entry)

Record implements Recorder. Encoding errors are retained and observable via Err; recording never panics.

type Log

type Log struct {
	Version string   `json:"version"`
	Creator *Creator `json:"creator"`
	Entries []*Entry `json:"entries"`
	Comment string   `json:"comment,omitempty"`
}

Log is the HAR 1.2 log object.

type MemoryBodyStore

type MemoryBodyStore struct{}

MemoryBodyStore keeps captured bodies in memory. It is the default store.

func (MemoryBodyStore) NewWriter

NewWriter implements BodyStore. A positive SizeHint pre-sizes the buffer (bounded by maxPreallocBytes) so growth re-copies are avoided for bodies with a truthful Content-Length; a wrong hint costs at most one bounded allocation and never breaks the capture.

type MemoryRecorder

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

MemoryRecorder collects finalized entries in memory. It is safe for concurrent use. Entries handed to it are immutable snapshots, so the copies returned by Entries can be shared freely.

func NewMemoryRecorder

func NewMemoryRecorder() *MemoryRecorder

NewMemoryRecorder returns an empty in-memory recorder.

func (*MemoryRecorder) Entries

func (r *MemoryRecorder) Entries() []*Entry

Entries returns a copy of the recorded entries in recording order.

func (*MemoryRecorder) EntriesByTrace

func (r *MemoryRecorder) EntriesByTrace(traceID string) []*Entry

EntriesByTrace returns a copy of the entries belonging to the given trace ID (see WithTraceID), in recording order. The recorder keeps the entries.

func (*MemoryRecorder) HAR

func (r *MemoryRecorder) HAR() *HAR

HAR builds a HAR 1.2 document from the entries recorded so far, ordered by request start time. In-flight exchanges are absent by definition: entries only reach the recorder once finalized.

func (*MemoryRecorder) HARForTrace

func (r *MemoryRecorder) HARForTrace(traceID string) *HAR

HARForTrace builds a HAR 1.2 document containing only the entries of the given trace ID (the recorder keeps them; combine with TakeTrace + NewHAR to export-and-drop in one step).

func (*MemoryRecorder) Len

func (r *MemoryRecorder) Len() int

Len returns the number of recorded entries.

func (*MemoryRecorder) Record

func (r *MemoryRecorder) Record(e *Entry)

Record implements Recorder.

func (*MemoryRecorder) RemoveTrace

func (r *MemoryRecorder) RemoveTrace(traceID string) int

RemoveTrace deletes every entry belonging to the given trace ID and returns how many were removed.

func (*MemoryRecorder) Reset

func (r *MemoryRecorder) Reset()

Reset discards all recorded entries.

func (*MemoryRecorder) TakeTrace

func (r *MemoryRecorder) TakeTrace(traceID string) []*Entry

TakeTrace atomically removes and returns the entries belonging to the given trace ID, in recording order. Atomicity matters on a shared client: an entry finalized concurrently can never be lost between a query and a separate delete.

func (*MemoryRecorder) WriteHAR

func (r *MemoryRecorder) WriteHAR(w io.Writer) error

WriteHAR serializes the current HAR document to w as indented JSON.

type NameValuePair

type NameValuePair struct {
	Name    string `json:"name"`
	Value   string `json:"value"`
	Comment string `json:"comment,omitempty"`
}

NameValuePair is the HAR record for headers and query parameters.

type NetworkInfo

type NetworkInfo struct {
	DNSAddresses []string `json:"dnsAddresses,omitempty"`
	// DNSCoalesced marks that this exchange's DNS answer was shared with a
	// concurrent lookup for the same host (singleflight) rather than issued
	// on its own.
	DNSCoalesced     bool    `json:"dnsCoalesced,omitempty"`
	Network          string  `json:"network,omitempty"`
	LocalAddress     string  `json:"localAddress,omitempty"`
	RemoteAddress    string  `json:"remoteAddress,omitempty"`
	IPVersion        string  `json:"ipVersion,omitempty"`
	ConnectionReused bool    `json:"connectionReused"`
	WasIdle          bool    `json:"wasIdle"`
	IdleTimeMS       float64 `json:"idleTimeMs,omitempty"`
	// Proxy is the redacted proxy URL selected by a standard http.Transport.
	// Custom RoundTrippers may only expose the dialed host:port.
	Proxy string `json:"proxy,omitempty"`
	HTTP2 bool   `json:"http2"`
	// PutIdle reports whether the connection went back to the idle pool
	// after this exchange. Best effort: the pool return can race with entry
	// finalization, so absence means "not observed", not "did not happen".
	PutIdle *PutIdleInfo `json:"putIdle,omitempty"`
}

NetworkInfo is the "_network" extension: connection-level facts observed through httptrace. These describe the physical connection this exchange used — when a proxy is in play (Proxy != ""), RemoteAddress, IPVersion and DNSAddresses therefore refer to the proxy, not the origin server: the origin is never observable client-side through a proxy. HTTP2 is true only when HTTP/2 was positively observed.

type OnEntryCompleted

type OnEntryCompleted func(context.Context, *Entry)

OnEntryCompleted is invoked with the request context and the finished HAR entry every time an exchange is finalized.

type Option

type Option func(*Options)

Option mutates Options during NewTransport.

func WithBodyStore

func WithBodyStore(s BodyStore) Option

WithBodyStore sets the storage backend for captured body bytes.

func WithCaptureCertificates

func WithCaptureCertificates(v, raw bool) Option

WithCaptureCertificates toggles certificate detail capture; raw controls embedding of raw DER bytes.

func WithCaptureCookies

func WithCaptureCookies(v bool) Option

WithCaptureCookies toggles cookie capture.

func WithCaptureHeaders

func WithCaptureHeaders(v bool) Option

WithCaptureHeaders toggles header capture.

func WithCaptureRawTrace

func WithCaptureRawTrace(v bool) Option

WithCaptureRawTrace toggles the "_trace" raw event extension. Details are sanitized by RedactErrorMessage before export.

func WithCaptureRequestBody

func WithCaptureRequestBody(v bool) Option

WithCaptureRequestBody toggles request body content capture.

func WithCaptureResponseBody

func WithCaptureResponseBody(v bool) Option

WithCaptureResponseBody toggles response body content capture.

func WithCaptureTLS

func WithCaptureTLS(v bool) Option

WithCaptureTLS toggles the "_tls" extension.

func WithContentDecoder

func WithContentDecoder(encoding string, dec ContentDecoder) Option

WithContentDecoder registers a decoder for a Content-Encoding token (case-insensitive), e.g. "br" or "zstd". See ContentDecoder for recipes.

func WithEmbedBodies

func WithEmbedBodies(v bool) Option

WithEmbedBodies toggles embedding captured body content into the HAR document. See Options.EmbedBodies.

func WithErrorRedactor

func WithErrorRedactor(fn func(string) string) Option

WithErrorRedactor sets the error message redaction function.

func WithHashBodies

func WithHashBodies(enabled bool, algorithm string) Option

WithHashBodies configures body hashing.

func WithInternalErrorMode

func WithInternalErrorMode(m InternalErrorMode) Option

WithInternalErrorMode sets the internal error policy.

func WithLogf

func WithLogf(fn func(format string, args ...any)) Option

WithLogf sets the logger used by InternalErrorLog.

func WithMaxRequestBodyBytes

func WithMaxRequestBodyBytes(n int64) Option

WithMaxRequestBodyBytes sets the request body capture limit (<= 0: unlimited).

func WithMaxResponseBodyBytes

func WithMaxResponseBodyBytes(n int64) Option

WithMaxResponseBodyBytes sets the response body capture limit (<= 0: unlimited).

func WithOnEntryCompleted

func WithOnEntryCompleted(fn OnEntryCompleted) Option

WithOnEntryCompleted sets the per-entry completion callback.

func WithOnInternalError

func WithOnInternalError(fn func(error)) Option

WithOnInternalError sets the internal error callback.

func WithOptions

func WithOptions(o Options) Option

WithOptions replaces the whole Options value. Apply it first when combining with other Option values.

func WithRedactCookies

func WithRedactCookies(names ...string) Option

WithRedactCookies appends cookie names to the redaction list.

func WithRedactHeaders

func WithRedactHeaders(names ...string) Option

WithRedactHeaders appends header names to the redaction list.

func WithRedactJSONFields

func WithRedactJSONFields(names ...string) Option

WithRedactJSONFields appends JSON field names to the redaction list.

func WithRedactQueryParameters

func WithRedactQueryParameters(names ...string) Option

WithRedactQueryParameters appends query parameter names to the redaction list.

func WithRedactXMLElements

func WithRedactXMLElements(names ...string) Option

WithRedactXMLElements appends XML element local names to the redaction list (SOAP bodies included).

type Options

type Options struct {
	// CaptureRequestBody enables storing request body content. Body size and
	// completion state are always tracked, even when this is false.
	CaptureRequestBody bool
	// CaptureResponseBody enables storing response body content.
	CaptureResponseBody bool
	// EmbedBodies controls whether captured body content is embedded into
	// the HAR document (postData.text / content.text). When false, bodies
	// are still captured into the BodyStore and the record keeps sizes,
	// hashes, truncation state and the store reference
	// ("_requestBody"/"_responseBody".store) — the intended production
	// setting together with FileBodyStore, keeping HAR documents small
	// while body content stays retrievable. Like the Capture* flags, the
	// zero value disables embedding; WithEmbedBodies enables it explicitly.
	EmbedBodies bool
	// MaxRequestBodyBytes limits how many request body bytes are stored.
	// Values <= 0 mean unlimited. The total size keeps being counted after
	// the limit is reached; only content capture stops.
	MaxRequestBodyBytes int64
	// MaxResponseBodyBytes is the response-side equivalent of
	// MaxRequestBodyBytes.
	MaxResponseBodyBytes int64
	// CaptureTLS enables the "_tls" extension built from tls.ConnectionState.
	CaptureTLS bool
	// CaptureCertificates includes peer certificate details in "_tls".
	CaptureCertificates bool
	// CaptureRawCertificates additionally embeds each certificate's raw DER
	// bytes as Base64. Off by default because of size.
	CaptureRawCertificates bool
	// CaptureHeaders enables recording request/response headers and trailers.
	CaptureHeaders bool
	// CaptureCookies enables recording parsed cookies.
	CaptureCookies bool

	// RedactHeaders lists header names (case-insensitive) whose values are
	// replaced with "[REDACTED]". See DefaultRedactedHeaders.
	RedactHeaders []string
	// RedactQueryParameters lists query parameter names (case-insensitive)
	// to redact, both in request.url and in request.queryString. The same
	// list is applied to application/x-www-form-urlencoded body params.
	RedactQueryParameters []string
	// RedactCookies lists cookie names (case-insensitive) to redact. A cookie
	// is also redacted when its carrying header (Cookie / Set-Cookie) is in
	// RedactHeaders.
	RedactCookies []string
	// RedactJSONFields lists JSON object field names (case-insensitive) whose
	// values are replaced recursively in captured JSON bodies. Only applied
	// to fully captured (complete, non-truncated) bodies.
	RedactJSONFields []string
	// RedactXMLElements lists XML element local names (case-insensitive,
	// namespace prefixes ignored) whose text content is replaced in captured
	// XML/SOAP bodies — e.g. "Password" covers <wsse:Password> in a
	// WS-Security UsernameToken. The matched element's whole subtree is
	// redacted; the rest of the document is preserved byte-for-byte.
	// Attribute values are not redacted. Only applied to fully captured
	// (complete, non-truncated) bodies.
	RedactXMLElements []string

	// HashBodies enables hashing of body streams. The hash covers every byte
	// that actually flowed, including bytes beyond the capture limit.
	HashBodies bool
	// BodyHashAlgorithm selects the hash: "sha256" (default), "sha1" or
	// "md5". Unknown values fall back to sha256.
	BodyHashAlgorithm string

	// CaptureRawTrace stores every raw httptrace event under "_trace". Event
	// details pass through RedactErrorMessage before export.
	CaptureRawTrace bool

	// ContentDecoders maps Content-Encoding tokens (lower-case) to decoders
	// used when building the HAR record, so compressed captured bodies can
	// be stored in readable, decoded form. DefaultOptions installs the
	// stdlib-only set (gzip, x-gzip, deflate); WithContentDecoder registers
	// additional ones such as brotli or zstd — see ContentDecoder for
	// ready-to-paste recipes. Decoding never touches the bytes the caller
	// reads and never runs on partial or truncated captures.
	ContentDecoders map[string]ContentDecoder

	// BodyStore provides storage for captured body bytes. Nil means
	// MemoryBodyStore.
	BodyStore BodyStore

	// InternalErrorMode selects the internal error policy. See the constants.
	InternalErrorMode InternalErrorMode
	// OnInternalError, when set, receives every recorder-internal error.
	OnInternalError func(error)
	// Logf is used by InternalErrorLog. Nil falls back to log.Printf.
	Logf func(format string, args ...any)

	// OnEntryCompleted, when set, is invoked with the request context and the
	// finished entry every time an exchange is finalized (after
	// Transport.Recorder.Record).
	OnEntryCompleted OnEntryCompleted

	// RedactErrorMessage, when set, is applied to every error message and raw
	// trace detail before export (they can contain URLs or credentials).
	RedactErrorMessage func(string) string
}

Options configures a Transport. The zero value disables all capturing; use DefaultOptions (applied automatically by NewTransport) for sensible production defaults.

Options must not be mutated after the Transport has served its first request.

func DefaultOptions

func DefaultOptions() Options

DefaultOptions returns the production-safe options NewTransport starts from. Body streams are counted for lifecycle and size metadata, but their content is neither stored, embedded nor hashed by default. TLS metadata, headers and cookies remain enabled, with DefaultRedactedHeaders applied.

type PostData

type PostData struct {
	MimeType string      `json:"mimeType"`
	Params   []PostParam `json:"params,omitempty"`
	Text     string      `json:"text,omitempty"`
	Encoding string      `json:"_encoding,omitempty"`
	Comment  string      `json:"comment,omitempty"`
}

PostData is the HAR postData record. "_encoding" is an extension marking Base64-encoded binary request bodies (plain HAR has no encoding field on postData).

type PostParam

type PostParam struct {
	Name        string `json:"name"`
	Value       string `json:"value,omitempty"`
	FileName    string `json:"fileName,omitempty"`
	ContentType string `json:"contentType,omitempty"`
}

PostParam is a single posted form parameter.

type PutIdleInfo

type PutIdleInfo struct {
	Returned bool   `json:"returned"`
	Error    string `json:"error,omitempty"`
}

PutIdleInfo is the structured outcome of the connection's return to the keep-alive pool.

type Recorder

type Recorder interface {
	Record(entry *Entry)
}

Recorder receives finalized, immutable entries. Implementations must be safe for concurrent use: entries arrive from whichever goroutine finishes reading a response body.

type RecorderFunc

type RecorderFunc func(*Entry)

RecorderFunc adapts a function into a Recorder (the "callback recorder"). The function must be safe for concurrent use.

func (RecorderFunc) Record

func (f RecorderFunc) Record(e *Entry)

Record implements Recorder.

type Request

type Request struct {
	Method      string          `json:"method"`
	URL         string          `json:"url"`
	HTTPVersion string          `json:"httpVersion"`
	Cookies     []Cookie        `json:"cookies"`
	Headers     []NameValuePair `json:"headers"`
	QueryString []NameValuePair `json:"queryString"`
	PostData    *PostData       `json:"postData,omitempty"`
	// HeadersSize is -1: the exact serialized header bytes written to the
	// wire (including headers added internally by http.Transport) are not
	// observable at the RoundTripper layer.
	HeadersSize int64  `json:"headersSize"`
	BodySize    int64  `json:"bodySize"`
	Comment     string `json:"comment,omitempty"`
}

Request is the HAR request record.

type Response

type Response struct {
	Status      int             `json:"status"`
	StatusText  string          `json:"statusText"`
	HTTPVersion string          `json:"httpVersion"`
	Cookies     []Cookie        `json:"cookies"`
	Headers     []NameValuePair `json:"headers"`
	Content     *Content        `json:"content"`
	RedirectURL string          `json:"redirectURL"`
	// HeadersSize is -1: wire-level header size is not observable here.
	HeadersSize int64 `json:"headersSize"`
	// BodySize is the payload size received on the wire, or -1 when unknown
	// (body not fully read, or transparently decompressed by http.Transport
	// so the compressed wire size is no longer observable).
	BodySize int64  `json:"bodySize"`
	Comment  string `json:"comment,omitempty"`
}

Response is the HAR response record. For exchanges that failed before an HTTP response existed, Status is 0 and "_error" on the entry carries the failure detail.

type TLSInfo

type TLSInfo struct {
	Version            string     `json:"version"`
	CipherSuite        string     `json:"cipherSuite"`
	NegotiatedProtocol string     `json:"negotiatedProtocol,omitempty"`
	ServerName         string     `json:"serverName,omitempty"`
	HandshakeComplete  bool       `json:"handshakeComplete"`
	DidResume          bool       `json:"didResume"`
	OCSPStapled        bool       `json:"ocspStapled"`
	SCTCount           int        `json:"sctCount"`
	VerifiedChains     int        `json:"verifiedChains"`
	PeerCertificates   []CertInfo `json:"peerCertificates,omitempty"`
}

TLSInfo is the "_tls" extension built from tls.ConnectionState.

type Timings

type Timings struct {
	Blocked float64 `json:"blocked"`
	DNS     float64 `json:"dns"`
	Connect float64 `json:"connect"`
	Send    float64 `json:"send"`
	Wait    float64 `json:"wait"`
	Receive float64 `json:"receive"`
	SSL     float64 `json:"ssl"`
	Comment string  `json:"comment,omitempty"`
}

Timings is the HAR timings record. All values are milliseconds; -1 means the phase did not apply or could not be measured (per HAR 1.2).

type TraceEvent

type TraceEvent struct {
	Name   string `json:"name"`
	Time   string `json:"time"`
	Detail string `json:"detail,omitempty"`
}

TraceEvent is one raw httptrace event, stored under the "_trace" extension when Options.CaptureRawTrace is enabled.

type TraceStore

type TraceStore interface {
	// EntriesByTrace returns the entries belonging to the trace ID, in
	// recording order, without removing them.
	EntriesByTrace(traceID string) []*Entry
	// RemoveTrace deletes the trace's entries and returns how many were
	// removed.
	RemoveTrace(traceID string) int
	// TakeTrace atomically removes and returns the trace's entries, in
	// recording order.
	TakeTrace(traceID string) []*Entry
}

TraceStore is an optional capability for recorders that retain entries and can query or remove them by trace ID (see WithTraceID). The Recorder interface itself stays minimal — a custom recorder is still just one method — and callers discover the capability by type assertion:

if ts, ok := rec.(TraceStore); ok {
	entries := ts.TakeTrace(id)
}

MemoryRecorder and HARFileRecorder implement TraceStore. JSONStreamRecorder cannot: its entries leave the process the moment they are recorded, so there is nothing left to query or remove.

type Transport

type Transport struct {
	// Base is the wrapped RoundTripper; nil means http.DefaultTransport.
	Base http.RoundTripper
	// Recorder receives finalized entries; nil disables recording (the
	// OnEntryCompleted callback still fires).
	Recorder Recorder
	// Options configures capturing and redaction.
	Options Options
	// contains filtered or unexported fields
}

Transport is an http.RoundTripper that records every exchange passing through it. It wraps a base RoundTripper (http.DefaultTransport when Base is nil) and never alters the request, response, or error the caller sees.

Transport is safe for concurrent use by multiple goroutines provided its fields are not mutated after the first request. Prefer NewTransport, which also applies DefaultOptions; a zero-value literal works but captures nothing until Options are set. When a standard *http.Transport has a Proxy callback, NewTransport clones it once so the selected proxy URL can be observed without invoking that callback twice; configure the base before passing it in and close idle connections through this Transport or its http.Client.

func NewTransport

func NewTransport(base http.RoundTripper, rec Recorder, opts ...Option) *Transport

NewTransport builds a Transport wrapping base. rec may be nil, in which case a fresh MemoryRecorder is installed (accessible via the Recorder field). Options start from DefaultOptions and are adjusted by opts.

func (*Transport) CloseIdleConnections

func (t *Transport) CloseIdleConnections()

CloseIdleConnections forwards to the wrapped transport when it supports it, keeping http.Client.CloseIdleConnections working through the wrapper.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper. The caller's request object is never mutated: recording hooks are attached to a clone. The returned response and error are exactly what the base transport produced, except that resp.Body is wrapped to observe the caller's reads.

Directories

Path Synopsis
otelrecorder module

Jump to

Keyboard shortcuts

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