recorder

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 40 Imported by: 0

README

recorder

CI Go Reference

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

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

Motivation

Reliable records of outbound HTTP exchanges are essential in systems where requests may need to be investigated, reconciled, or audited later. This is particularly important in financial workflows, where a successful response alone may not provide enough context to explain an operational incident or a disputed transaction.

Most HTTP recording tools focus on completed request/response pairs. In production, however, failures can occur at any stage of the exchange: DNS resolution, TCP connection, proxy negotiation, TLS handshake, request transmission, response streaming, or context cancellation. Diagnosing these failures requires transport-level timing and error information alongside the HTTP data.

recorder captures this complete client exchange lifecycle as a portable HAR 1.2 document while preserving the behavior of Go's net/http stack. It records only observable data, keeps body capture opt-in, and applies configurable redaction and sensitive-value protection to the recorded copy. The resulting artifact is suitable for debugging, incident analysis, reconciliation, and controlled audit workflows without turning the recorder itself into a new source of application failures.

Requirements

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

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

Quick start

package main

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

	"github.com/mgurevin/recorder"
)

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

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

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

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

What gets recorded

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

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

Security issues should be reported privately as described in SECURITY.md. Release history is maintained in CHANGELOG.md, and reproducible performance measurements and configuration guidance are documented in BENCHMARK.md.

Default configuration

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

Option field Default Notes
CaptureRequestBody false lifecycle and byte counts tracked; content capture is opt-in
CaptureResponseBody false lifecycle and byte counts tracked; content capture is opt-in
EmbedBodies false body text is not embedded by default
MaxRequestBodyBytes 1 MiB content capture limit; <= 0 means unlimited
MaxResponseBodyBytes 1 MiB content capture limit; <= 0 means unlimited
CaptureTLS true _tls extension enabled
CaptureCertificates true peer certificate metadata enabled
CaptureRawCertificates false raw DER not embedded by default
CaptureHeaders true headers and trailers recorded
CaptureCookies true parsed cookies recorded
Redaction.Common.Headers Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-API-Key case-insensitive; applies to requests and responses
other Redaction rules empty opt-in; Common applies to both directions, Request/Response add direction-specific rules
HashBodies false full-stream hashing is opt-in
BodyHashAlgorithm sha256 sha1/md5 supported; unknown values fall back to sha256
CaptureRawTrace false raw httptrace event list disabled by default
ContentDecoders gzip, x-gzip, deflate stdlib decoders for record-time decoding
BodyCapturePolicy nil optional per-request/per-response decision; failures are metadata-only
BodyStore MemoryBodyStore used when nil
InternalErrorMode InternalErrorIgnore reports through OnInternalError if set
OnInternalError nil optional callback for recorder-internal errors
Logf nil used by InternalErrorLog; nil falls back to the standard log package
OnEntryCompleted nil optional per-entry callback (context + entry)
RedactErrorMessage nil optional error message redactor

Also note:

  • WithOptions(Options{}) replaces the whole struct: zero-value options disable optional headers, cookies, TLS, body content, embedding, hashing and raw-trace capture. Core exchange fields plus body lifecycle, byte counts and completion state are still recorded.
  • Options must not be mutated after the transport served its first request.

Options reference

