recorder

package module
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 42 Imported by: 0

README

recorder

CI Go Reference Coverage API compatibility

recorder is a dependency-free evidence-generation tool for Go HTTP clients. It records complete exchanges as HAR 1.2—including failures, timings, connection/TLS facts, streamed body lifecycle, redaction audit, and redirect correlation—using only facts observable at the http.RoundTripper boundary. Information that was not observed remains absent or explicitly unknown.

It was built for cases—especially financial API integrations—where preserving an accurate, privacy-aware record of what the client observed is operationally important. It never retries requests, consumes bodies on the caller's behalf, or modifies live request and response data. Recording failures are contained: they may reduce the captured evidence, but never replace or alter the HTTP response or error returned to the application.

Its design is guided by four principles:

  • Observe without interference.
  • Record only verifiable facts.
  • Fail without affecting HTTP behavior.
  • Keep resource use and sensitive data bounded.

Features

  • Complete HAR 1.2 records for successful and failed HTTP exchanges
  • DNS, connect, proxy, TLS, request, response, and body-stream diagnostics
  • Accurate timing waterfalls and redirect/trace correlation
  • Bounded body capture with memory or managed file storage
  • Streaming redaction for JSON, NDJSON, XML, form, and multipart bodies
  • Redact, AES-GCM encrypt, or HMAC-tokenize sensitive values
  • Request-scoped redaction, capture policies, sampling, and tail retention
  • Bounded asynchronous delivery with batching and backpressure controls
  • OpenTelemetry metrics and span-event integration
  • Browser-only HAR Inspector with replay, protection audit, and safe previews
  • Loopback-only DebugStreamRecorder and Inspector live mode for local debugging
  • Optional cmd/recorder CLI for bounded validation, summaries, conversion, evidence and FileBodyStore verification, deterministic fixture workflows, compatibility diagnostics, reconciliation, and local Inspector handoff
  • Bounded HAR/NDJSON readers and deterministic network-free HTTP test fixtures
  • Standard-library-only core package

Install

go get github.com/mgurevin/recorder

For capture-file workflows, install the optional recorder CLI:

go install github.com/mgurevin/recorder/cmd/recorder@latest

The minimum supported Go release is documented in go.mod and verified in CI.

Quick start

rec := recorder.NewMemoryRecorder()
config := recorder.DefaultConfig()

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

resp, err := client.Get("https://example.com/")
if err == nil {
	_, _ = io.Copy(io.Discard, resp.Body)
	_ = resp.Body.Close()
}

_ = rec.WriteHAR(os.Stdout)

A successful exchange finalizes when the response body reaches EOF, is closed, or fails. Transport errors finalize immediately. A response body that is never read and never closed produces no entry and also prevents normal net/http connection reuse.

Configuration

The public configuration model is intentionally explicit. Start with a default value, modify fields, and pass it to the constructor:

config := recorder.DefaultConfig()
config.CaptureRequestBody = true
config.CaptureResponseBody = true
config.EmbedBodies = false
config.MaxResponseBodyBytes = 4 << 20
config.HashBodies = true
config.Redaction = recorder.RedactionConfig{
	Common: recorder.RedactionRules{
		Headers:    recorder.DefaultRedactedHeaders(),
		JSONFields: []string{"password", "token"},
		XMLElements: []string{"Password"},
	},
}

if err := config.Validate(); err != nil {
	log.Fatal(err)
}

transport := recorder.NewTransport(http.DefaultTransport, rec, config)

Config{} is a deliberately minimal zero value. DefaultConfig() is the recommended production baseline. Call Validate after applying application settings so invalid combinations, unsupported algorithms, missing protection providers, and malformed registrations fail during startup. Validation is static: it performs no I/O and does not invoke callbacks or providers.

Important defaults:

  • body content capture, embedding, hashing, and raw trace capture are off;
  • headers, cookies, TLS facts, and certificate metadata are on;
  • request and response capture limits are 1 MiB;
  • authorization, proxy authorization, cookies, and common API-key headers are redacted;
  • gzip and deflate record-time decoding are registered;
  • internal recorder failures are logged through log.Printf unless a custom Logf is supplied;
  • sampling and retention policies are nil, so every exchange is recorded.

Request-scoped redaction can add rules without creating another client:

req = recorder.RequestWithRedaction(req, recorder.RedactionConfig{
	Request: recorder.RedactionRules{JSONFields: []string{"cardNumber"}},
})

A request-scoped annotation can describe why an exchange was made without a recorder-specific extension:

req = recorder.RequestWithComment(req, "Authorize payment for order 42")

The annotation is copied verbatim to the standard HAR entry.comment field and appears in the Inspector. Redirect hops inherit it through the request context. Comments are not redacted; never place credentials, tokens, personal data, or other secrets in them.

Body capture and storage

Captured bytes pass through decoding and streaming redaction once, then reach the configured BodyStore. Embedded and externally stored representations therefore obey the same rules. JSON, NDJSON, XML, form-urlencoded, and multipart built-ins preserve non-redacted bytes; malformed input fails closed around a matched sensitive value.

For managed disk storage:

storeConfig := recorder.DefaultFileBodyStoreConfig()
storeConfig.MaxBytes = 1 << 30
storeConfig.MaxFiles = 10_000

store, err := recorder.NewFileBodyStore("/var/spool/recorder-bodies", storeConfig)
if err != nil { return err }

config := recorder.DefaultConfig()
config.CaptureResponseBody = true
config.BodyStore = store
if err := config.Validate(); err != nil { return err }
transport := recorder.NewTransport(http.DefaultTransport, rec, config)

FileBodyStore publishes committed assets atomically, reconciles abandoned partials, enforces byte/file caps, and exposes Open, Release, Reconcile, and bounded statistics. The HAR entry and body asset are separate durability domains. FileBodyStoreConfig.MaintenanceMode opens an existing store without creating directories, changing permissions, recovering partials, or permitting new writers; it is intended for explicit Open, Release, and Reconcile maintenance. See DESIGN.md.

Use recorder verify --body-store ./spool capture.har to check referenced assets and hashes without mutation. Use recorder reconcile --body-store ./spool capture.har to preview unreferenced committed assets; deletion requires the command's explicit authoritative apply controls.

Custom redactors implement BodyRedactor. They identify sensitive values and delegate their representation to the supplied BodyValueProtector, so redact, encrypt, and tokenize modes behave identically for built-ins and extensions. Selected plaintext is streamed into a BodyValue, then FinishTo writes the protected bytes directly to the redactor output. Complete non-importable examples live under docs/examples.

Sensitive-value protection

Selected values support three modes: fixed redaction, AES-GCM encryption, and HMAC tokenization. Oversized values and protection failures fall back to [REDACTED]. JSON protection encrypts the raw JSON token, including quotes or container syntax, so exact reconstruction remains possible.

ProtectionKeyProvider and ProtectionKeyResolver are direct function types; no interface adapter is required. Providers receive the request context and are resolved at most once per protection mode and exchange; the request and response use the same immutable key snapshot. Tokens carry a key ID, and ProtectedTokenKeyID, DecryptProtectedValueWith, and VerifyProtectedTokenWith support archives containing values written before and after key rotation. Never record keys in HAR files or logs.

Sampling and retention

Policies are function types—no interface/adapter pair is required:

config.HeadSamplingPolicy = recorder.HeadSamplingPolicy(
	func(ctx context.Context, meta recorder.HeadSamplingMeta) recorder.HeadSamplingDecision {
		if meta.Path == "/health" { return recorder.HeadSampleDrop }
		return recorder.HeadSampleFull
	},
)

NewRateHeadSampler provides deterministic rate sampling. It prefers a key installed with WithSamplingKey, then the trace ID. Tail retention runs after capture and completion callbacks; discarded managed body assets are released.

