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 ¶
- Constants
- Variables
- func DecryptProtectedValue(token string, key ProtectionKey) ([]byte, error)
- func DecryptProtectedValueWith(token string, resolver ProtectionKeyResolver) ([]byte, error)
- func DefaultRedactedHeaders() []string
- func DeflateDecoder(r io.Reader) (io.ReadCloser, error)
- func GzipDecoder(r io.Reader) (io.ReadCloser, error)
- func ProtectedTokenKeyID(token string) (string, error)
- func RequestWithComment(req *http.Request, comment string) *http.Request
- func RequestWithRedaction(req *http.Request, config RedactionConfig) *http.Request
- func SamplingKeyFromContext(ctx context.Context) (string, bool)
- func TraceContext(ctx context.Context) (context.Context, string)
- func TraceIDFromContext(ctx context.Context) (string, bool)
- func VerifyProtectedToken(token string, value []byte, key ProtectionKey) (bool, error)
- func VerifyProtectedTokenWith(token string, value []byte, resolver ProtectionKeyResolver) (bool, error)
- func WithRequestComment(ctx context.Context, comment string) context.Context
- func WithRequestRedaction(ctx context.Context, config RedactionConfig) context.Context
- func WithSamplingKey(ctx context.Context, key string) context.Context
- func WithTraceID(ctx context.Context, id string) context.Context
- type AsyncBackpressurePolicy
- type AsyncDropHandler
- type AsyncDropReason
- type AsyncRecorder
- type AsyncRecorderConfig
- type AsyncRecorderStats
- type BodyCaptureDecision
- type BodyCaptureMeta
- type BodyCapturePolicy
- type BodyDirection
- type BodyInfo
- type BodyMetadata
- type BodyRedactionInfo
- type BodyRedactor
- type BodyStore
- type BodyValue
- type BodyValueProtector
- type BodyWriter
- type Cache
- type CertInfo
- type Config
- type Content
- type ContentDecoder
- type Cookie
- type Creator
- type DebugStreamRecorder
- type DebugStreamRecorderConfig
- type DebugStreamRecorderStats
- type Entry
- type ErrorInfo
- type Expect100Info
- type FileBodyStore
- func (s *FileBodyStore) NewWriter(_ context.Context, _ BodyMetadata) (BodyWriter, error)
- func (s *FileBodyStore) Open(ref string) (io.ReadCloser, error)
- func (s *FileBodyStore) Reconcile(liveRefs []string, grace time.Duration, dryRun bool) (ReconcileResult, error)
- func (s *FileBodyStore) Release(ref string) error
- func (s *FileBodyStore) ReleaseEntryAssets(entry *Entry) error
- func (s *FileBodyStore) Stats() FileBodyStoreStats
- type FileBodyStoreConfig
- type FileBodyStoreStats
- type HAR
- type HARFileRecorder
- func (r *HARFileRecorder) Close() error
- func (r *HARFileRecorder) EntriesByTrace(traceID string) []*Entry
- func (r *HARFileRecorder) Flush() error
- func (r *HARFileRecorder) Record(e *Entry) error
- func (r *HARFileRecorder) RecordBatch(entries []*Entry) error
- func (r *HARFileRecorder) RemoveTrace(traceID string) int
- func (r *HARFileRecorder) TakeTrace(traceID string) []*Entry
- type HeadSamplingDecision
- type HeadSamplingMeta
- type HeadSamplingPolicy
- type InformationalResponse
- type InternalErrorMode
- type JSONStreamRecorder
- type Log
- type MemoryBodyStore
- type MemoryRecorder
- func (r *MemoryRecorder) Entries() []*Entry
- func (r *MemoryRecorder) EntriesByTrace(traceID string) []*Entry
- func (r *MemoryRecorder) HAR() *HAR
- func (r *MemoryRecorder) HARForTrace(traceID string) *HAR
- func (r *MemoryRecorder) Len() int
- func (r *MemoryRecorder) Record(e *Entry) error
- func (r *MemoryRecorder) RecordBatch(entries []*Entry) error
- func (r *MemoryRecorder) RemoveTrace(traceID string) int
- func (r *MemoryRecorder) Reset()
- func (r *MemoryRecorder) Snapshot() ([]*Entry, MemoryRecorderStats)
- func (r *MemoryRecorder) Stats() MemoryRecorderStats
- func (r *MemoryRecorder) TakeTrace(traceID string) []*Entry
- func (r *MemoryRecorder) WriteHAR(w io.Writer) error
- type MemoryRecorderStats
- type NameValuePair
- type NetworkInfo
- type OnEntryCompleted
- type PostData
- type PostParam
- type ProtectionCounts
- type ProtectionKey
- type ProtectionKeyProvider
- type ProtectionKeyResolver
- type ProtectionMode
- type PutIdleInfo
- type ReconcileResult
- type Recorder
- type RecorderEntryExtension
- type RecorderFunc
- type RedactionConfig
- type RedactionInfo
- type RedactionRules
- type RedactionScopeInfo
- type Request
- type Response
- type RetentionDecision
- type RetentionPolicy
- type SamplingStats
- type SensitiveValueProtection
- type TLSInfo
- type Timings
- type TraceEvent
- type TraceStore
- type Transport
Examples ¶
Constants ¶
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.
const ( BodyRedactionRedacted = "redacted" BodyRedactionUnchanged = "unchanged" BodyRedactionFailed = "failed" )
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.
const DefaultMemoryRecorderCapacity = 1024
DefaultMemoryRecorderCapacity is the maximum number of finalized entries retained by NewMemoryRecorder.
const RecorderExtensionVersion = "1"
RecorderExtensionVersion is the frozen _recorder wire-schema version.
Variables ¶
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.
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
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
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
SamplingKeyFromContext returns the key installed by WithSamplingKey.
func TraceContext ¶
TraceContext is a convenience wrapper around WithTraceID that generates a fresh random trace ID and returns it alongside the derived context.
func TraceIDFromContext ¶
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
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
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 ¶
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
type BodyCapturePolicy func(context.Context, BodyCaptureMeta, BodyCaptureDecision) (BodyCaptureDecision, error)
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
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
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 ¶
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 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
func (r *DebugStreamRecorder) Stats() DebugStreamRecorderStats
Stats returns a concurrency-safe snapshot.
type DebugStreamRecorderConfig ¶ added in v0.4.2
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
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.
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.
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 ¶
func (MemoryBodyStore) NewWriter(_ context.Context, meta BodyMetadata) (BodyWriter, error)
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
func (r *MemoryRecorder) Stats() MemoryRecorderStats
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.
type MemoryRecorderStats ¶ added in v0.4.0
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 ¶
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
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 ¶
PutIdleInfo is the structured outcome of the connection's return to the keep-alive pool.
type ReconcileResult ¶ added in v0.4.0
ReconcileResult reports an authoritative live-reference reconciliation.
type Recorder ¶
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
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 ¶
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 ¶
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.
Source Files
¶
- async_recorder.go
- body_redactor.go
- body_store.go
- byte_sink.go
- capture_policy.go
- content_decoder.go
- debug_stream_recorder.go
- errors.go
- file_recorder.go
- form_stream_redactor.go
- har.go
- internal_error.go
- json_stream_redactor.go
- memory_recorder.go
- multi_recorder.go
- multipart_stream_redactor.go
- options.go
- options_validate.go
- recorder.go
- redact.go
- redaction_audit.go
- request_body.go
- request_comment.go
- request_redaction.go
- response_body.go
- sampling.go
- sensitive_value.go
- trace.go
- transport.go
- xml_stream_redactor.go
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
|