Option Effect
WithOptions(o) Replace the entire Options value; apply first when combining
WithCaptureRequestBody(v) Toggle request body content capture (size/state always tracked)
WithCaptureResponseBody(v) Toggle response body content capture
WithEmbedBodies(v) Off: keep sizes/hashes/store refs but embed no body text — the production setting with FileBodyStore
WithMaxRequestBodyBytes(n) Request capture limit; counting continues past it
WithMaxResponseBodyBytes(n) Response capture limit; also bounds record-time decoding
WithCaptureTLS(v) Toggle the _tls extension
WithCaptureCertificates(v, raw) Toggle certificate details; raw embeds Base64 DER
WithCaptureHeaders(v) Toggle header/trailer recording
WithCaptureCookies(v) Toggle parsed cookie recording
WithRedaction(config) Add common and direction-specific header, query, cookie, JSON, XML, and custom body-redactor rules
WithHashBodies(enabled, alg) Toggle body hashing / choose algorithm
WithBodyStore(s) Storage backend for captured bytes (MemoryBodyStore, FileBodyStore, custom)
WithCaptureRawTrace(v) Record every raw httptrace event under _trace
WithContentDecoder(enc, dec) Register a record-time decoder (e.g. brotli, zstd) for a Content-Encoding
WithBodyCapturePolicy(policy) Override capture, embed, hash, limit, or body redactor for each body
WithInternalErrorMode(m) Ignore (default) or Log; recorder failures never alter the HTTP result
WithOnInternalError(fn) Callback for recorder-internal errors
WithLogf(fn) Logger used by InternalErrorLog
WithOnEntryCompleted(fn) Per-entry completion callback — the integration hook (OTel adapter uses it)
WithErrorRedactor(fn) Filter applied to every recorded error message

WithRequestRedaction(ctx, config) and RequestWithRedaction(req, config) use the same RedactionConfig per call and add their rules to the immutable Transport configuration.

RedactionConfig has three scopes: Common applies to both recorded sides, while Request and Response add rules only to that direction. Each scope is a RedactionRules value containing Headers, QueryParameters, Cookies, JSONFields, XMLElements, and BodyRedactors. Repeated configurations merge additively; for one normalized MIME type, the most recently merged custom body redactor wins.

Production recipes

Full forensic capture

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

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

Hashing covers every streamed byte and can become the throughput ceiling on large bodies. See BENCHMARK.md and compare Benchmark100MBStreamingBody with Benchmark100MBStreamingBodyNoHash on the deployment hardware before disabling integrity metadata.

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

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

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

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

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

Body capture, storage, and hashing

Three independent concerns:

  • Counting always runs, even with capture off: totalBytes and the HAR size fields reflect the real stream, beyond any limit.
  • Capture writes content to the BodyStore until Max*BodyBytes is reached; past the limit only counting (and hashing) continues, and the record is marked truncated. MemoryBodyStore (default) buffers in memory; FileBodyStore spools to temp files so large bodies never live in memory — cleaning up its files is the caller's responsibility (paths are exposed via _requestBody/_responseBody.store).
  • The selected built-in or custom body redactor runs as a streaming transform before bytes reach the BodyStore. It does not write a raw body and overwrite it later. Each body selects one redactor, calls Redact once, passes each input byte through its returned writer once, and closes that writer once. Embedding reuses the already-redacted stored representation.
  • Embedding (EmbedBodies) decides whether captured content becomes postData.text / content.text in the HAR. With WithEmbedBodies(false) the document stays small while sizes, hashes, truncation state and the store reference remain.

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

Per-exchange capture policy

WithBodyCapturePolicy can override the global body options for each request and response independently. The callback receives immutable metadata and the decision derived from the transport's current Options:

policy := recorder.BodyCapturePolicyFunc(func(
	ctx context.Context,
	meta recorder.BodyCaptureMeta,
	decision recorder.BodyCaptureDecision,
) (recorder.BodyCaptureDecision, error) {
	if meta.Direction == recorder.ResponseBody && meta.StatusCode >= 500 {
		decision.Capture = true
		decision.Embed = false
		decision.Hash = true
		decision.MaxBodyBytes = 256 << 10
	}

	mediaType, _, _ := mime.ParseMediaType(meta.ContentType)
	if meta.Direction == recorder.ResponseBody && meta.StatusCode >= 500 &&
		mediaType == "application/problem+json" {
		decision.RedactorOverride = problemJSONRedactor
	}

	return decision, nil
})

transport := recorder.NewTransport(base, rec,
	recorder.WithBodyCapturePolicy(policy),
)