Async recording

Multiple sinks can receive the same immutable entry without custom fan-out code:

fanout := recorder.NewMultiRecorder(durableSink, debugStream)

async, err := recorder.NewAsyncRecorder(fanout, recorder.DefaultAsyncRecorderConfig())

MultiRecorder calls every sink in argument order and joins failures without short-circuiting. It preserves batch delivery where a sink supports it and falls back to ordered Record calls otherwise. Fan-out is synchronous and does not close downstream sinks; wrapping it with AsyncRecorder keeps sink latency out of response finalization, while the application remains responsible for closing each owned resource. The first sink is a required argument; passing a nil sink is treated as a startup wiring bug and panics immediately.

asyncConfig := recorder.DefaultAsyncRecorderConfig()
asyncConfig.QueueCapacity = 1024
asyncConfig.BatchSize = 64
asyncConfig.FlushInterval = 10 * time.Millisecond

async, err := recorder.NewAsyncRecorder(sink, asyncConfig)
if err != nil { return err }

// At shutdown, wait for queued entries and surface downstream failures.
if err := async.Close(shutdownContext); err != nil { return err }

The default AsyncBlock policy preserves evidence but can delay exchange finalization behind a stalled sink. Internal sink and callback failures are logged by default with the same policy as Transport, and Close returns the first worker or downstream failure after draining. Record can report only a synchronous queue-state failure because downstream writes happen later. Drop policies and a bounded block timeout are explicit alternatives. Async recording is bounded but not crash-durable.

Evidence-backed HTTP fixtures

The optional hario and hartest packages turn recorder evidence into deterministic http.Client tests without expanding the core recording API:

document, err := hario.ReadHAR(file, hario.DefaultReadConfig())
if err != nil { return err }

fixture, err := hartest.NewTransport(document.Log.Entries, hartest.DefaultConfig())
if err != nil { return err }

client := &http.Client{Transport: fixture}

hario reads and validates bounded HAR or streaming NDJSON input. hartest strictly matches unused exchanges by method, URL/query, selected headers, and exact body and request trailers, then returns the captured response, response trailers, or recorded failure. One optional request normalizer handles real-world volatile IDs, timestamps, query parameters, and semantic JSON matching without replacing the safe matcher. It has no real-network fallback. Incomplete or unresolved request evidence fails closed; weakening body matching requires an explicit test configuration.

For large captures, hario.NewHARStream and NewNDJSONStream feed hartest.NewStreamTransport one validated entry at a time. Capture-order tests release consumed entries instead of retaining the complete fixture; Verify drains and validates the remaining source.

For shell-driven fixture preparation, recorder fixture selects reviewed exchanges into HAR or NDJSON. recorder serve-fixture exposes the same strict, network-free matching behavior through a loopback HTTP server for applications that cannot inject an http.RoundTripper.

Protected-value resolution is optional and source entries remain immutable. Inspector exports preserve the original protected representation by default, even after an operator resolves values in browser memory. A separate, dangerous resolved-export workflow can create a clearly named derived plaintext fixture only after showing a value-free sensitivity summary and requiring two explicit acknowledgements. The Inspector can export all entries, explicitly selected exchanges, or the current trace as HAR or NDJSON and reports external or incomplete body evidence before download.

See the complete hario and hartest contracts and the non-importable har-fixture example.

HAR extension contract

Recorder-specific entry data lives under one namespace:

{
  "_recorder": {
    "schemaVersion": "1",
    "traceId": "...",
    "state": "completed",
    "network": {},
    "tls": {},
    "requestBody": {},
    "responseBody": {},
    "redaction": {}
  }
}

Removing _recorder leaves plain HAR 1.2. The frozen v1 contract is published at schema/recorder-har-v1.schema.json. The Inspector rejects unknown recorder schema versions instead of guessing. Use recorder validate for a bounded structural check, or recorder doctor when schema compatibility and optional FileBodyStore health should be reported together.

Inspector and OpenTelemetry

The browser-only Inspector opens local HAR files and JSONStreamRecorder NDJSON, supports safe body previews, trace-chain waterfalls, replay commands, redaction audit, in-memory resolution of protected values, and fixture export as HAR or NDJSON. It is also deployable through GitHub Pages.

recorder inspect capture.har validates the local file, serves it from a temporary loopback endpoint, and opens the trusted Inspector without uploading the capture.

For local development, DebugStreamRecorder can publish finalized entries to one Inspector window over a bounded SSE stream. Its queue retains recent entries until an Inspector connects or reconnects and reports oldest-entry eviction as a visible gap; it is intentionally not a durable or production recorder. The application owns the loopback HTTP server:

liveConfig := recorder.DefaultDebugStreamRecorderConfig()
// Optional: trust an exact remotely hosted Inspector origin. This permits
// JavaScript from that origin to read the local stream.
liveConfig.AllowedOrigins = []string{"https://mgurevin.github.io"}

live, err := recorder.NewDebugStreamRecorder(liveConfig)
if err != nil { return err }

server := &http.Server{
	Addr:    "127.0.0.1:7070",
	Handler: live,
}
go func() {
	if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
		log.Printf("debug stream: %v", err)
	}
}()

recorderConfig := recorder.DefaultConfig()
if err := recorderConfig.Validate(); err != nil { return err }
client := &http.Client{
	Transport: recorder.NewTransport(http.DefaultTransport, live, recorderConfig),
}

Run the Inspector locally, choose live, and connect to http://127.0.0.1:7070. The application must shut down both the server and recorder. AsyncRecorder can isolate response finalization from live-view encoding, while MultiRecorder can send the same entries to an independent file or evidence sink. Their complete lifecycle and fan-out wiring are shown in the runnable debug-stream example.

By default only loopback browser origins may subscribe. AllowedOrigins accepts exact HTTP(S) origins—scheme, host, and non-default port—not URL paths or wildcards. It does not relax the loopback peer restriction. Add a hosted Inspector origin only when you trust every script served by that origin; captured entries may contain credentials and other sensitive data.

For example, an Inspector published at https://mgurevin.github.io/recorder/ sends the origin https://mgurevin.github.io. Configure the origin without /recorder/:

debugStreamConfig := recorder.DefaultDebugStreamRecorderConfig()
debugStreamConfig.QueueCapacity = 1_000
debugStreamConfig.AllowedOrigins = append(
	debugStreamConfig.AllowedOrigins,
	"https://mgurevin.github.io",
)

This configuration supports both the Pages-hosted Inspector and a local Inspector: loopback origins such as http://localhost:5173 remain allowed by default and do not need to be added. Keep the server bound to 127.0.0.1.

Safari note: Safari/WebKit may block an HTTPS Pages Inspector from connecting to an HTTP loopback stream as mixed content even when CORS is configured correctly. Use the local HTTP Inspector, or serve the loopback endpoint over HTTPS with a certificate trusted by the local machine.

The separate otelrecorder module exports bounded span events, metrics, async queue health, managed body-store health, and sampling outcomes. It never exports body content, raw URLs, key IDs, or error messages. Its full metric/unit and alerting guide is in otelrecorder/README.md.

Operational guidance

  • Consume or close every successful response body.
  • Keep body capture bounded; avoid embedding large production bodies.
  • Configure redaction before enabling capture and test representative payloads.
  • Treat HAR files and body assets as sensitive evidence.
  • Close and flush recorders during graceful shutdown.
  • Monitor async drops, blocked writers, store utilization, redaction failures, and fail-closed protection fallbacks.

Design details are in DESIGN.md, reproducible performance results in BENCHMARK.md, releases in CHANGELOG.md, and the release procedure in RELEASING.md. Report vulnerabilities privately as described in SECURITY.md.

Command-line tools