The request decision runs before the HTTP call and therefore has status code zero. The response decision runs after response headers arrive and can inspect the status and content metadata. Decisions are frozen per physical exchange, including redirect hops. Returning Capture=false also disables embedding and a per-body redactor override. RedactorOverride=nil preserves the redactor selected by the effective Transport/request-scoped RedactionConfig; a non-nil override replaces it for that single body while still using the library-owned protect/audit lifecycle. This is useful for runtime-only choices, such as applying a specialized redactor exclusively to 5xx application/problem+json responses. Policy errors and panics are reported via OnInternalError and fail closed to metadata-only recording without changing the live HTTP result. Policies may be called concurrently and must return quickly.

Redaction

  • Headers, query parameters, cookies are redacted by case-insensitive name; a cookie is also redacted when its carrier header (Cookie / Set-Cookie) is in the header list.
  • URL-encoded form redaction applies the query-parameter rules while application/x-www-form-urlencoded bodies stream into the BodyStore. Matching values become %5BREDACTED%5D; duplicate fields, ordering, key spelling, separators, and every unmatched byte remain unchanged. The same redacted representation backs postData.text and postData.params.
  • Multipart form redaction applies those rules to part names in explicit multipart/form-data bodies. Matching field/file payloads become [REDACTED]; matching files also have their filename metadata redacted. Unmatched parts and multipart framing remain byte-for-byte unchanged. Ambiguous headers, invalid boundaries, and unmatched nested multiparts stop store capture instead of falling back to raw bytes.
  • JSON field redaction recursively replaces matching object-field values while preserving every unredacted byte (including whitespace, key order, duplicate keys, number spelling, and escapes). UTF-8 BOM input and every document in application/x-ndjson are handled.
  • XML element redaction replaces the complete subtree inside matching elements (by local name, namespace prefixes ignored — "Password" covers <wsse:Password>), preserving the rest of the document byte-for-byte. XML attribute values are not redacted.
  • Inside a matched XML subtree, mismatched or prematurely closed tags keep suppression active; malformed markup cannot end redaction early.
  • A bounded prefix sniffer recognizes JSON/XML sent under a generic or incorrect content type such as text/plain.
Request-scoped redaction

A shared http.Client can add endpoint-specific redaction rules at the call site without rebuilding its Transport. RedactionConfig.Common applies to both directions; Request and Response add direction-specific rules. Name selectors are always added to the Transport configuration and cannot disable production-safe defaults:

req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
if err != nil {
	return err
}

req = recorder.RequestWithRedaction(req, recorder.RedactionConfig{
	Request: recorder.RedactionRules{
		Headers:         []string{"X-Customer-Token"},
		QueryParameters: []string{"account"},
		JSONFields:      []string{"cardNumber", "cvv"},
	},
	Response: recorder.RedactionRules{
		Headers:    []string{"X-Settlement-Token"},
		JSONFields: []string{"iban", "balance"},
	},
})

resp, err := client.Do(req)

WithRequestRedaction(ctx, config) is the context-only equivalent. Both helpers copy input slices and maps; the original request and rule values may be reused or modified afterward. Repeated calls merge their rules. Names remain case-insensitive and duplicate body-redactor MIME registrations use the most recent request-scoped registration.

BodyRedactors is the deliberate exception to additive selector behavior: a request-scoped registration replaces the global or built-in redactor selected for that MIME type. Treat request-scoped redactor implementations as trusted code and keep them concurrency-safe.

Rules follow the request context across redirects and apply to every physical exchange in that redirect chain. A CheckRedirect hook can attach different rules when a later hop needs separate treatment. The precedence order is:

global RedactionConfig
  < request-scoped RedactionConfig
  < BodyCaptureDecision.RedactorOverride for that body

The effective request and response redactors are frozen once per exchange, so one shared client safely handles concurrent calls with different rules. The request context should contain selector names and redactor implementations, never plaintext secrets or protection keys.

Sensitive-value protection modes

Built-in rules use [REDACTED] by default. They can instead encrypt values for authorized recovery or create deterministic, irreversible tokens for correlation:

keys := recorder.ProtectionKeyProviderFunc(func(mode recorder.ProtectionMode) (recorder.ProtectionKey, error) {
	switch mode {
	case recorder.ProtectionEncrypt:
		return recorder.ProtectionKey{ID: "enc-2026-07", Key: encryptionKeyFromKMS}, nil // exactly 32 bytes
	case recorder.ProtectionTokenize:
		return recorder.ProtectionKey{ID: "tok-2026-07", Key: tokenKeyFromKMS}, nil // at least 32 bytes
	default:
		return recorder.ProtectionKey{}, errors.New("unsupported protection mode")
	}
})

transport := recorder.NewTransport(base, rec,
	recorder.WithRedaction(recorder.RedactionConfig{Common: recorder.RedactionRules{
		JSONFields: []string{"password", "accountNumber"},
	}}),
	recorder.WithSensitiveValueProtection(recorder.SensitiveValueProtection{
		Mode:          recorder.ProtectionEncrypt,
		KeyProvider:   keys,
		MaxValueBytes: 64 << 10,
	}),
)

The modes are mutually exclusive for one transport:

  • ProtectionRedact writes [REDACTED] and requires no key.
  • ProtectionEncrypt writes REC-ENC-v1.<key-id>.<payload> using AES-256-GCM with a fresh 96-bit crypto/rand nonce for every value. The key ID is authenticated as additional data. A matched value is buffered only up to MaxValueBytes (64 KiB by default, hard-clamped to 16 MiB).
  • ProtectionTokenize writes REC-TOK-v1.<key-id>.<hmac> using HMAC-SHA-256. It streams the value through HMAC without buffering and enables equality correlation for values protected by the same key. It is not encryption and cannot recover the original value.

If a key provider fails, a key has an invalid length, random nonce generation fails, or an encrypted value exceeds its limit, that value becomes [REDACTED]. Raw plaintext is never used as a fallback. A non-positive limit selects the safe default; it never means unlimited.

Key-provider, key-validation, randomness, and cryptographic failures are also reported through OnInternalError and InternalErrorLog. To prevent a broken key service from producing one log per value, failures are aggregated to one report per request/response direction and exchange; the report includes the affected value count and wraps the first cause. Expected value_too_large policy fallbacks remain visible in _redaction but are not internal errors.

Protection covers values selected by the existing header, query, cookie, JSON, XML, URL-encoded form, and multipart rules. JSON encrypts the exact raw JSON value (including its quotes or container syntax); XML encrypts bytes inside the matched outer element; forms encrypt the original encoded value; multipart encrypts the part payload and protects a matching filename separately. All unmatched bytes retain the same byte-for-byte guarantees.

Use separate encryption and tokenization keys, obtain them from a KMS or secret manager, and rotate them by changing the non-secret key ID and active key material. Keep old decryption keys only as long as recorded data must be recoverable. Do not reuse protection keys for unrelated protocols. Because tokenization is deterministic, it reveals equality and is vulnerable to guessing when the input domain is small; use encryption or full redaction for low-entropy secrets.

DecryptProtectedValue and VerifyProtectedToken are provided for trusted server-side tooling. Never place plaintext or keys in HAR metadata, logs, URLs, command history, or persistent browser storage.

Custom body redactors

Implement BodyRedactor to add a streaming transform for another media type:

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

type BodyValueProtector interface {
	NewValue() BodyValue
}

type BodyValue interface {
	io.Writer
	Finish() string
}

transport := recorder.NewTransport(base, rec,
	recorder.WithRedaction(recorder.RedactionConfig{Common: recorder.RedactionRules{
		BodyRedactors: map[string]recorder.BodyRedactor{"text/csv": csvRedactor},
	}}),
)

Registration matches the normalized base MIME type exactly, ignoring case and parameters. A custom registration overrides the built-in handler for the same type, and the last registration wins. Redact may run concurrently for different bodies; each returned writer belongs to one body and must flush but not close dst. Constructor, write, close, panic, and short-write failures stop capture, are reported through OnInternalError, and never alter the live HTTP exchange. Custom redactors are trusted streaming components: keep their own buffers bounded and fail closed when input cannot be parsed safely.