The optional recorder CLI manages captured evidence offline without adding dependencies to applications that only use the library:

Workflow Command
Validate, summarize, or convert bounded HAR/NDJSON input validate, summarize, convert
Open a local capture in the browser Inspector inspect
Check body references, hashes, schema, and local compatibility verify, doctor
Select reviewed exchanges or serve a network-free test fixture fixture, serve-fixture
Preview or explicitly apply FileBodyStore orphan cleanup reconcile
recorder validate capture.har
recorder summarize capture.har --json
recorder inspect capture.har
recorder fixture capture.har --method POST --host api.example.com \
  --output testdata/orders.ndjson

Commands use the bounded hario readers. They do not record traffic or replay requests to the real network; serve-fixture binds a local fixture server and reconcile is a dry run unless destructive application is explicitly confirmed. See the complete command and JSON-output contract.

Maintenance

The repository keeps release evidence and maintenance gates in GitHub Actions:

  • make check enforces pinned Go formatting/lint rules, race-enabled tests and vet across every Go module, Inspector lint/type/build/coverage checks, and benchmark smoke runs. CI also tests each module at its supported minimum Go version and at the current stable release.
  • make api-diff compares the root, hario, hartest, and otelrecorder public APIs with their latest release tags. Unreviewed incompatible changes fail CI; the README badge links to the repository-hosted report.
  • External wire-contract tests pin the _recorder JSON encoding and published schema identity/version. Internal AST guards preserve the intentionally small API design; apidiff handles compiler-visible compatibility.
  • make coverage-report applies component-specific thresholds, publishes the weighted Go/Inspector dashboard on GitHub Pages, and retains source-level reports as GitHub artifacts for 14 days. No coverage data leaves GitHub.
  • make vulncheck runs govulncheck for every Go module and audits Inspector runtime and build dependencies; high and critical npm advisories fail CI.
  • make sbom-check generates and validates separate SPDX JSON inventories for the standalone Recorder module, optional otelrecorder module, and Inspector. Recorder excludes documentation examples and the other artifacts; release assets are provenance-attested and are not committed.

License

MIT

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 one versioned _recorder extension. Removing that object 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()
config := recorder.DefaultConfig()
if err := config.Validate(); err != nil {
	log.Fatal(err)
}
client := &http.Client{
	Transport: recorder.NewTransport(http.DefaultTransport, rec, config),
}
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

Examples

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 (
	BodyRedactionRedacted  = "redacted"
	BodyRedactionUnchanged = "unchanged"
	BodyRedactionFailed    = "failed"
)
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 _recorder.state. Only terminal states (completed, failed, closed_early) ever appear in exported entries, because entries are emitted exclusively at finalization.

View Source
const DefaultMemoryRecorderCapacity = 1024

DefaultMemoryRecorderCapacity is the maximum number of finalized entries retained by NewMemoryRecorder.

View Source
const RecorderExtensionVersion = "1"

RecorderExtensionVersion is the frozen _recorder wire-schema version.

Variables

View Source
var ErrAsyncRecorderClosed = errors.New("recorder: async recorder is closed")

ErrAsyncRecorderClosed is returned when Record is called after shutdown has begun. The entry is rejected and reported to the configured drop handler.

View Source
var ErrDebugStreamRecorderClosed = errors.New("recorder: debug stream recorder is closed")

ErrDebugStreamRecorderClosed is returned when Record is called after the development stream has been closed.

Functions

func DecryptProtectedValue added in v0.2.0

func DecryptProtectedValue(token string, key ProtectionKey) ([]byte, error)

DecryptProtectedValue decrypts a REC-ENC-v1 token with the matching key. It is useful for trusted tooling; applications should avoid persisting the returned plaintext.

func DecryptProtectedValueWith added in v0.4.0

func DecryptProtectedValueWith(token string, resolver ProtectionKeyResolver) ([]byte, error)

DecryptProtectedValueWith resolves the key ID embedded in token and decrypts it. It is intended for trusted archive tooling spanning key rotations.

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 ProtectedTokenKeyID added in v0.4.0

func ProtectedTokenKeyID(token string) (string, error)

ProtectedTokenKeyID reports the non-secret key ID embedded in a supported protected token without decrypting or verifying its payload.

func RequestWithComment added in v0.4.1

func RequestWithComment(req *http.Request, comment string) *http.Request

RequestWithComment clones req with a context carrying a HAR entry comment. It returns nil when req is nil and never mutates the supplied request.

func RequestWithRedaction added in v0.3.0

func RequestWithRedaction(req *http.Request, config RedactionConfig) *http.Request

RequestWithRedaction clones req with a context carrying additional redaction rules. It returns nil when req is nil and never mutates the supplied request.

func SamplingKeyFromContext added in v0.4.0

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

SamplingKeyFromContext returns the key installed by WithSamplingKey.

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 VerifyProtectedToken added in v0.2.0

func VerifyProtectedToken(token string, value []byte, key ProtectionKey) (bool, error)

VerifyProtectedToken reports whether value produced a REC-TOK-v1 token.

func VerifyProtectedTokenWith added in v0.4.0

func VerifyProtectedTokenWith(token string, value []byte, resolver ProtectionKeyResolver) (bool, error)

VerifyProtectedTokenWith resolves the key ID embedded in token and verifies value. It is intended for trusted archive tooling spanning key rotations.

func WithRequestComment added in v0.4.1

func WithRequestComment(ctx context.Context, comment string) context.Context

WithRequestComment returns a context carrying a caller-supplied annotation for each physical HTTP exchange started under that context. Recorder stores the value verbatim in the standard HAR entry.comment field; callers must not include secrets or other data that should be redacted.

Redirect requests inherit the comment through their context. A later call replaces the earlier value, and an empty comment omits the HAR field.

func WithRequestRedaction added in v0.3.0

func WithRequestRedaction(ctx context.Context, config RedactionConfig) context.Context

WithRequestRedaction returns a context carrying additional redaction rules. Repeated calls merge rules; for duplicate body-redactor MIME registrations, the most recent call wins. Input slices and maps are copied before storage.

func WithSamplingKey added in v0.4.0

func WithSamplingKey(ctx context.Context, key string) context.Context

WithSamplingKey returns a context carrying a stable application sampling key. Rate policies prefer it over TraceID, preserving decisions across related requests even when no recorder trace context is installed.

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 _recorder.traceId and an incrementing redirectIndex, which links redirect chains followed by http.Client into one logical trace.

Types

type AsyncBackpressurePolicy added in v0.4.0

type AsyncBackpressurePolicy uint8

AsyncBackpressurePolicy controls what AsyncRecorder does when its bounded queue is full.

const (
	// AsyncBlock preserves entries by waiting for queue capacity. This is the
	// default. A stalled sink can therefore delay response-body Read or Close
	// calls that finalize HTTP exchanges.
	AsyncBlock AsyncBackpressurePolicy = iota

	// AsyncDropNewest preserves the already accepted FIFO prefix and drops the
	// entry currently being recorded when the queue is full.
	AsyncDropNewest

	// AsyncDropOldest removes the oldest queued (not in-flight) entry to make
	// room for the entry currently being recorded.
	AsyncDropOldest
)

type AsyncDropHandler added in v0.4.0

type AsyncDropHandler func(*Entry, AsyncDropReason) error

AsyncDropHandler observes an entry that AsyncRecorder discarded. It runs after the queue lock is released and must be concurrency-safe and bounded. Returned errors are reported through AsyncRecorder's internal-error policy and returned by Close.

type AsyncDropReason added in v0.4.0

type AsyncDropReason string

AsyncDropReason identifies the bounded reason an entry was not delivered.