For every selected plaintext value, create one BodyValue, stream its bytes through Write, then write the string returned by Finish to dst. Recorder centrally applies the configured redact, encrypt, or tokenize mode, enforces the maximum protected-value size, emits versioned tokens, handles key failures fail-closed, and records the replacement and protection audit counts. Redaction retains no plaintext, encryption buffers only up to the configured value limit, and tokenization streams through HMAC. The protector and its values are scoped to one body writer; finish each value exactly once and do not retain either after the writer closes. See the complete streaming CSV example.

Redaction audit metadata

Entries include _redaction when a recorded value was changed or a body redactor ran. It summarizes request/response URL, header, query, cookie, and body work, plus changed error and raw-trace messages. The central protector reports redacted and its replacement/protection counts whenever a built-in or custom redactor finishes at least one value. Any successful redactor that finishes no protected values reports unchanged; parser or writer failures report failed. Protection summaries additionally count redacted, encrypted, and tokenized outcomes and fixed fail-closed reason codes such as value_too_large. They never contain key IDs, tokens, rule names, plaintext, or underlying error messages.

The extension deliberately excludes configured field/header/cookie names, original values, concrete Go type names, and error text. Absence of _redaction means no audit event was observed; it does not prove that an older HAR was produced without redaction because earlier versions did not emit this extension.

Rules that hold everywhere:

  • Redaction applies only to the recorded copy — the live HTTP request and response are never modified.
  • Streaming parsers cap JSON/form keys, XML tags, and multipart part headers at 64 KiB; JSON/XML nesting is capped at 1024 and MIME sniffing at 4 KiB. Limit violations stop store capture rather than falling back to unredacted bytes.
  • Because a streaming sink cannot roll back committed output, a matched field stays redacted even if later input proves malformed or incomplete.
  • Malformed syntax that appears before a field/element can prevent the parser from recognizing that later match; do not rely on body redaction as an input-validation mechanism.
  • Body hashes are computed over the real wire/caller bytes, never over redacted bytes.
  • Error messages can carry secrets too: WithErrorRedactor filters every recorded error string.

Failures and error classification

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

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

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

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

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

Timings and observability limits

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

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

Compression and content decoding

  1. Transparent gzip — when you don't set Accept-Encoding, http.Transport negotiates and decompresses gzip itself. The record stores decoded bytes (_decoded: true), bodySize is -1.
  2. Record-time decoding — when a request or response has an explicit Content-Encoding and a decoder is registered, body redaction streams the decoded form into the BodyStore. Request and response hashes/counters still describe the encoded bytes that flowed; response bodySize keeps the wire view.

gzip, x-gzip and deflate (zlib-wrapped or raw, sniffed like browsers) ship by default using only the standard library. Brotli/zstd are deliberately not bundled so the core module remains dependency-free. A complete, pinned and tested implementation is available in docs/examples/content-decoders:

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

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

Safety rails: decoded output larger than the resolved directional body limit (MaxRequestBodyBytes or MaxResponseBodyBytes, unless policy overrides it) is refused, so a compression bomb cannot blow the capture budget. When body redaction is active, unknown/multi-step encodings and decoder failures stop store capture rather than falling back to raw bytes. Failures are reported through OnInternalError and never affect the live request or bytes received by the caller.

Recorder implementations

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

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

Trace correlation

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

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

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

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

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

HAR inspector

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

HAR Inspector screenshot

Open the live Inspector — HAR files selected from disk are parsed locally in your browser and are not uploaded.

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

Workflow:

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

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

OpenTelemetry export (otelrecorder)

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

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

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

With an active span in the request context, each exchange becomes a recorder.http.exchange span event. Metrics are always recorded: total and per-phase duration, streamed and captured body sizes, exchange failures, closed-early/truncation and capture outcomes, protection-mode value counts, fail-closed fallbacks, and body-redactor outcomes.

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

Development

Go formatting and linting use golangci-lint v2.11.4. gofumpt and goimports provide deterministic formatting; wsl_v5 enforces the project's blank-line grouping rules. Use make format to apply all safe Go and Inspector fixes across the repository, make lint for a non-mutating style check, and make check for the complete local pre-push verification. Individual targets are available when a faster feedback loop is useful:

make format          # apply formatting and safe lint fixes
make lint            # lint both Go modules and the Inspector
make test            # non-race Go tests plus Inspector tests
make test-race       # race-enabled tests for both Go modules
make vet             # vet both Go modules
make inspector-check # Inspector tests, type-check and production build
make benchmark-smoke # compile and run every benchmark once
make check           # complete non-mutating pre-push verification

The Inspector uses the stable TypeScript 7 native compiler for type-checking. ESLint consumes Microsoft's @typescript/typescript6 compatibility API because TypeScript 7.0 does not yet expose a stable programmatic API.

Architecture decisions, invariants and known limitations are documented in DESIGN.md. Benchmark methodology, current measurements, and performance-sensitive configuration guidance are in BENCHMARK.md.

Documentation

Overview

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

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

Usage:

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

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

Index

Constants

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

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

View Source
const (
	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 "_state". Only terminal states (completed, failed, closed_early) ever appear in exported entries, because entries are emitted exclusively at finalization.

Variables

This section is empty.

Functions

func 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 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 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 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 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 WithTraceID

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

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

Types

type 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

type BodyCapturePolicy interface {
	DecideBodyCapture(context.Context, BodyCaptureMeta, BodyCaptureDecision) (BodyCaptureDecision, error)
}

BodyCapturePolicy decides how one body is recorded. The defaults argument reflects the transport's capture Options; 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 BodyCapturePolicyFunc added in v0.2.0

BodyCapturePolicyFunc adapts a function to BodyCapturePolicy.

func (BodyCapturePolicyFunc) DecideBodyCapture added in v0.2.0

type BodyDirection added in v0.2.0

type BodyDirection string

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

const (
	RequestBody  BodyDirection = "request"
	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 external reference (e.g. a file path) 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 is the "_requestBody" / "_responseBody" extension describing what happened to a body stream.

type BodyMetadata

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

BodyMetadata describes the body stream a BodyWriter is created for.

type 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
	Finish() string
}

BodyValue accepts one selected plaintext value in chunks. Finish returns 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]. A BodyValue must not be reused after Finish.

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
	Close() error
	// Bytes returns the bytes captured so far (used when embedding content
	// into the HAR document).
	Bytes() ([]byte, error)
	// Ref returns an external reference to the stored content (e.g. a file
	// path), or "" when the content lives inline in memory.
	Ref() string
}

BodyWriter receives 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 Content

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

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

type ContentDecoder

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

ContentDecoder turns a compressed body stream into its decoded form. It is used 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 "_decoded": true 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"

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

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

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

Cookie is the HAR cookie record.

type Creator

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

Creator identifies the producing application.

type Entry

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

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

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

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

func (*Entry) StartTime

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

StartTime returns the request start time used for ordering entries.

type ErrorInfo

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

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

type Expect100Info

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

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

type FileBodyStore

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

FileBodyStore spools captured bodies to temporary files so large bodies do not have to live in memory. Structured redaction is applied by the capture pipeline before bytes reach this store. Files are NOT deleted automatically; their paths are exposed via BodyInfo.Store and cleanup is the caller's responsibility.

func (FileBodyStore) NewWriter

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

NewWriter implements BodyStore.

type HAR

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

HAR is the top-level HAR 1.2 document.

func NewHAR

func NewHAR(entries []*Entry) *HAR

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

func (*HAR) Write

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

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

type HARFileRecorder

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

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

func NewHARFileRecorder

func NewHARFileRecorder(path string) *HARFileRecorder

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

func (*HARFileRecorder) Close

func (r *HARFileRecorder) Close() error

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

func (*HARFileRecorder) EntriesByTrace

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

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

func (*HARFileRecorder) Flush

func (r *HARFileRecorder) Flush() error

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

func (*HARFileRecorder) Record

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

Record implements Recorder.

func (*HARFileRecorder) RemoveTrace

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

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