const (
	// AsyncDropPolicyNewest means the configured full-queue policy rejected
	// the entry being recorded.
	AsyncDropPolicyNewest AsyncDropReason = "policy_newest"
	// AsyncDropPolicyOldest means the configured full-queue policy evicted the
	// oldest queued entry.
	AsyncDropPolicyOldest AsyncDropReason = "policy_oldest"
	// AsyncDropTimeoutNewest means a bounded wait expired and rejected the
	// entry being recorded.
	AsyncDropTimeoutNewest AsyncDropReason = "timeout_newest"
	// AsyncDropTimeoutOldest means a bounded wait expired and evicted the
	// oldest queued entry.
	AsyncDropTimeoutOldest AsyncDropReason = "timeout_oldest"
	// AsyncDropClosed means shutdown rejected an entry.
	AsyncDropClosed AsyncDropReason = "closed"
)

type AsyncRecorder added in v0.4.0

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

AsyncRecorder decouples entry finalization from a downstream Recorder with one bounded FIFO queue and one worker. It preserves accepted-entry order but does not provide crash durability, retries, or proof of persistence.

func NewAsyncRecorder added in v0.4.0

func NewAsyncRecorder(sink Recorder, config AsyncRecorderConfig) (*AsyncRecorder, error)

NewAsyncRecorder wraps sink with a bounded asynchronous queue. Config must specify a positive queue capacity and batch size; DefaultAsyncRecorderConfig supplies the evidence-preserving baseline of 1,024 entries, batches of one, and AsyncBlock. Blocking prevents queue-overflow loss during normal process operation, but a slow or stalled sink can delay application goroutines finalizing HTTP exchanges.

Example
package main

import (
	"context"
	"fmt"

	"github.com/mgurevin/recorder"
)

func main() {
	sink := recorder.NewMemoryRecorder()

	async, err := recorder.NewAsyncRecorder(sink, recorder.DefaultAsyncRecorderConfig())
	if err != nil {
		panic(err)
	}

	if err := async.Record(&recorder.Entry{}); err != nil {
		panic(err)
	}

	if err := async.Close(context.Background()); err != nil {
		panic(err)
	}

	fmt.Println(sink.Len())
}
Output:
1

func (*AsyncRecorder) Close added in v0.4.0

func (r *AsyncRecorder) Close(ctx context.Context) error

Close stops accepting entries and waits for the queue and active downstream call to drain. If ctx expires, draining continues in the background and a later Close call may wait again. Active Recorder.Record calls cannot be cancelled because Recorder intentionally has no context-aware method.

func (*AsyncRecorder) Record added in v0.4.0

func (r *AsyncRecorder) Record(entry *Entry) error

Record implements Recorder. With AsyncBlock it may wait for queue capacity; dropping policies return immediately when the queue is full. Calls made after Close begins return ErrAsyncRecorderClosed and are counted as DroppedClosed. Configured drop policies are intentional outcomes and return nil while remaining observable through Stats and DropHandler.

func (*AsyncRecorder) Stats added in v0.4.0

func (r *AsyncRecorder) Stats() AsyncRecorderStats

Stats returns a concurrency-safe point-in-time snapshot.

type AsyncRecorderConfig added in v0.4.0

type AsyncRecorderConfig struct {
	// QueueCapacity is the maximum number of accepted entries awaiting delivery.
	QueueCapacity int
	// Backpressure controls behavior when QueueCapacity is exhausted.
	Backpressure AsyncBackpressurePolicy
	// CloseSink transfers io.Closer ownership of the downstream sink.
	CloseSink bool
	// InternalErrorMode selects the internal error policy. See the constants.
	InternalErrorMode InternalErrorMode
	// OnInternalError observes contained sink, callback, and close failures.
	OnInternalError func(error)
	// Logf is used by InternalErrorLog. Nil falls back to log.Printf.
	Logf func(format string, args ...any)
	// BatchSize is the maximum entries passed to RecordBatch; one disables batching.
	BatchSize int
	// FlushInterval bounds how long a partial batch waits; zero flushes immediately.
	// A positive value requires BatchSize above one and a batch-capable sink.
	FlushInterval time.Duration
	// BlockTimeout bounds AsyncBlock; zero waits without a deadline. It is valid
	// only with Backpressure set to AsyncBlock.
	BlockTimeout time.Duration
	// BlockTimeoutPolicy is the drop policy used after BlockTimeout.
	BlockTimeoutPolicy AsyncBackpressurePolicy
	// DropHandler observes entries discarded by policy, timeout, or close.
	DropHandler AsyncDropHandler
}

AsyncRecorderConfig defines bounded queue, batching, backpressure, callback, and downstream ownership behavior for NewAsyncRecorder.

func DefaultAsyncRecorderConfig added in v0.4.0

func DefaultAsyncRecorderConfig() AsyncRecorderConfig

DefaultAsyncRecorderConfig returns the evidence-preserving production baseline, including visible internal-error logging.

type AsyncRecorderStats added in v0.4.0

type AsyncRecorderStats struct {
	Capacity int

	Accepted         uint64
	Processed        uint64
	BatchesProcessed uint64
	MaxBatchSize     int

	BlockedRecords   uint64
	CurrentlyBlocked int
	TotalBlockTime   time.Duration
	MaxBlockTime     time.Duration
	OldestBlockAge   time.Duration

	DroppedNewest        uint64
	DroppedOldest        uint64
	DroppedTimeoutNewest uint64
	DroppedTimeoutOldest uint64
	DroppedClosed        uint64
	DropHandlerErrors    uint64
	DropHandlerPanics    uint64

	SinkPanics uint64
	SinkErrors uint64

	Pending  int
	InFlight int
}

AsyncRecorderStats is a point-in-time snapshot of queue and sink activity. Processed means the downstream Record call was attempted; the minimal Recorder interface cannot prove that a sink durably persisted an entry.

type BodyCaptureDecision added in v0.2.0

type BodyCaptureDecision struct {
	Capture          bool
	Embed            bool
	Hash             bool
	MaxBodyBytes     int64
	RedactorOverride BodyRedactor
}

BodyCaptureDecision controls capture for one request or response body. RedactorOverride, when non-nil, replaces the redactor selected by the Transport and request-scoped RedactionConfig for this body only. Nil keeps the existing selection; it does not disable redaction.

type BodyCaptureMeta added in v0.2.0

type BodyCaptureMeta struct {
	Direction       BodyDirection
	Method          string
	Scheme          string
	Host            string
	Path            string
	StatusCode      int
	ContentType     string
	ContentEncoding string
	ContentLength   int64
	TraceID         string
	RedirectIndex   int
}

BodyCaptureMeta is the immutable, non-body metadata supplied to a BodyCapturePolicy. It excludes query values, headers, and cookies. StatusCode is zero for request decisions; Path is URL-escaped.

type BodyCapturePolicy added in v0.2.0

BodyCapturePolicy decides how one body is recorded. The defaults argument reflects the transport's capture Config; RedactorOverride starts nil because RedactionConfig selection remains active unless explicitly overridden. Implementations may be called concurrently and must not retain or mutate HTTP objects.

type BodyDirection added in v0.2.0

type BodyDirection string

BodyDirection identifies which side of an exchange a capture decision applies to.

const (
	// RequestBody identifies the caller-provided request stream.
	RequestBody BodyDirection = "request"
	// ResponseBody identifies the response stream returned to the caller.
	ResponseBody BodyDirection = "response"
)

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 opaque external reference when the BodyStore keeps content
	// out of memory. The referenced bytes are the captured
	// representation, which may already be decoded and/or redacted.
	Store string `json:"store,omitempty"`
}

BodyInfo describes _recorder.requestBody or responseBody and records what happened to a body stream.

type BodyMetadata

type BodyMetadata struct {
	// ExchangeID correlates the body with its immutable entry.
	ExchangeID string
	// Direction is "request" or "response".
	Direction string
	// ContentType is the observed media type, including parameters.
	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 BodyRedactionInfo added in v0.2.0

type BodyRedactionInfo struct {
	Kind         string            `json:"kind"`
	Outcome      string            `json:"outcome"`
	Replacements *int64            `json:"replacements,omitempty"`
	Protection   *ProtectionCounts `json:"protection,omitempty"`
}

BodyRedactionInfo reports which body redactor ran and its outcome.

type BodyRedactor added in v0.2.0

type BodyRedactor interface {
	Redact(dst io.Writer, contentType string, protector BodyValueProtector) (io.WriteCloser, error)
}

BodyRedactor creates a streaming transform for one captured body. Redact may be called concurrently for different bodies; each returned writer is owned by one body and each input byte passes through it once. Close must flush parser state but must not close dst.

type BodyStore

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

BodyStore creates BodyWriter instances. Implementations receive the capture pipeline's processed representation, while BodyInfo counters and hashes continue to describe the original caller/wire stream. Implementations must be safe for concurrent use.

type BodyValue added in v0.3.0

type BodyValue interface {
	io.Writer
	FinishTo(io.Writer) error
}

BodyValue accepts one selected plaintext value in chunks. FinishTo writes its protected representation and records exactly one replacement. Encryption buffers only up to the configured value limit, tokenization streams through HMAC, and failures or oversized values fail closed to [REDACTED]. FinishTo may be repeated to write the same result, but a BodyValue must not receive more plaintext after its first FinishTo call.

type BodyValueProtector added in v0.3.0

type BodyValueProtector interface {
	NewValue() BodyValue
}

BodyValueProtector creates bounded streaming values that apply the transport's configured sensitive-value mode and token format. It is scoped to one body writer and must not be retained after that writer closes.

type BodyWriter

type BodyWriter interface {
	io.Writer
	// Commit finalizes and publishes the captured representation. It is
	// idempotent; Ref must remain empty until Commit succeeds.
	Commit() error
	// Abort closes the writer and discards its captured representation. It is
	// idempotent and must not publish a Ref.
	Abort() error
	// Bytes returns the bytes captured so far (used when embedding content
	// into the HAR document).
	Bytes() ([]byte, error)
	// Ref returns an opaque external reference to the stored content, or ""
	// when the content lives inline in memory.
	Ref() string
}

BodyWriter receives the captured representation of one body stream. When configured, the capture pipeline may decode and/or redact bytes before Write; it never requires a store to rewrite already-persisted content. 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 Config added in v0.4.0

type Config 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
	// (_recorder.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; set EmbedBodies to enable it.
	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 _recorder.tls built from tls.ConnectionState.
	CaptureTLS bool
	// CaptureCertificates includes peer certificate details in _recorder.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

	// Redaction contains global common and direction-specific selector rules
	// plus trusted custom body redactors. Assign the baseline directly;
	// request contexts may add more through WithRequestRedaction.
	Redaction RedactionConfig

	// SensitiveValueProtection controls whether values selected by configured
	// redaction rules and body redactors are removed, reversibly encrypted, or
	// deterministically tokenized. The zero value redacts with [REDACTED].
	// Protection failures always fail closed to [REDACTED].
	SensitiveValueProtection SensitiveValueProtection

	// 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". Validate rejects unknown values; unchecked configurations retain
	// the fail-safe sha256 runtime fallback.
	BodyHashAlgorithm string

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

	// ContentDecoders maps Content-Encoding tokens (lower-case) to decoders
	// used by the capture and HAR-building pipeline. With body redaction
	// configured, supported encodings are decoded and redacted
	// while streaming into the BodyStore; otherwise a fully captured body
	// may be decoded when embedded in the HAR. DefaultConfig installs the
	// stdlib-only set (gzip, x-gzip, deflate); add entries to the map for
	// encodings such as brotli or zstd — see ContentDecoder for
	// ready-to-paste recipes. Decoding never touches bytes read by the caller.
	ContentDecoders map[string]ContentDecoder

	// BodyCapturePolicy optionally overrides capture, embedding, hashing,
	// limits, and body-redactor selection for each request and response body.
	// Policy errors and panics fail closed to metadata-only recording.
	BodyCapturePolicy BodyCapturePolicy

	// HeadSamplingPolicy decides before exchange instrumentation whether to
	// record fully, retain metadata only, or bypass recording entirely.
	HeadSamplingPolicy HeadSamplingPolicy
	// RetentionPolicy decides whether a finalized entry reaches Recorder. The
	// completion callback still runs first; capture cost has already been paid.
	RetentionPolicy RetentionPolicy

	// BodyStore provides transactional storage for captured body bytes. Writers
	// commit on body finalization and abort on retry or processing/storage
	// failure. 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 instrumented exchange is finalized, before
	// retention and Recorder.Record. The entry and body assets are borrowed for
	// the callback duration; retaining ownership requires a Recorder.
	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
}

Config configures a Transport. The zero value disables every optional content/metadata capture; finalized entries still contain the core exchange lifecycle and body byte/completion accounting. Pass DefaultConfig explicitly to NewTransport for sensible production defaults.

Config is copied by NewTransport. Maps, callbacks, policies, stores and other referenced values must be treated as immutable after construction.

Example (Redaction)
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/mgurevin/recorder"
)

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		_, _ = io.WriteString(writer, `{"secret":"value","keep":1}`)
	}))
	defer server.Close()

	records := recorder.NewMemoryRecorder()
	config := recorder.DefaultConfig()
	config.CaptureResponseBody = true
	config.EmbedBodies = true

	config.Redaction.Common.JSONFields = []string{"secret"}
	if err := config.Validate(); err != nil {
		panic(err)
	}

	client := &http.Client{
		Transport: recorder.NewTransport(http.DefaultTransport, records, config),
	}

	response, err := client.Get(server.URL)
	if err != nil {
		panic(err)
	}

	_, _ = io.Copy(io.Discard, response.Body)
	_ = response.Body.Close()

	fmt.Println(records.Entries()[0].Response.Content.Text)
}
Output:
{"secret":"[REDACTED]","keep":1}

func DefaultConfig added in v0.4.0

func DefaultConfig() Config

DefaultConfig returns the recommended production-safe configuration. 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.

func (Config) Validate added in v0.5.0

func (c Config) Validate() error

Validate reports static configuration errors without performing I/O or invoking callbacks, policies, stores, decoders, redactors, or key providers. Both Config{} and DefaultConfig() are valid. Call Validate after applying application settings and before passing the copied configuration to NewTransport.

Example
package main

import (
	"fmt"

	"github.com/mgurevin/recorder"
)

func main() {
	config := recorder.DefaultConfig()
	config.CaptureTLS = false

	fmt.Println(config.Validate())
}
Output:
recorder: Config.CaptureCertificates requires CaptureTLS

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"`
	// contains filtered or unexported fields
}

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. RecorderEntryExtension.ResponseBodyDecoded reports when Text and Size describe the decoded form rather than 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 by the recording pipeline. With body redaction enabled, a captured request or response carrying a registered Content-Encoding is decoded and redacted before bytes reach the BodyStore. Otherwise, a fully captured response may be decoded when embedded in the HAR. Decoded HAR response content is marked by _recorder.responseBodyDecoded while bodySize, the body hash and body counters keep describing the encoded bytes observed by the caller. The live request and caller-visible response bytes 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. A complete, independently pinned and tested example is under docs/examples/content-decoders:

import "github.com/andybalholm/brotli"

config.ContentDecoders["br"] = func(r io.Reader) (io.ReadCloser, error) {
	return io.NopCloser(brotli.NewReader(r)), nil
}

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