func (*HARFileRecorder) TakeTrace

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

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

type InformationalResponse

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

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

type InternalErrorMode

type InternalErrorMode int

InternalErrorMode controls how recorder-internal failures (body store errors, protection/key failures, recorder panics) are reported. Protection failures are aggregated per exchange direction to avoid log storms. The wrapped HTTP call is never retried or altered by an internal error.

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

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

type JSONStreamRecorder

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

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

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

func NewJSONStreamRecorder

func NewJSONStreamRecorder(w io.Writer) *JSONStreamRecorder

NewJSONStreamRecorder creates a streaming recorder writing to w.

func (*JSONStreamRecorder) Err

func (r *JSONStreamRecorder) Err() error

Err returns the first encoding error encountered, if any.

func (*JSONStreamRecorder) Record

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

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

type Log

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

Log is the HAR 1.2 log object.

type MemoryBodyStore

type MemoryBodyStore struct{}

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

func (MemoryBodyStore) NewWriter

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

type MemoryRecorder

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

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

func NewMemoryRecorder

func NewMemoryRecorder() *MemoryRecorder

NewMemoryRecorder returns an empty in-memory recorder.

func (*MemoryRecorder) Entries

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

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

func (*MemoryRecorder) EntriesByTrace

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

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

func (*MemoryRecorder) HAR

func (r *MemoryRecorder) HAR() *HAR

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

func (*MemoryRecorder) HARForTrace

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

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

func (*MemoryRecorder) Len

func (r *MemoryRecorder) Len() int

Len returns the number of recorded entries.

func (*MemoryRecorder) Record

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

Record implements Recorder.

func (*MemoryRecorder) RemoveTrace

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

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

func (*MemoryRecorder) Reset

func (r *MemoryRecorder) Reset()

Reset discards all recorded entries.

func (*MemoryRecorder) TakeTrace

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

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

func (*MemoryRecorder) WriteHAR

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

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

type NameValuePair

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

NameValuePair is the HAR record for headers and query parameters.

type NetworkInfo

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

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

type OnEntryCompleted

type OnEntryCompleted func(context.Context, *Entry)

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

type Option

type Option func(*Options)

Option mutates Options during NewTransport.

func WithBodyCapturePolicy added in v0.2.0

func WithBodyCapturePolicy(policy BodyCapturePolicy) Option

WithBodyCapturePolicy installs a per-request/per-response body capture policy. Nil restores the global Options behavior.

func WithBodyStore

func WithBodyStore(s BodyStore) Option

WithBodyStore sets the storage backend for captured body bytes.

func WithCaptureCertificates

func WithCaptureCertificates(v, raw bool) Option

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

func WithCaptureCookies

func WithCaptureCookies(v bool) Option

WithCaptureCookies toggles cookie capture.

func WithCaptureHeaders

func WithCaptureHeaders(v bool) Option

WithCaptureHeaders toggles header capture.

func WithCaptureRawTrace

func WithCaptureRawTrace(v bool) Option

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

func WithCaptureRequestBody

func WithCaptureRequestBody(v bool) Option

WithCaptureRequestBody toggles request body content capture.

func WithCaptureResponseBody

func WithCaptureResponseBody(v bool) Option

WithCaptureResponseBody toggles response body content capture.

func WithCaptureTLS

func WithCaptureTLS(v bool) Option

WithCaptureTLS toggles the "_tls" extension.

func WithContentDecoder

func WithContentDecoder(encoding string, dec ContentDecoder) Option

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

func WithEmbedBodies

func WithEmbedBodies(v bool) Option

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

func WithErrorRedactor

func WithErrorRedactor(fn func(string) string) Option

WithErrorRedactor sets the error message redaction function.

func WithHashBodies

func WithHashBodies(enabled bool, algorithm string) Option

WithHashBodies configures body hashing.

func WithInternalErrorMode

func WithInternalErrorMode(m InternalErrorMode) Option

WithInternalErrorMode sets the internal error policy.

func WithLogf

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

WithLogf sets the logger used by InternalErrorLog.