config.ContentDecoders["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 DebugStreamRecorder added in v0.4.2

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

DebugStreamRecorder publishes finalized entries as server-sent events to one local subscriber. It is intended only for live local development and debugging: its bounded queue retains recent entries while disconnected, drops the oldest queued entry when full, and is not a durable evidence sink.

DebugStreamRecorder is an http.Handler. The application owns the HTTP server and its lifecycle; ServeHTTP accepts only loopback peers. Browser origins are restricted to loopback unless explicitly configured.

func NewDebugStreamRecorder added in v0.4.2

func NewDebugStreamRecorder(config DebugStreamRecorderConfig) (*DebugStreamRecorder, error)

NewDebugStreamRecorder creates a bounded, single-subscriber development stream. Use DefaultDebugStreamRecorderConfig as the baseline.

func (*DebugStreamRecorder) Close added in v0.4.2

func (r *DebugStreamRecorder) Close() error

Close disconnects the active subscriber and rejects future records. It is safe to call more than once.

func (*DebugStreamRecorder) Record added in v0.4.2

func (r *DebugStreamRecorder) Record(entry *Entry) error

Record implements Recorder. Entries wait in the bounded queue until the subscriber receives them. When the queue is full, Record drops its oldest entry and reports that loss to the next subscriber as a gap event.

func (*DebugStreamRecorder) ServeHTTP added in v0.4.2

func (r *DebugStreamRecorder) ServeHTTP(w http.ResponseWriter, request *http.Request)

ServeHTTP streams entry and gap events. A second concurrent subscriber gets HTTP 409. Requests from non-loopback peers are always rejected. Browser origins must be loopback or explicitly listed in AllowedOrigins.

func (*DebugStreamRecorder) Stats added in v0.4.2

Stats returns a concurrency-safe snapshot.

type DebugStreamRecorderConfig added in v0.4.2

type DebugStreamRecorderConfig struct {
	QueueCapacity  int
	AllowedOrigins []string
}

DebugStreamRecorderConfig controls the bounded, single-subscriber development stream. QueueCapacity must be positive. AllowedOrigins adds exact HTTP(S) browser origins to the loopback origins accepted by default.

func DefaultDebugStreamRecorderConfig added in v0.4.2

func DefaultDebugStreamRecorderConfig() DebugStreamRecorderConfig

DefaultDebugStreamRecorderConfig returns conservative settings for local interactive inspection.

type DebugStreamRecorderStats added in v0.4.2

type DebugStreamRecorderStats struct {
	SubscriberActive bool
	Published        uint64
	Dropped          uint64
}

DebugStreamRecorderStats is a point-in-time view of the development stream.

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"`

	Recorder *RecorderEntryExtension `json:"_recorder,omitempty"`
	// contains filtered or unexported fields
}

Entry is a single HAR entry: one physical HTTP exchange. Recorder-specific data lives only in the versioned _recorder application extension; removing it 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 is one of the Phase* constants.
	Phase string `json:"phase"`
	// Type is the innermost observed Go error type.
	Type string `json:"type"`
	// Message is the configured redacted top-level error text.
	Message string `json:"message"`
	// Timeout and Temporary preserve matching error interface facts.
	Timeout   bool `json:"timeout"`
	Temporary bool `json:"temporary"`
	// ContextCanceled and ContextDeadlineExceeded describe the request context,
	// not merely an error that happens to match a context sentinel.
	ContextCanceled         bool `json:"contextCanceled"`
	ContextDeadlineExceeded bool `json:"contextDeadlineExceeded"`
	// Cause is the configured redacted context cause, when present.
	Cause string `json:"cause,omitempty"`
	// UnwrapChain lists bounded Go error type names from outermost to innermost.
	UnwrapChain []string `json:"unwrapChain,omitempty"`
}

ErrorInfo is the structured _recorder.error value 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 {
	// contains filtered or unexported fields
}

FileBodyStore spools processed body representations into a bounded managed directory. Writers use partial files and publish opaque references only after an atomic commit. Committed assets remain until explicitly released or removed by an authoritative reconciliation. A root must be owned by one process; FileBodyStore does not provide cross-process locking.

func NewFileBodyStore added in v0.4.0

func NewFileBodyStore(dir string, config FileBodyStoreConfig) (*FileBodyStore, error)

NewFileBodyStore opens a managed body store rooted at dir. Config must specify positive byte and file limits; DefaultFileBodyStoreConfig supplies the bounded baseline of 1 GiB, 10,000 files, and a one-hour partial TTL.

func (*FileBodyStore) NewWriter

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

NewWriter implements BodyStore.

func (*FileBodyStore) Open added in v0.4.0

func (s *FileBodyStore) Open(ref string) (io.ReadCloser, error)

Open opens a committed opaque body reference for reading.

func (*FileBodyStore) Reconcile added in v0.4.0

func (s *FileBodyStore) Reconcile(liveRefs []string, grace time.Duration, dryRun bool) (ReconcileResult, error)

Reconcile removes committed assets absent from the authoritative liveRefs set and older than grace. Dry-run reports without deleting.

func (*FileBodyStore) Release added in v0.4.0

func (s *FileBodyStore) Release(ref string) error

Release removes one committed body asset. Missing assets are treated as an idempotent success.

func (*FileBodyStore) ReleaseEntryAssets added in v0.4.0

func (s *FileBodyStore) ReleaseEntryAssets(entry *Entry) error

ReleaseEntryAssets releases the request and response body references owned by entry. Empty and duplicate references are ignored.

func (*FileBodyStore) Stats added in v0.4.0

func (s *FileBodyStore) Stats() FileBodyStoreStats

Stats returns an atomic point-in-time lifecycle and capacity snapshot.

type FileBodyStoreConfig added in v0.4.0

type FileBodyStoreConfig struct {
	// MaxBytes bounds committed and partial bytes owned by the store.
	MaxBytes int64
	// MaxFiles bounds committed and partial files owned by the store.
	MaxFiles int
	// PartialTTL controls startup cleanup of abandoned partial files. Zero
	// removes every partial during startup; negative values are invalid.
	PartialTTL time.Duration
	// SyncOnCommit fsyncs the file and containing directory during publish. It
	// does not make the separately recorded Entry crash-durable.
	SyncOnCommit bool
	// MaintenanceMode opens an existing store without creating directories,
	// changing permissions, or recovering partial files. NewWriter is disabled;
	// Open, Release, Reconcile, and Stats remain available. This makes dry-run
	// reconciliation observational while preserving the normal constructor's
	// path and reference validation.
	MaintenanceMode bool
}

FileBodyStoreConfig defines managed disk capacity and commit durability.

func DefaultFileBodyStoreConfig added in v0.4.0

func DefaultFileBodyStoreConfig() FileBodyStoreConfig

DefaultFileBodyStoreConfig returns bounded managed-store defaults.

type FileBodyStoreStats added in v0.4.0

type FileBodyStoreStats struct {
	MaxBytes          int64
	MaxFiles          int
	PartialBytes      int64
	CommittedBytes    int64
	PartialFiles      int
	CommittedFiles    int
	CommittedTotal    uint64
	AbortedTotal      uint64
	ReleasedTotal     uint64
	QuotaRejected     uint64
	RecoveredPartials uint64
	WriteFailures     uint64
	CommitFailures    uint64
	AbortFailures     uint64
	ReleaseFailures   uint64
}

FileBodyStoreStats is an atomic point-in-time lifecycle and capacity snapshot. Totals are process-lifetime counters except RecoveredPartials, which includes startup recovery.

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) error

Record implements Recorder.

func (*HARFileRecorder) RecordBatch added in v0.4.0

func (r *HARFileRecorder) RecordBatch(entries []*Entry) error

RecordBatch processes a batch with one lock acquisition.

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 HeadSamplingDecision added in v0.4.0

type HeadSamplingDecision uint8

HeadSamplingDecision selects how much work the Transport performs for an exchange. The zero value preserves the configured full-recording behavior.

const (
	// HeadSampleFull applies the configured capture, hashing, trace, and
	// redaction behavior.
	HeadSampleFull HeadSamplingDecision = iota
	// HeadSampleMetadataOnly records exchange lifecycle and core metadata
	// without body content, hashing, certificates, or raw trace events.
	HeadSampleMetadataOnly
	// HeadSampleDrop bypasses recorder instrumentation for this exchange.
	HeadSampleDrop
)

type HeadSamplingMeta added in v0.4.0

type HeadSamplingMeta struct {
	Method        string
	Scheme        string
	Host          string
	Path          string
	MIMEType      string
	ContentLength int64
	SamplingKey   string
	TraceID       string
	RedirectIndex int
}

HeadSamplingMeta is the bounded, non-secret request metadata available before exchange instrumentation is constructed. MIMEType excludes parameters.

type HeadSamplingPolicy added in v0.4.0

type HeadSamplingPolicy func(context.Context, HeadSamplingMeta) HeadSamplingDecision

HeadSamplingPolicy decides whether an exchange is fully recorded, reduced to metadata, or passed directly to the wrapped transport. Implementations must be fast, side-effect free, and safe for concurrent use.

func NewRateHeadSampler added in v0.4.0

func NewRateHeadSampler(fraction float64, sampled, unsampled HeadSamplingDecision) (HeadSamplingPolicy, error)

NewRateHeadSampler creates a deterministic rate policy. SamplingKey is used first, then TraceID. Without either, each physical exchange uses crypto/rand and redirect-chain consistency is not guaranteed.

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 are reported by a Transport or AsyncRecorder. Protection failures are aggregated per exchange direction to avoid log storms. Internal errors never replace the wrapped HTTP result.

const (
	// InternalErrorIgnore does not log internal errors. A configured
	// OnInternalError callback still receives them. This is the default.
	InternalErrorIgnore InternalErrorMode = iota

	// InternalErrorLog behaves like InternalErrorIgnore but additionally writes
	// the error using 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 _recorder.traceId, 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) Record

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

Record implements Recorder and reports encoding or write failures directly.

func (*JSONStreamRecorder) RecordBatch added in v0.4.0

func (r *JSONStreamRecorder) RecordBatch(entries []*Entry) error

RecordBatch encodes independent NDJSON documents into one temporary buffer and issues one downstream Write.

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 retaining the newest DefaultMemoryRecorderCapacity finalized entries. Once full, recording a new entry evicts the oldest retained entry.

func NewMemoryRecorderWithCapacity added in v0.4.0

func NewMemoryRecorderWithCapacity(capacity int) (*MemoryRecorder, error)

NewMemoryRecorderWithCapacity returns an empty in-memory recorder retaining the newest capacity finalized entries. Capacity must be positive.

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) error

Record implements Recorder.

func (*MemoryRecorder) RecordBatch added in v0.4.0

func (r *MemoryRecorder) RecordBatch(entries []*Entry) error

RecordBatch implements batchRecorder with one lock acquisition.

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) Snapshot added in v0.4.0

func (r *MemoryRecorder) Snapshot() ([]*Entry, MemoryRecorderStats)

Snapshot atomically returns the retained entries in recording order and the corresponding retention statistics.

func (*MemoryRecorder) Stats added in v0.4.0

Stats returns an atomic point-in-time snapshot of retention state.

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 MemoryRecorderStats added in v0.4.0

type MemoryRecorderStats struct {
	Capacity int
	Retained int
	Evicted  uint64
}

MemoryRecorderStats is an atomic point-in-time snapshot of retention state. Evicted is a lifetime counter and is not cleared by Reset.

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 _recorder.network value: 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 before retention and Recorder delivery. The entry and external body assets are borrowed only for the callback duration.

type PostData

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

	Comment string `json:"comment,omitempty"`
	// contains filtered or unexported fields
}

PostData is the standard HAR postData record. Binary request-body encoding is reported by RecorderEntryExtension.RequestBodyEncoding.

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 ProtectionCounts added in v0.2.0

type ProtectionCounts struct {
	Redacted  int64            `json:"redacted,omitempty"`
	Encrypted int64            `json:"encrypted,omitempty"`
	Tokenized int64            `json:"tokenized,omitempty"`
	Fallbacks map[string]int64 `json:"fallbacks,omitempty"`
}

ProtectionCounts summarizes representation modes and fail-closed causes. Fallback keys are fixed recorder-defined codes and never contain errors, field names, key IDs, tokens, or original values.

type ProtectionKey added in v0.2.0

type ProtectionKey struct {
	ID  string
	Key []byte
}

ProtectionKey is the active key used for a recorded sensitive value. ID is embedded in protected tokens to support rotation; it must not itself be sensitive. Encryption requires exactly 32 key bytes. Tokenization accepts 32 or more bytes.

type ProtectionKeyProvider added in v0.2.0

type ProtectionKeyProvider func(context.Context, ProtectionMode) (ProtectionKey, error)

ProtectionKeyProvider returns the active key for the request context and requested mode. Recorder resolves each mode at most once per exchange, and the request and response share that immutable key snapshot. The function may be called concurrently across exchanges and must not return key material that callers mutate.

type ProtectionKeyResolver added in v0.4.0

type ProtectionKeyResolver func(keyID string) (ProtectionKey, error)

ProtectionKeyResolver resolves historical key material by the non-secret ID embedded in a protected token. The function may be called concurrently and must not return key material that callers mutate.

type ProtectionMode added in v0.2.0

type ProtectionMode string

ProtectionMode controls how configured sensitive values are represented in recorded copies. The zero value preserves the traditional [REDACTED] behavior.

const (
	// ProtectionRedact replaces every selected value with [REDACTED].
	ProtectionRedact ProtectionMode = "redact"
	// ProtectionEncrypt emits a reversible, key-ID-bearing AES-256-GCM token.
	ProtectionEncrypt ProtectionMode = "encrypt"
	// ProtectionTokenize emits a deterministic, non-reversible HMAC token.
	ProtectionTokenize ProtectionMode = "tokenize"
)

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 ReconcileResult added in v0.4.0

type ReconcileResult struct {
	Scanned       int
	Released      int
	ReleasedBytes int64
}

ReconcileResult reports an authoritative live-reference reconciliation.

type Recorder

type Recorder interface {
	Record(entry *Entry) error
}

Recorder receives finalized, immutable entries. Implementations must be safe for concurrent use: entries arrive from whichever goroutine finishes reading a response body. Record reports synchronous sink failures; Transport contains and forwards them through its configured internal-error policy.

func NewMultiRecorder added in v0.4.2

func NewMultiRecorder(first Recorder, others ...Recorder) Recorder

NewMultiRecorder creates a synchronous fan-out Recorder. Each call is sent to every recorder in argument order, even when an earlier recorder fails. Multiple failures are returned as one errors.Join-compatible error.

Multi-recorder fan-out does not add synchronization, clone entries, recover panics, or own downstream lifecycle. Every supplied recorder must satisfy the normal concurrent-use and immutable-entry Recorder contract. Wrap the result in AsyncRecorder when fan-out must not run on response-finalization goroutines. The first recorder is mandatory; a nil recorder is a wiring error and causes NewMultiRecorder to panic.

type RecorderEntryExtension added in v0.4.0

type RecorderEntryExtension struct {
	SchemaVersion            string                  `json:"schemaVersion"`
	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"`
	Redaction                *RedactionInfo          `json:"redaction,omitempty"`
	RequestBodyEncoding      string                  `json:"requestBodyEncoding,omitempty"`
	ResponseBodyDecoded      bool                    `json:"responseBodyDecoded,omitempty"`
}