func WithMaxRequestBodyBytes

func WithMaxRequestBodyBytes(n int64) Option

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

func WithMaxResponseBodyBytes

func WithMaxResponseBodyBytes(n int64) Option

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

func WithOnEntryCompleted

func WithOnEntryCompleted(fn OnEntryCompleted) Option

WithOnEntryCompleted sets the per-entry completion callback.

func WithOnInternalError

func WithOnInternalError(fn func(error)) Option

WithOnInternalError sets the internal error callback.

func WithOptions

func WithOptions(o Options) Option

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

func WithRedaction added in v0.3.0

func WithRedaction(config RedactionConfig) Option

WithRedaction adds common and direction-specific rules to the Transport's redaction baseline. Name selectors are additive. A later custom body-redactor registration wins for the same normalized MIME type.

func WithSensitiveValueProtection added in v0.2.0

func WithSensitiveValueProtection(config SensitiveValueProtection) Option

WithSensitiveValueProtection configures the representation of values selected by built-in redaction rules.

type Options

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

	// Redaction contains global common and direction-specific selector rules
	// plus trusted custom body redactors. WithRedaction adds to this baseline;
	// request contexts may add more through WithRequestRedaction.
	Redaction RedactionConfig

	// SensitiveValueProtection controls whether values selected by built-in
	// redaction rules 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". Unknown values fall back to sha256.
	BodyHashAlgorithm string

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

	// ContentDecoders maps Content-Encoding tokens (lower-case) to decoders
	// used 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. DefaultOptions installs the
	// stdlib-only set (gzip, x-gzip, deflate); WithContentDecoder registers
	// additional ones such as brotli or zstd — see ContentDecoder for
	// ready-to-paste recipes. Decoding never touches 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

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

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

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

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

Options configures a Transport. The zero value disables every optional content/metadata capture; finalized entries still contain the core exchange lifecycle and body byte/completion accounting. Use DefaultOptions (applied automatically by NewTransport) for sensible production defaults.

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

func DefaultOptions

func DefaultOptions() Options

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

type PostData

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

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

type PostParam

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

PostParam is a single posted form parameter.

type 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 interface {
	ProtectionKey(mode ProtectionMode) (ProtectionKey, error)
}

ProtectionKeyProvider returns the active key for the requested mode. It may be called concurrently and must not return key material that callers mutate.

type ProtectionKeyProviderFunc added in v0.2.0

type ProtectionKeyProviderFunc func(ProtectionMode) (ProtectionKey, error)

ProtectionKeyProviderFunc adapts a function to ProtectionKeyProvider.

func (ProtectionKeyProviderFunc) ProtectionKey added in v0.2.0

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   ProtectionMode = "redact"
	ProtectionEncrypt  ProtectionMode = "encrypt"
	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 Recorder

type Recorder interface {
	Record(entry *Entry)
}

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

type RecorderFunc

type RecorderFunc func(*Entry)

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

func (RecorderFunc) Record

func (f RecorderFunc) Record(e *Entry)

Record implements Recorder.

type 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 WithRedaction 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 "_error" on the entry carries the failure detail.

type SensitiveValueProtection added in v0.2.0

type SensitiveValueProtection struct {
	Mode          ProtectionMode
	KeyProvider   ProtectionKeyProvider
	MaxValueBytes int
}

SensitiveValueProtection configures the representation of all values selected by built-in redaction rules. 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 "_tls" extension built from tls.ConnectionState.

type Timings

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

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

type TraceEvent

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

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

type TraceStore

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

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

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

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

type Transport

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

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

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

func NewTransport

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

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

func (*Transport) CloseIdleConnections

func (t *Transport) CloseIdleConnections()

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

func (*Transport) RoundTrip

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

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

Directories

Path Synopsis
docs
examples/csv-redactor
Package csvredactor demonstrates a streaming recorder.BodyRedactor for CSV request and response bodies.
Package csvredactor demonstrates a streaming recorder.BodyRedactor for CSV request and response bodies.
otelrecorder module

Jump to

Keyboard shortcuts

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