RecorderEntryExtension contains every recorder-specific HAR entry field.

type RecorderFunc

type RecorderFunc func(*Entry) error

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) error

Record implements Recorder.

type RedactionConfig added in v0.3.0

type RedactionConfig struct {
	// Common applies to request and response recordings.
	Common RedactionRules
	// Request adds rules only to request recordings.
	Request RedactionRules
	// Response adds rules only to response recordings.
	Response RedactionRules
}

RedactionConfig contains common and direction-specific redaction rules. Common rules apply to both sides. The same type configures a Transport with Config.Redaction and an individual request with WithRequestRedaction.

type RedactionInfo added in v0.2.0

type RedactionInfo struct {
	Request  *RedactionScopeInfo `json:"request,omitempty"`
	Response *RedactionScopeInfo `json:"response,omitempty"`
	Errors   int64               `json:"errors,omitempty"`
	RawTrace int64               `json:"rawTrace,omitempty"`
}

RedactionInfo summarizes actual redaction work without exposing rule names or original values. Counts describe values changed in the recorded copy.

type RedactionRules added in v0.3.0

type RedactionRules struct {
	// Headers lists header names whose values are protected. A cookie is also
	// protected when its Cookie or Set-Cookie carrier header is selected.
	Headers []string
	// QueryParameters protects URL/query-string values, URL-encoded form
	// fields, and matching multipart field/file payloads and filenames.
	QueryParameters []string
	// Cookies lists parsed cookie names whose values are protected.
	Cookies []string
	// JSONFields recursively protects matching JSON object-field values while
	// preserving unaffected source bytes.
	JSONFields []string
	// XMLElements protects matching XML element subtrees by local name;
	// namespace prefixes are ignored and attributes are not selected.
	XMLElements []string
	// BodyRedactors registers trusted streaming redactors by exact normalized
	// base MIME type. A later merged registration wins for the same type.
	BodyRedactors map[string]BodyRedactor
}

RedactionRules adds field selectors and body redactors for one side of an HTTP exchange. Names are matched case-insensitively. Configurations merge additively, so request-scoped selectors cannot remove Transport selectors. A BodyRedactors entry is an explicit trusted override for its MIME type.

type RedactionScopeInfo added in v0.2.0

type RedactionScopeInfo struct {
	URL             int64              `json:"url,omitempty"`
	Headers         int64              `json:"headers,omitempty"`
	QueryParameters int64              `json:"queryParameters,omitempty"`
	Cookies         int64              `json:"cookies,omitempty"`
	Body            *BodyRedactionInfo `json:"body,omitempty"`
	Protection      *ProtectionCounts  `json:"protection,omitempty"`
}

RedactionScopeInfo summarizes one side of an exchange.

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 _recorder.error carries the failure detail.

type RetentionDecision added in v0.4.0

type RetentionDecision uint8

RetentionDecision controls whether a finalized entry reaches the Recorder.

const (
	// RetainEntry delivers the finalized entry to the configured Recorder.
	RetainEntry RetentionDecision = iota
	// DiscardEntry omits the finalized entry and releases its external assets.
	DiscardEntry
)

type RetentionPolicy added in v0.4.0

type RetentionPolicy func(context.Context, *Entry) RetentionDecision

RetentionPolicy runs after OnEntryCompleted and before Recorder.Record. Capture cost has already been paid. Implementations must be concurrency-safe.

type SamplingStats added in v0.4.0

type SamplingStats struct {
	HeadFull             uint64
	HeadMetadataOnly     uint64
	HeadDropped          uint64
	HeadPanics           uint64
	HeadInvalidDecisions uint64

	Retained                  uint64
	Discarded                 uint64
	RetentionPanics           uint64
	RetentionInvalidDecisions uint64
	AssetReleaseFailures      uint64
}

SamplingStats is an atomic snapshot of head and tail decisions.

type SensitiveValueProtection added in v0.2.0

type SensitiveValueProtection struct {
	// Mode selects redact, encrypt, or tokenize. The zero value redacts.
	Mode ProtectionMode
	// KeyProvider supplies encryption or tokenization key material.
	KeyProvider ProtectionKeyProvider
	// MaxValueBytes bounds one value buffered for encryption.
	MaxValueBytes int
}

SensitiveValueProtection configures the representation of every value selected by built-in or custom body redactors. MaxValueBytes bounds a single value buffered for encryption. Tokenization streams values through HMAC without retaining them. Values <= 0 select the safe 64 KiB default; values above 16 MiB are clamped. Failures and oversized values are replaced with [REDACTED].

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 _recorder.tls value 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 _recorder.trace when Config.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 {
	// contains filtered or unexported fields
}

Transport is an http.RoundTripper that records exchanges passing through it. A HeadSamplingPolicy may deliberately bypass recording for selected exchanges. Transport 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 immutable after construction. A zero-value Transport is not supported; construct one with NewTransport and an explicit Config. 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, recorder Recorder, config Config) *Transport

NewTransport builds an immutable Transport wrapping base. A nil base uses http.DefaultTransport. A nil recorder disables sink delivery while keeping OnEntryCompleted available.

Example
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/mgurevin/recorder"
)

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(writer, "ok")
	}))
	defer server.Close()

	records := recorder.NewMemoryRecorder()

	config := recorder.DefaultConfig()
	if err := config.Validate(); err != nil {
		panic(err)
	}

	client := &http.Client{
		Transport: recorder.NewTransport(http.DefaultTransport, records, config),
	}

	response, err := client.Get(server.URL)
	if err != nil {
		panic(err)
	}

	_, _ = io.Copy(io.Discard, response.Body)
	if err := response.Body.Close(); err != nil {
		panic(err)
	}

	entry := records.Entries()[0]
	fmt.Println(entry.Response.Status, entry.Recorder.State, entry.Recorder.ResponseBody.TotalBytes)
}
Output:
200 completed 2

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.

func (*Transport) SamplingStats added in v0.4.0

func (t *Transport) SamplingStats() SamplingStats

SamplingStats returns a concurrency-safe point-in-time sampling snapshot.

Directories

Path Synopsis
cmd
recorder command
Command recorder provides bounded offline validation, summaries, conversion, evidence verification, fixture preparation and serving, local Inspector handoff, compatibility diagnostics, and explicit FileBodyStore reconciliation for recorder HAR and NDJSON captures.
Command recorder provides bounded offline validation, summaries, conversion, evidence verification, fixture preparation and serving, local Inspector handoff, compatibility diagnostics, and explicit FileBodyStore reconciliation for recorder HAR and NDJSON captures.
docs
examples/csv-redactor command
Command csv-redactor contains a copy-oriented example of a streaming recorder.BodyRedactor for CSV request and response bodies.
Command csv-redactor contains a copy-oriented example of a streaming recorder.BodyRedactor for CSV request and response bodies.
examples/debug-stream command
Command debug-stream demonstrates ephemeral, single-browser live inspection during local development.
Command debug-stream demonstrates ephemeral, single-browser live inspection during local development.
examples/har-fixture command
Command har-fixture demonstrates an optional, network-free HTTP fixture backed by recorder HAR evidence.
Command har-fixture demonstrates an optional, network-free HTTP fixture backed by recorder HAR evidence.
Package hario reads and validates bounded HAR 1.2 documents and recorder NDJSON entry streams.
Package hario reads and validates bounded HAR 1.2 documents and recorder NDJSON entry streams.
Package hartest turns recorded HAR or NDJSON entries into deterministic, network-free HTTP client fixtures.
Package hartest turns recorded HAR or NDJSON entries into deterministic, network-free HTTP client fixtures.
otelrecorder module

Jump to

Keyboard shortcuts

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