Documentation
¶
Overview ¶
Package audit provides a standalone, taxonomy-driven audit logging framework for Go applications.
The library validates every audit event against a consumer-defined taxonomy, delivers events asynchronously via a buffered channel, and fans out to multiple configurable outputs.
Multi-Module Structure ¶
Output backends live in separate Go modules so consumers import only what they need:
- github.com/axonops/audit — core (this package; depends on github.com/goccy/go-yaml for ParseTaxonomyYAML)
- github.com/axonops/audit/file — file output with rotation
- github.com/axonops/audit/syslog — RFC 5424 syslog (TCP/UDP/TLS)
- github.com/axonops/audit/webhook — batched HTTP webhook
- github.com/axonops/audit/loki — Grafana Loki output with stream labels
- github.com/axonops/audit/outputconfig — YAML-based output configuration
- github.com/axonops/audit/outputs — convenience: blank-import to register all output types
- github.com/axonops/audit/secrets — secret provider interface for ref+ URI resolution
StdoutOutput and the audittest package ship with core and require no additional import.
Stability ¶
This package follows semantic versioning. The public API is stable as of v1.0.0; breaking changes will not be introduced within a major version.
Quick Start ¶
Define your events in a YAML taxonomy, configure outputs in a second YAML file, and create an auditor with a single call:
//go:embed taxonomy.yaml
var taxonomyYAML []byte
auditor, err := outputconfig.New(ctx, taxonomyYAML, "outputs.yaml")
if err != nil {
log.Fatal(err)
}
defer func() { _ = auditor.Close() }()
err = auditor.AuditEvent(audit.MustNewEventKV("user_create",
"outcome", "success",
"actor_id", "alice",
))
For exploration without YAML files, use DevTaxonomy and NewStdout:
stdout, _ := audit.NewStdout()
auditor, err := audit.New(
audit.WithTaxonomy(audit.DevTaxonomy("user_create")),
audit.WithAppName("demo"),
audit.WithHost("localhost"),
audit.WithOutputs(stdout),
)
See the progressive examples in the examples/ directory for complete working applications.
Core API ¶
- Auditor — core type; created via New
- Option — functional option for New: WithTaxonomy, WithOutputs, WithFormatter, WithMetrics, WithQueueSize, WithShutdownTimeout, WithValidationMode, WithOmitEmpty
Events ¶
There are three emission paths, in order of recommendation:
- Generated typed builders from cmd/audit-gen — compile-time field safety, built-in FieldsDonor sentinel for the zero-drain-side-allocations fast path. Use these whenever event types are known at compile time.
- EventHandle obtained via Auditor.Handle or Auditor.MustHandle — the recommended path when the event type is known at startup but not at compile time (from configuration, a database, or a plugin registry). Cache the handle at startup; per-event calls via EventHandle.Audit skip the basicEvent allocation that NewEvent pays via interface escape.
- NewEvent and NewEventKV — the map-based escape hatch for ad-hoc emission and quick exploration. Each call allocates one basicEvent on the heap (interface escape); NewEventKV additionally allocates the intermediate Fields map.
Symbols in this group:
- Event — interface for typed audit events; pass to Auditor.AuditEvent
- Auditor.AuditEvent — emit an event with context.Background (convenience wrapper)
- Auditor.AuditEventContext — emit with a request-scoped context.Context for cancellation / deadline propagation (#600)
- Auditor.Logger — read the diagnostic logger configured via WithDiagnosticLogger (runtime swap dropped in #696)
- EventHandle — pre-validated handle for zero-caller-side-allocation audit calls; see Auditor.Handle and Auditor.MustHandle
- EventHandle.Audit / EventHandle.AuditContext — handle-side ctx-aware variants
- EventHandle.AuditEvent / EventHandle.AuditEventContext — handle-side event-typed variants
- NewEvent — creates an event for dynamic use without code generation
- NewEventKV — creates an event from alternating key-value pairs (slog-style)
- Fields — defined type over map[string]any with Fields.Has, Fields.String, Fields.Int accessors
See docs/event-emission-paths.md for a side-by-side comparison of the three paths with examples and benchmark numbers.
Outputs ¶
- Output — interface for audit event destinations (file, syslog, webhook, stdout)
- [Stdout] — convenience constructor for StdoutOutput writing to os.Stdout
- StdoutOutput — writes events to stdout or any io.Writer; included in core
- WithOutputs — registers unnamed outputs; WithNamedOutput for per-output routing
- DeliveryReporter — optional interface for outputs that handle their own delivery metrics
- MetadataWriter — optional interface for outputs that need structured per-event context (event type, severity, category, timestamp)
- EventMetadata — per-event value type passed to [MetadataWriter.WriteWithMetadata]
Formatters ¶
- Formatter — interface for event serialisation
- JSONFormatter — default; line-delimited JSON with deterministic field order
- CEFFormatter — Common Event Format for SIEM integration (Splunk, ArcSight, QRadar)
- FormatOptions — per-output context for sensitivity label exclusion
Taxonomy ¶
- Taxonomy — consumer-defined event schema; registered via WithTaxonomy
- EventDef — definition of a single event type's required and optional fields
- CategoryDef — category grouping with optional default severity
- DevTaxonomy — creates a permissive development taxonomy (not for production)
- ParseTaxonomyYAML — parses a YAML document into a Taxonomy; use with //go:embed
- ValidateTaxonomy — validates a Taxonomy for internal consistency
- SensitivityConfig — sensitivity label definitions for field classification
- SensitivityLabel — a single label with global field mappings and regex patterns
Event Routing ¶
- EventRoute — per-output event filter (include/exclude categories, severity range)
- ValidateEventRoute — validates route configuration against a taxonomy
- MatchesRoute — checks whether an event matches a route filter
HTTP Middleware ¶
- Middleware — wraps an HTTP handler to capture request metadata for audit logging
- Hints — per-request audit metadata populated by handlers via HintsFromContext
- TransportMetadata — auto-captured HTTP fields (client IP, method, status code, duration)
- EventBuilder — callback that transforms hints + transport into an audit event
Metrics ¶
- Metrics — optional instrumentation interface; track deliveries, drops, and errors
Introspection ¶
The auditor exposes runtime introspection primitives for operators:
- Auditor.QueueLen, Auditor.QueueCap — core queue saturation
- Auditor.OutputNames — names of configured outputs
- Auditor.IsDisabled — whether the auditor is a no-op
- Auditor.LastDeliveryAge — duration since each output last delivered successfully; use in /healthz handlers to detect silently-stalled async outputs (TCP half-open, retries exhausted) where Write enqueues succeed but no events ever land. See examples/18-health-endpoint for a runnable /healthz pattern.
Error Discrimination ¶
Validation errors returned by Auditor.AuditEvent wrap ErrValidation as a parent sentinel. Specific sub-sentinels identify the failure:
- ErrUnknownEventType — event type not in taxonomy
- ErrMissingRequiredField — required fields absent
- ErrUnknownField — unrecognised fields (strict mode only)
Use errors.Is to match broadly or narrowly:
if errors.Is(err, audit.ErrValidation) { /* any validation failure */ }
if errors.Is(err, audit.ErrUnknownEventType) { /* specific case */ }
Use errors.As to access the ValidationError struct:
var ve *audit.ValidationError
if errors.As(err, &ve) { log.Println(ve.Error()) }
ErrQueueFull and ErrClosed are NOT validation errors and will never match ErrValidation.
Code Generation Support ¶
- LabelInfo — sensitivity label descriptor; embedded in FieldInfo
- FieldInfo — field descriptor with name, required flag, and labels; returned by generated builders
- CategoryInfo — category descriptor with name and optional severity; returned by generated builders
Advanced ¶
- OutputFactory — function signature for output factory registration
- RegisterOutputFactory — registers a factory by type name (used by output modules)
- LookupOutputFactory — retrieves a registered factory by type name
- TLSPolicy — shared TLS version and cipher suite policy for outputs
- HMACConfig — per-output HMAC integrity configuration
- ComputeHMAC — computes HMAC over a payload, returns lowercase hex
- VerifyHMAC — verifies an HMAC value matches a payload
- ValidateHMACConfig — validates HMAC configuration at startup
- OutputOption — per-output configuration for WithNamedOutput: WithRoute, WithOutputFormatter, WithExcludeLabels, WithHMAC
- MigrateTaxonomy — applies version migration to a Taxonomy
How Taxonomy Validation Works ¶
The framework does not hardcode event types, field names, or categories. Consumers register their entire audit taxonomy at bootstrap via WithTaxonomy. The framework then validates every Auditor.AuditEvent call against the registered definitions, catching missing required fields, unknown event types, and unrecognised field names at runtime.
Sensitivity Labels ¶
Consumers MAY define sensitivity labels in SensitivityConfig to classify fields (e.g., "pii", "financial"). Labels are assigned to fields via three mechanisms: explicit per-event annotation in the YAML fields: map, global field name mapping in [SensitivityLabel.Fields], and regex patterns in [SensitivityLabel.Patterns]. Per-output field stripping is configured via WithNamedOutput using the excludeLabels parameter. Framework fields (timestamp, event_type, severity, duration_ms, event_category, app_name, host, timezone, pid) are never stripped.
Reserved Standard Fields ¶
The library defines 31 well-known audit field names (actor_id, source_ip, reason, target_id, etc.) that are always accepted without taxonomy declaration. These reserved standard fields have generated setter methods on every builder and map to standard ArcSight CEF extension keys. See ReservedStandardFieldNames for the complete list.
Framework Fields ¶
Every serialised event includes framework fields that identify the deployment: app_name, host, timezone (set via WithAppName, WithHost, WithTimezone or outputs YAML), and pid (auto-captured via os.Getpid). These fields cannot be stripped by sensitivity labels and are emitted in both JSON and CEF output.
Async Delivery ¶
Events are enqueued to a buffered channel (configurable capacity, default 10,000) and drained by a single background goroutine. If the buffer is full, Auditor.AuditEvent returns ErrQueueFull and the drop is recorded via the Metrics interface.
Performance — Fast Path and Slow Path ¶
The drain pipeline has two paths with distinct allocation profiles. See docs/performance.md for the full table and benchmark methodology.
Fast path: events constructed via cmd/audit-gen-generated typed builders satisfy the FieldsDonor extension interface (unexported sentinel donateFields()), and the auditor takes ownership of the event's Fields map. The formatter writes into a pool-leased *bytes.Buffer that is shared across every output and category pass for the same event; per-output post-fields (event_category, _hmac_version, _hmac) are appended in place into a per-event scratch buffer. This path achieves zero allocations on the drain side after warm-up.
Slow path: events constructed via NewEvent or NewEventKV do not implement FieldsDonor, so the auditor defensively copies the caller's Fields map. Per-event allocation cost is the map clone plus any-boxing of non-string values, plus one basicEvent on the heap from the interface escape. The drain-side serialisation still benefits from the zero-copy buffer lease. When the event type is dynamic but known at startup, EventHandle.Audit eliminates the basicEvent allocation (no interface escape) while still taking the same defensive-copy path for the Fields map.
Outputs receive bytes from the leased formatter buffer. Per the [Output.Write] contract, implementations MUST NOT retain data past the call — all first-party outputs (file, syslog, webhook, loki) copy on enqueue.
Graceful Shutdown ¶
Auditor.Close MUST be called when the auditor is no longer needed. Failing to call Close leaks the drain goroutine and causes any buffered events to be lost. Close signals the drain goroutine to stop, waits up to the configured WithShutdownTimeout for pending events to flush, then closes all outputs in parallel. Events still in the buffer when the shutdown timeout expires are lost; a warning is emitted via log/slog. Close is idempotent via sync.Once.
Index ¶
- Constants
- Variables
- func AppendPostField(data []byte, formatter Formatter, field PostField) []byte
- func AppendPostFields(data []byte, formatter Formatter, fields []PostField) []byte
- func CheckSSRFAddress(address string, allowPrivate bool) error
- func CheckSSRFIP(ip net.IP, allowPrivate bool) error
- func ComputeHMAC(payload, salt []byte, algorithm string) (string, error)
- func DefaultCEFFieldMapping() map[string]string
- func IsReservedStandardField(name string) bool
- func MatchesRoute(route *EventRoute, eventType, category string, severity int) bool
- func Middleware(auditor *Auditor, builder EventBuilder) func(http.Handler) http.Handler
- func MigrateTaxonomy(t *Taxonomy) error
- func MustRegisterOutputFactory(typeName string, factory OutputFactory)
- func NewSSRFDialControl(opts ...SSRFOption) func(string, string, syscall.RawConn) error
- func RegisterOutputFactory(typeName string, factory OutputFactory) error
- func RegisteredOutputTypes() []string
- func ReservedStandardFieldNames() []string
- func SupportedHMACAlgorithms() []string
- func ValidateEventRoute(route *EventRoute, taxonomy *Taxonomy) error
- func ValidateHMACConfig(cfg *HMACConfig) error
- func ValidateOutputName(name string) error
- func ValidateTaxonomy(t Taxonomy) error
- func VerifyHMAC(payload []byte, hmacValue string, salt []byte, algorithm string) (bool, error)
- func WrapUnknownFieldError(err error, target any) error
- func WriteJSONBytes(buf *bytes.Buffer, b []byte)
- func WriteJSONString(buf *bytes.Buffer, s string)
- type Auditor
- func (a *Auditor) AuditEvent(evt Event) error
- func (a *Auditor) AuditEventContext(ctx context.Context, evt Event) error
- func (a *Auditor) ClearOutputRoute(outputName string) error
- func (a *Auditor) Close() error
- func (a *Auditor) DisableCategory(category string) error
- func (a *Auditor) DisableEvent(eventType string) error
- func (a *Auditor) EnableCategory(category string) error
- func (a *Auditor) EnableEvent(eventType string) error
- func (a *Auditor) Handle(eventType string) (*EventHandle, error)
- func (a *Auditor) IsCategoryEnabled(category string) bool
- func (a *Auditor) IsDisabled() bool
- func (a *Auditor) IsEventEnabled(eventType string) bool
- func (a *Auditor) IsSynchronous() bool
- func (a *Auditor) LastDeliveryAge(outputName string) time.Duration
- func (a *Auditor) Logger() *slog.Logger
- func (a *Auditor) MustHandle(eventType string) *EventHandle
- func (a *Auditor) OutputNames() []string
- func (a *Auditor) OutputRoute(outputName string) (EventRoute, error)
- func (a *Auditor) QueueCap() int
- func (a *Auditor) QueueLen() int
- func (a *Auditor) SetOutputRoute(outputName string, route *EventRoute) error
- type CEFFormatter
- type CategoryDef
- type CategoryInfo
- type DeliveryReporter
- type DestinationKeyer
- type Event
- type EventBuilder
- type EventDef
- type EventHandle
- func (e *EventHandle) Audit(fields Fields) error
- func (e *EventHandle) AuditContext(ctx context.Context, fields Fields) error
- func (e *EventHandle) AuditEvent(evt Event) error
- func (e *EventHandle) AuditEventContext(ctx context.Context, evt Event) error
- func (e *EventHandle) Categories() []CategoryInfo
- func (e *EventHandle) Description() string
- func (e *EventHandle) EventType() string
- func (e *EventHandle) FieldInfoMap() map[string]FieldInfo
- type EventMetadata
- type EventRoute
- type EventStatus
- type FieldInfo
- type Fields
- type FieldsDonor
- type FormatOptions
- type Formatter
- type FrameworkContext
- type FrameworkFieldSetter
- type HMACConfig
- type HMACSalt
- type Hints
- type JSONFormatter
- type LabelInfo
- type LastDeliveryReporter
- type MetadataWriter
- type Metrics
- type NoOpMetrics
- func (NoOpMetrics) RecordBufferDrop()
- func (NoOpMetrics) RecordDelivery(string, EventStatus)
- func (NoOpMetrics) RecordFiltered(string)
- func (NoOpMetrics) RecordOutputError(string)
- func (NoOpMetrics) RecordOutputFiltered(string)
- func (NoOpMetrics) RecordQueueDepth(int, int)
- func (NoOpMetrics) RecordSerializationError(string)
- func (NoOpMetrics) RecordSubmitted()
- func (NoOpMetrics) RecordValidationError(string)
- type NoOpOutputMetrics
- type NoopSanitizer
- type Option
- func WithAppName(name string) Option
- func WithDiagnosticLogger(l *slog.Logger) Option
- func WithDisabled() Option
- func WithFormatter(f Formatter) Option
- func WithHost(host string) Option
- func WithMetrics(m Metrics) Option
- func WithNamedOutput(output Output, opts ...OutputOption) Option
- func WithOmitEmpty() Option
- func WithOutputs(outputs ...Output) Option
- func WithQueueSize(n int) Option
- func WithSanitizer(s Sanitizer) Option
- func WithShutdownTimeout(d time.Duration) Option
- func WithStandardFieldDefaults(defaults map[string]any) Option
- func WithSynchronousDelivery() Option
- func WithTaxonomy(t *Taxonomy) Option
- func WithTimezone(tz string) Option
- func WithValidationMode(m ValidationMode) Option
- type Output
- type OutputFactory
- type OutputMetrics
- type OutputMetricsFactory
- type OutputOption
- type PostField
- type ReservedFieldType
- type SSRFBlockedError
- type SSRFOption
- type SSRFReason
- type Sanitizer
- type SensitivityConfig
- type SensitivityLabel
- type StdoutConfig
- type StdoutOutput
- type TLSPolicy
- type Taxonomy
- type TimestampFormat
- type TransportMetadata
- type ValidationError
- type ValidationMode
Examples ¶
- Auditor.AuditEvent
- Auditor.AuditEventContext
- Auditor.Close
- Auditor.EnableCategory
- Auditor.Handle
- Auditor.MustHandle
- Auditor.SetOutputRoute
- EventRoute (Exclude)
- EventRoute (Include)
- FieldsDonor
- HintsFromContext
- Middleware
- Middleware (Router)
- Middleware (Skip)
- New
- NewStdoutOutput
- ParseTaxonomyYAML
- ParseTaxonomyYAML (SensitivityLabels)
- ParseTaxonomyYAML (Validation)
- WithFormatter
Constants ¶
const ( // ValidationStrict rejects unknown fields with an error; it is the // default when no [WithValidationMode] option is supplied. ValidationStrict ValidationMode = "strict" // ValidationWarn logs a warning for unknown fields via [log/slog] // but accepts the event. ValidationWarn ValidationMode = "warn" // ValidationPermissive silently accepts unknown fields. ValidationPermissive ValidationMode = "permissive" // DefaultQueueSize is the default async intake queue capacity. DefaultQueueSize = 10_000 // MaxQueueSize is the maximum allowed async intake queue capacity. // Values above this limit cause [New] to return an error // wrapping [ErrConfigInvalid]. MaxQueueSize = 1_000_000 // DefaultShutdownTimeout is the default graceful shutdown deadline. DefaultShutdownTimeout = 5 * time.Second // MaxShutdownTimeout is the maximum allowed graceful shutdown deadline. // Values above this limit cause [New] to return an error // wrapping [ErrConfigInvalid]. Setting ShutdownTimeout too low on a // high-throughput system causes events to be lost at shutdown. MaxShutdownTimeout = 60 * time.Second )
const ( // MinSeverity is the minimum allowed severity (inclusive). MinSeverity = 0 // MaxSeverity is the maximum allowed severity (inclusive). MaxSeverity = 10 )
Severity bounds for event and route severity values. These follow the CEF range convention (0 = least severe, 10 = most severe). Both bounds are inclusive. See [EventDef.Severity], [CategoryDef.Severity], and [EventRoute.MinSeverity] / [EventRoute.MaxSeverity].
const FieldSanitizerFailed = "sanitizer_failed"
FieldSanitizerFailed names the framework field set to true on the middleware audit event when [Sanitizer.SanitizePanic] itself panicked during panic-recovery. The original panic value is used in both the audit event and the re-raise.
const FieldSanitizerFailedFields = "sanitizer_failed_fields"
FieldSanitizerFailedFields names the framework field appended to the event when one or more [Sanitizer.SanitizeField] calls panicked. The value is a []string of the offending field keys, sorted for stability.
const MaxOutputNameLength = 128
MaxOutputNameLength is the maximum allowed length for an output name.
const MinSaltLength = 16
MinSaltLength is the minimum salt length in bytes for HMAC computation, per NIST SP 800-224 (minimum key length: 128 bits).
const SanitizerPanicSentinel = "[sanitizer_panic]"
SanitizerPanicSentinel is the placeholder value substituted into Fields when [Sanitizer.SanitizeField] panics on a particular key. Callers can search for this string in audit logs to identify fields that failed scrubbing without leaking the original value.
Variables ¶
var ( // ErrClosed is returned by [Auditor.AuditEvent] when the auditor has // been shut down via [Auditor.Close]. Once returned, all subsequent // [Auditor.AuditEvent] calls return ErrClosed immediately. ErrClosed = errors.New("audit: auditor is closed") // ErrQueueFull is returned by [Auditor.AuditEvent] when the async // intake queue is at capacity and the event is dropped. Consumers // SHOULD treat this as a drop notification rather than a fatal error. // Increasing [WithQueueSize] or reducing event emission rate // reduces the frequency of this error. ErrQueueFull = errors.New("audit: queue full") // ErrDuplicateDestination is returned by [WithOutputs] and // [WithNamedOutput] when two outputs implement [DestinationKeyer] // and return the same key. This prevents accidental double-delivery // to the same file, syslog address, or webhook URL. ErrDuplicateDestination = errors.New("audit: duplicate destination") // ErrConfigInvalid is the sentinel error wrapped by all configuration // validation failures. Use [errors.Is] to test for it: // // if errors.Is(err, audit.ErrConfigInvalid) { ... } ErrConfigInvalid = errors.New("audit: config validation failed") // ErrHandleNotFound is returned by [Auditor.Handle], and wrapped in // the panic value of [Auditor.MustHandle], when the requested event // type is not registered in the taxonomy. ErrHandleNotFound = errors.New("audit: event type not found") // ErrOutputClosed is returned by [Output.Write] when the output has // already been closed. ErrOutputClosed = errors.New("audit: output is closed") // ErrEventTooLarge is returned by async output [Output.Write] // methods (syslog, loki, webhook) when the supplied event byte // length exceeds the output's configured MaxEventBytes. Wrapped // alongside [ErrValidation] so callers can discriminate via // errors.Is: // // if errors.Is(err, audit.ErrEventTooLarge) { ... } // if errors.Is(err, audit.ErrValidation) { ... } // // Introduced by #688 as a DoS defence against consumer-controlled // memory pressure — a 10 MiB event × 10 000-slot buffer could // pin ~100 GiB before backpressure triggers. Default cap is // 1 MiB per output; configurable via each output's MaxEventBytes // Config field. ErrEventTooLarge = errors.New("audit: event exceeds max_event_bytes") // ErrDisabled is returned by methods that require a taxonomy // ([Auditor.EnableCategory], [Auditor.DisableCategory], // [Auditor.EnableEvent], [Auditor.DisableEvent], // [Auditor.SetOutputRoute]) when called on a disabled auditor. // [Auditor.Handle] returns a valid no-op handle instead. ErrDisabled = errors.New("audit: auditor is disabled") // ErrTaxonomyRequired is returned by [New] when [WithTaxonomy] was // not called (unless [WithDisabled] is applied). Sibling of // [ErrAppNameRequired] / [ErrHostRequired] — all three mark missing // required options and support [errors.Is] discrimination. ErrTaxonomyRequired = errors.New("audit: taxonomy is required: use WithTaxonomy") // ErrAppNameRequired is returned by [New] when [WithAppName] was // not called. All auditors (except those constructed with // [WithDisabled]) must set an app name for compliance — every // emitted event carries app_name as a framework field, and a blank // value undermines attribution. Matches the [outputconfig.Load] // YAML-path requirement for symmetry across construction paths. ErrAppNameRequired = errors.New("audit: app_name is required: use WithAppName") // ErrHostRequired is returned by [New] when [WithHost] was not // called. All auditors (except those constructed with [WithDisabled]) // must set a host identifier for compliance. Matches the // [outputconfig.Load] YAML-path requirement for symmetry across // construction paths. ErrHostRequired = errors.New("audit: host is required: use WithHost") // ErrTaxonomyInvalid is the sentinel error wrapped by taxonomy // validation failures. Use [errors.Is] to test for it: // // if errors.Is(err, audit.ErrTaxonomyInvalid) { ... } ErrTaxonomyInvalid = errors.New("audit: taxonomy validation failed") // ErrInvalidTaxonomyName is returned by [ValidateTaxonomy] when a // category name, sensitivity label name, event type key, or field // name fails the character-set or length rule. Names must match // `^[a-z][a-z0-9_]*$` and be no longer than 128 bytes — enforced // at load to keep bidi overrides, Unicode confusables, CEF/JSON // metacharacters, and all C0/C1 control bytes out of downstream // log consumers and SIEM dashboards (issue #477). // // Always wrapped alongside [ErrTaxonomyInvalid] via [errors.Join], // so either sentinel satisfies [errors.Is]: // // if errors.Is(err, audit.ErrInvalidTaxonomyName) { ... } // if errors.Is(err, audit.ErrTaxonomyInvalid) { ... } ErrInvalidTaxonomyName = errors.New("audit: invalid taxonomy name") // ErrInvalidInput is returned by [ParseTaxonomyYAML] when the input // is structurally unsuitable — empty, a multi-document YAML stream, // or syntactically invalid. Taxonomy content validation errors wrap // [ErrTaxonomyInvalid] instead. ErrInvalidInput = errors.New("audit: invalid input") // ErrValidation is the parent sentinel for all [Auditor.AuditEvent] // validation failures (unknown event type, missing required fields, // unknown fields in strict mode). Use [errors.Is] to catch any // validation failure: // // if errors.Is(err, audit.ErrValidation) { ... } // // [ErrQueueFull] and [ErrClosed] are NOT validation errors. ErrValidation = errors.New("audit: validation error") // ErrUnknownEventType is returned by [Auditor.AuditEvent] when the // event type is not registered in the taxonomy. Always wrapped // alongside [ErrValidation] via [ValidationError]. ErrUnknownEventType = errors.New("audit: unknown event type") // ErrMissingRequiredField is returned by [Auditor.AuditEvent] when // one or more required fields are absent. Always wrapped alongside // [ErrValidation] via [ValidationError]. ErrMissingRequiredField = errors.New("audit: missing required field") // ErrUnknownField is returned by [Auditor.AuditEvent] in strict // validation mode when one or more fields are not declared in the // taxonomy. Always wrapped alongside [ErrValidation] via // [ValidationError]. ErrUnknownField = errors.New("audit: unknown field") // ErrUnknownFieldType is returned by [Auditor.AuditEvent] in // strict validation mode when a [Fields] entry carries a value // of a type not in the supported set documented on [Fields]. // Always wrapped alongside [ErrValidation] via [ValidationError]. // // In warn and permissive modes, unsupported values are coerced // via fmt.Sprintf("%v", v) instead of returning this error; in // warn mode a diagnostic-logger warning is emitted as well. See // the Fields godoc for the full type vocabulary and behaviour // matrix. ErrUnknownFieldType = errors.New("audit: unsupported field value type") // ErrHMACMalformed is returned by [VerifyHMAC] when the // supplied HMAC value is structurally invalid — empty, the // wrong length for the algorithm's hash size, or contains // non-hex characters. Validation runs BEFORE the constant-time // compare, since malformed inputs are pre-authentication // structural rejects and not timing-sensitive. // // Always paired with [ErrValidation] via [errors.Join] so // consumers can discriminate: // // if errors.Is(err, audit.ErrHMACMalformed) { ... } // if errors.Is(err, audit.ErrValidation) { ... } ErrHMACMalformed = errors.New("audit: hmac value malformed") // ErrSSRFBlocked is the sentinel wrapped by every // [SSRFBlockedError] produced by [CheckSSRFIP] / // [CheckSSRFAddress]. Use [errors.Is] for broad discrimination // and [errors.As] (against `*SSRFBlockedError`) when the // specific block reason is needed for metrics or incident // routing: // // var ssrfErr *audit.SSRFBlockedError // if errors.As(err, &ssrfErr) { // metricSSRFBlocked.With("reason", string(ssrfErr.Reason)).Inc() // } // if errors.Is(err, audit.ErrSSRFBlocked) { ... } ErrSSRFBlocked = errors.New("audit: address blocked by SSRF protection") // ErrReservedFieldName is returned by [Auditor.AuditEvent] when // the event's Fields map uses a name reserved for library-emitted // fields (for example `_hmac`, `_hmac_version`). These names would // collide with library output and could enable canonicalisation- // ambiguity attacks on HMAC verifiers (issue #473). This check runs // regardless of [ValidationMode]; permissive mode cannot opt out. // Always wrapped alongside [ErrValidation] via [ValidationError]. ErrReservedFieldName = errors.New("audit: reserved field name") )
Sentinel errors returned by the audit package. Use errors.Is to test for these in consumer code.
var ErrHijackNotSupported = errors.New("audit: underlying ResponseWriter does not support hijacking")
ErrHijackNotSupported is returned by the middleware's response writer Hijack method when the underlying http.ResponseWriter does not implement http.Hijacker.
Functions ¶
func AppendPostField ¶ added in v0.1.2
AppendPostField appends a single post-serialisation field to cached bytes. This is the zero-allocation fast path for the common case of appending one field (event_category, HMAC). For multiple fields, use AppendPostFields.
func AppendPostFields ¶
AppendPostFields appends one or more post-serialisation fields to cached bytes. The formatter type determines the syntax: JSON: ,"key":"val" inserted before }\n CEF: key=val inserted before the newline.
func CheckSSRFAddress ¶
CheckSSRFAddress validates that a resolved address is not blocked by SSRF policy. The address must be in host:port format.
On rejection, returns *SSRFBlockedError wrapping ErrSSRFBlocked. Parse errors (bad address format, unparseable IP) return plain errors and do NOT wrap ErrSSRFBlocked.
func CheckSSRFIP ¶
CheckSSRFIP validates that an IP address is not blocked by SSRF policy. Exported for direct unit testing of IP classification.
On rejection, returns *SSRFBlockedError wrapping ErrSSRFBlocked. IPv4-mapped IPv6 forms (e.g. ::ffff:10.0.0.1) are normalised to their IPv4 equivalent before classification — a consumer cannot bypass the block list by bracketing an IPv4 address as an IPv6 literal.
func ComputeHMAC ¶
ComputeHMAC computes the HMAC for the given payload and returns the lowercase hex-encoded result. The algorithm must be one of the supported NIST-approved values (see SupportedHMACAlgorithms).
ComputeHMAC returns a non-nil error in three cases:
- len(payload) == 0 — the empty payload is rejected to prevent "empty event was signed" ambiguity.
- len(salt) == 0 — the empty salt is rejected because an HMAC with empty key collapses to a plain hash (no authentication).
- algorithm not in SupportedHMACAlgorithms — unknown algorithms are rejected rather than silently falling back.
The returned string is always lowercase hex, matching what VerifyHMAC accepts on the receiving side.
func DefaultCEFFieldMapping ¶
DefaultCEFFieldMapping returns a new map containing the built-in field mapping from audit field names to standard CEF extension keys. Each call returns a distinct map instance; callers may freely mutate the result. Consumers can use this as a base, add or override entries, and pass the result to [CEFFormatter.FieldMapping].
func IsReservedStandardField ¶
IsReservedStandardField reports whether name is a reserved standard field. Reserved standard fields are well-known audit field names (actor_id, source_ip, reason, etc.) that are always accepted without taxonomy declaration. Uses a precomputed map for O(1) lookup.
func MatchesRoute ¶
func MatchesRoute(route *EventRoute, eventType, category string, severity int) bool
MatchesRoute reports whether an event should be delivered to an output with the given route. eventType is the event name, category is its taxonomy category, severity is the event's resolved severity (0-10). An empty route matches all events.
Severity filtering is an AND condition: the event must pass both the severity check and the category/event type check. Severity is checked first for performance — severity-only routes (the PagerDuty use case) short-circuit without entering the category/event type logic.
When pre-computed sets are available (route created via setRoute), category/event type lookups are O(1). Falls back to slices.Contains for routes constructed as direct struct literals.
func Middleware ¶
Middleware returns HTTP middleware that captures transport metadata automatically and calls the EventBuilder after the handler returns. The builder transforms Hints (populated by the handler) and TransportMetadata into an audit event.
If auditor is nil, the returned middleware is an identity function that passes requests through without auditing. This allows consumers to conditionally disable audit middleware without nil-checking at every call site.
Middleware panics if builder is nil. Passing a nil builder is a programming error: there is no recoverable behaviour when the event-building callback is absent. Pass a nil *Auditor instead to disable auditing without removing the middleware.
Placement ¶
Middleware SHOULD be placed OUTSIDE any panic-recovery middleware in the chain — i.e. the audit middleware wraps the recovery middleware, not the other way round. The rule matters because Middleware always catches panics internally (to record an audit event before the request goroutine unwinds) and then re-raises so that a downstream recovery middleware can render the final response.
Correct (audit outermost, recovery inside):
handler := audit.Middleware(auditor, builder)( // OUTER
recoveryMiddleware( // INNER — catches panic first
yourHandler,
),
)
Flow on a panic: yourHandler panics, the inner recovery middleware catches it and writes its chosen response (typically 500), the handler returns normally. Middleware sees the already-recorded status code on the response writer and records the audit event. No re-raise is emitted because invokeHandler observed a clean return. The status code in the audit event matches the status the recovery middleware actually wrote.
Wrong (recovery outermost — fragile, not recommended):
handler := recoveryMiddleware( // OUTER — catches re-raise
audit.Middleware(auditor, builder)( // INNER — records event, re-raises
yourHandler,
),
)
Flow on a panic: yourHandler panics, Middleware's internal recover() catches it, sets the response-writer status to 500, records the audit event, and re-raises. The outer recovery middleware then catches the re-raised panic and writes its own response. The audit event IS emitted, but with two downsides: the status code in the event is always 500 (set internally by Middleware before the re-raise), independent of what the outer recovery actually renders; and some recovery frameworks mishandle a second recover pass for unknown panic values, producing a crashed request goroutine with inconsistent logging.
See docs/http-middleware.md for the detailed rationale and framework-specific wiring examples (#491).
Example ¶
package main
import (
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
taxonomy := &audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{
"access": {Events: []string{"http_request"}},
},
Events: map[string]*audit.EventDef{
"http_request": {
Required: []string{"outcome"},
Optional: []string{"status_code"},
},
},
}
auditor, err := audit.New(
audit.WithTaxonomy(taxonomy),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = auditor.Close() }()
// The EventBuilder transforms per-request hints into an audit event.
builder := func(hints *audit.Hints, transport *audit.TransportMetadata) (string, audit.Fields, bool) {
return "http_request", audit.Fields{
"outcome": hints.Outcome,
"method": transport.Method,
"path": transport.Path,
"status_code": transport.StatusCode,
}, false
}
mw := audit.Middleware(auditor, builder)
_ = mw // wrap your http.Handler with mw(handler)
fmt.Println("middleware created")
}
Output: middleware created
Example (Router) ¶
package main
import (
"fmt"
)
func main() {
// Middleware works with any router that supports
// the func(http.Handler) http.Handler middleware pattern.
//
// // net/http
// mux := http.NewServeMux()
// mux.Handle("/", handler)
// http.ListenAndServe(":8080", mw(mux))
//
// // chi
// r := chi.NewRouter()
// r.Use(mw)
//
// // gorilla/mux
// r := mux.NewRouter()
// r.Use(mw)
fmt.Println("works with any router")
}
Output: works with any router
Example (Skip) ¶
package main
import (
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
taxonomy := &audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{
"access": {Events: []string{"http_request"}},
},
Events: map[string]*audit.EventDef{
"http_request": {
Required: []string{"outcome"},
Optional: []string{},
},
},
}
auditor, err := audit.New(
audit.WithTaxonomy(taxonomy),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = auditor.Close() }()
// Skip health-check endpoints to reduce noise.
builder := func(hints *audit.Hints, transport *audit.TransportMetadata) (string, audit.Fields, bool) {
if transport.Path == "/healthz" || transport.Path == "/readyz" {
return "", nil, true // skip
}
return "http_request", audit.Fields{
"outcome": hints.Outcome,
"path": transport.Path,
}, false
}
mw := audit.Middleware(auditor, builder)
_ = mw
fmt.Println("skip middleware created")
}
Output: skip middleware created
func MigrateTaxonomy ¶
MigrateTaxonomy applies backwards-compatible migrations to older taxonomy versions, transforming an in-memory *Taxonomy in place so it conforms to the schema this library version understands. Returns an error wrapping ErrTaxonomyInvalid if the version is unsupported (zero, above the maximum, or below the minimum still accepted by this library).
This function is called automatically by WithTaxonomy; it is exported so that ParseTaxonomyYAML and other callers can apply migration before calling ValidateTaxonomy. Currently only version 1 is defined; future versions will add migration steps here.
func MustRegisterOutputFactory ¶ added in v0.1.12
func MustRegisterOutputFactory(typeName string, factory OutputFactory)
MustRegisterOutputFactory is like RegisterOutputFactory but panics if typeName is empty or factory is nil. Intended for init() call sites where the inputs are literal and a programmer error should crash at startup. Mirrors regexp.MustCompile / [template.Must] — the canonical Go pattern.
func init() {
audit.MustRegisterOutputFactory("mine", mineFactory)
}
func NewSSRFDialControl ¶
NewSSRFDialControl returns a net.Dialer Control function that checks every resolved IP address before a connection is established. Use it with net/http.Transport:
transport := &http.Transport{
DialContext: (&net.Dialer{
Control: audit.NewSSRFDialControl(),
}).DialContext,
}
The Control function blocks connections to:
- Loopback addresses (127.0.0.0/8, ::1)
- Link-local addresses (169.254.0.0/16, fe80::/10)
- Cloud metadata endpoints (169.254.169.254, fd00:ec2::254)
- RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
- IPv6 unique local addresses (fc00::/7)
- RFC 6598 Shared Address Space (100.64.0.0/10, CGNAT)
- Deprecated IPv6 site-local (fec0::/10)
- Multicast addresses (224.0.0.0/4, ff00::/8)
- Unspecified addresses (0.0.0.0, ::)
- IPv4-mapped IPv6 forms of all of the above (e.g. ::ffff:10.0.0.1 is treated as 10.0.0.1).
Returned errors are *SSRFBlockedError (wrapping ErrSSRFBlocked).
Use AllowPrivateRanges to permit private and loopback addresses.
func RegisterOutputFactory ¶
func RegisterOutputFactory(typeName string, factory OutputFactory) error
RegisterOutputFactory registers a factory for the given output type name (e.g. "file", "syslog", "webhook"). It is intended to be called from init() functions in output modules.
Registering the same name twice overwrites the previous factory. This allows consumers to replace init()-registered default factories with metrics-aware factories before calling the config loader.
RegisterOutputFactory returns an error wrapping ErrValidation if typeName is empty or factory is nil. These are programming errors; callers in init() SHOULD panic on a non-nil return so the programmer error surfaces at startup:
func init() {
if err := audit.RegisterOutputFactory("mine", mineFactory); err != nil {
panic("audit/mine: register: " + err.Error())
}
}
Prior to #590 this function panicked directly; the signature change removes the last library-boundary panic in the public API.
Choosing a registration path ¶
RegisterOutputFactory is one of two registration paths. The other is github.com/axonops/audit/outputconfig.WithFactory, which passes a factory as a LoadOption to a single Load call without mutating the global registry. RegisterOutputFactory applies process-wide; WithFactory applies to one Load call only and takes precedence over any globally-registered factory for the same type name.
Use RegisterOutputFactory (typically via a blank-import of an output sub-module) for default production setup. Use WithFactory for tests, per-call overrides, or multiple auditors in one process with different factory bindings. See the "Output Factory Registration" section of docs/output-configuration.md for full guidance on choosing between them.
func RegisteredOutputTypes ¶
func RegisteredOutputTypes() []string
RegisteredOutputTypes returns a sorted list of all registered output type names. Useful for error messages suggesting available types.
func ReservedStandardFieldNames ¶
func ReservedStandardFieldNames() []string
ReservedStandardFieldNames returns the well-known audit field names that are always available on any event without explicit taxonomy declaration. These fields are automatically accepted by the unknown-field check and have standard CEF extension key mappings. The returned slice is a fresh copy in deterministic alphabetical order; callers may modify it safely.
The list is derived from the canonical type map in std_fields.go; see ReservedStandardFieldType for per-field type metadata.
func SupportedHMACAlgorithms ¶
func SupportedHMACAlgorithms() []string
SupportedHMACAlgorithms returns the list of supported HMAC algorithm names for use in documentation and error messages.
func ValidateEventRoute ¶
func ValidateEventRoute(route *EventRoute, taxonomy *Taxonomy) error
ValidateEventRoute checks that the route is well-formed: include and exclude fields are not mixed, severity fields are in range 0-10 and min does not exceed max, and all referenced categories and event types exist in the taxonomy.
func ValidateHMACConfig ¶
func ValidateHMACConfig(cfg *HMACConfig) error
ValidateHMACConfig checks that an HMACConfig is valid. Returns an error wrapping ErrConfigInvalid if the config is enabled but has missing or invalid fields. Salt values are never included in error messages.
func ValidateOutputName ¶ added in v0.1.10
ValidateOutputName checks that an output name is safe for use in metric labels, log messages, and YAML keys. Returns an error if the name is empty, too long, starts with an underscore (reserved), or contains characters outside [a-zA-Z0-9_-].
ValidateOutputName is called by outputconfig.Load for YAML-sourced output names. Programmatic names (via WithNamedOutput) are not validated because auto-generated names may contain characters outside the YAML-safe set (e.g. "webhook:host:port").
func ValidateTaxonomy ¶
ValidateTaxonomy checks the taxonomy for internal consistency. It verifies version bounds, category-event references, severity ranges, field overlaps, reserved field names, and sensitivity label validity. Returns an error wrapping ErrTaxonomyInvalid containing all problems found, with deterministic output. Callers MUST use errors.Is to test for ErrTaxonomyInvalid. When any consumer-controlled identifier (category name, event type key, field name, or sensitivity label name) violates [taxonomyNamePattern] or exceeds [maxTaxonomyNameLen], the returned error additionally wraps ErrInvalidTaxonomyName.
func VerifyHMAC ¶
VerifyHMAC verifies that the HMAC value matches the payload. The hmacValue MUST be lowercase hex-encoded (as produced by ComputeHMAC); uppercase hex is rejected to avoid the two-valid-encodings footgun.
Returns (true, nil) for a valid match, (false, nil) for a valid- format-but-wrong-digest, (false, err) for parameter or input errors. Structural rejects (empty, wrong length, non-hex) wrap both ErrValidation and ErrHMACMalformed and happen BEFORE the constant-time compare — malformed inputs are pre- authentication and not timing-sensitive (#483).
func WrapUnknownFieldError ¶ added in v0.1.10
WrapUnknownFieldError checks if err is (or wraps) a yaml.UnknownFieldError from goccy/go-yaml's DisallowUnknownField option and, if so, appends a "(valid: ...)" suffix listing the sorted YAML field names from target. Returns err unchanged when the error is not an unknown-field error.
Discrimination uses errors.As against the public type re-exported by github.com/goccy/go-yaml (v1.18.0+) — not string matching against upstream wording — so an upstream message rephrasing cannot cause silent regression.
target must be a struct or pointer to struct. The function extracts YAML tag names via reflection — no manual field lists needed.
func WriteJSONBytes ¶ added in v0.1.12
WriteJSONBytes is the []byte-input counterpart to WriteJSONString. Used by output backends (notably loki) that accumulate pre-serialised event lines as []byte and need to embed them as JSON string values on the wire. Avoiding the `string(b)` conversion at the call site eliminates one heap allocation per event on the drain hot path (#494/#495).
Behaviour is identical to WriteJSONString — the input is treated as UTF-8 bytes, escaped per RFC 7159, and emitted surrounded by double quotes. Multibyte sequences are preserved; ASCII control bytes and the JSON metacharacters (`"`, `\`, `<`, `>`, `&`) are \uXXXX / backslash-escaped.
func WriteJSONString ¶
WriteJSONString writes the JSON-encoded form of s directly to buf, producing byte-for-byte identical output to encoding/json.Marshal for string values. This includes HTML-safe escaping of <, >, and &, and JavaScript-safe escaping of U+2028/U+2029 line/paragraph separators. Invalid UTF-8 is replaced with \ufffd.
Writing directly to the buffer eliminates the per-call allocation that json.Marshal incurs for its return value.
WriteJSONString is exported for use by output modules (e.g. loki) that construct JSON payloads and need allocation-free string escaping.
Types ¶
type Auditor ¶ added in v0.1.10
type Auditor struct {
// contains filtered or unexported fields
}
Auditor is the core type. It validates events against a registered Taxonomy, filters by category and per-event overrides, and delivers events asynchronously to configured Output destinations.
The library uses log/slog for internal diagnostics (buffer drops, serialisation failures, output write errors). Consumers can configure the slog default handler to control this output.
An Auditor is safe for concurrent use by multiple goroutines.
func New ¶ added in v0.1.10
New creates a new Auditor from the given options.
Required options (unless WithDisabled is applied):
- WithTaxonomy — the event taxonomy. Missing → error.
- WithAppName — the application name. Missing → ErrAppNameRequired.
- WithHost — the host identifier. Missing → ErrHostRequired.
The app_name and host requirements match the [outputconfig.Load] YAML-path contract so that programmatic and declarative construction produce equally complete framework fields.
Defaults are: queue=10,000, shutdown=5s, validation=strict. Pass tuning options like WithQueueSize, WithShutdownTimeout, WithValidationMode, or WithOmitEmpty to override.
When WithDisabled is applied, New returns a valid no-op auditor without requiring a taxonomy, app name, or host. All Auditor.AuditEvent calls return nil immediately without validation or delivery. Methods that require a taxonomy (Auditor.EnableCategory, etc.) return ErrDisabled.
Example ¶
package main
import (
"bytes"
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
// Create a stdout output that writes to a buffer for this example.
var buf bytes.Buffer
stdout, err := audit.NewStdoutOutput(audit.StdoutConfig{Writer: &buf})
if err != nil {
log.Fatal(err)
}
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{
"write": {Events: []string{"user_create"}},
},
Events: map[string]*audit.EventDef{
"user_create": {Required: []string{"outcome", "actor_id"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
// Synchronous delivery — single-event docs example with no
// async drain goroutine. Avoids a CI flake where loaded
// runners can starve the drain goroutine past the default 5s
// shutdown timeout, leaving it runnable when goleak runs.
audit.WithSynchronousDelivery(),
audit.WithOutputs(stdout),
)
if err != nil {
log.Fatal(err)
}
// Emit an event — synchronous delivery means the buffer is
// populated before AuditEvent returns; no Close-before-assert
// ceremony is needed.
if err := auditor.AuditEvent(audit.NewEvent("user_create", audit.Fields{
"outcome": "success",
"actor_id": "alice",
})); err != nil {
log.Fatal(err)
}
// Close is still called for symmetry; in synchronous mode it is
// effectively a no-op for delivery and only closes outputs.
if err := auditor.Close(); err != nil {
log.Fatal(err)
}
// The buffer now contains the JSON-serialised event.
fmt.Println("has event_type:", bytes.Contains(buf.Bytes(), []byte(`"event_type":"user_create"`)))
fmt.Println("has actor_id:", bytes.Contains(buf.Bytes(), []byte(`"actor_id":"alice"`)))
}
Output: has event_type: true has actor_id: true
func (*Auditor) AuditEvent ¶ added in v0.1.10
AuditEvent validates and enqueues a typed audit event. Use generated event builders from audit-gen for compile-time field safety, or NewEvent for dynamic event construction.
AuditEvent returns ErrQueueFull if the async buffer is at capacity (the event is dropped), ErrClosed if the auditor has been closed, or a descriptive error for validation failures. If the event's category is globally disabled (and no per-event override enables it), the event is silently discarded without error.
AuditEvent is a convenience wrapper around Auditor.AuditEventContext with context.Background. Prefer Auditor.AuditEventContext when you have a request-scoped context (e.g. from an HTTP handler) — it honours cancellation and deadlines at the well-defined boundary points in the audit pipeline.
Example ¶
package main
import (
"bytes"
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
var buf bytes.Buffer
stdout, err := audit.NewStdoutOutput(audit.StdoutConfig{Writer: &buf})
if err != nil {
log.Fatal(err)
}
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{"write": {Events: []string{"doc_create"}}},
Events: map[string]*audit.EventDef{
"doc_create": {Required: []string{"outcome"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
// Synchronous delivery — see ExampleNew for rationale.
audit.WithSynchronousDelivery(),
audit.WithOutputs(stdout),
)
if err != nil {
log.Fatal(err)
}
if err = auditor.AuditEvent(audit.NewEvent("doc_create", audit.Fields{"outcome": "success"})); err != nil {
fmt.Println("audit error:", err)
return
}
if err = auditor.Close(); err != nil {
log.Fatal(err)
}
// The event is now in the buffer as a JSON line.
fmt.Println("has event_type:", bytes.Contains(buf.Bytes(), []byte(`"event_type":"doc_create"`)))
fmt.Println("has outcome:", bytes.Contains(buf.Bytes(), []byte(`"outcome":"success"`)))
}
Output: has event_type: true has outcome: true
func (*Auditor) AuditEventContext ¶ added in v0.1.12
AuditEventContext is the context.Context-aware variant of Auditor.AuditEvent. The context is checked at well-defined cancellation points — at the top of the validate / enqueue / sync- deliver path and between fan-out outputs in synchronous-delivery mode — but is NOT threaded into individual [Output.Write] calls; once an event is enqueued for the drain goroutine, it is no longer cancellable. See database/sql.QueryContext for the analogous pattern (ctx checked at boundaries, not mid-syscall).
When ctx is context.Background (or any context whose Done channel is nil), AuditEventContext takes the same fast path as the legacy Auditor.AuditEvent — single nil-check, no extra select, no measurable overhead.
When ctx is cancelled or its deadline expires before the event is queued, AuditEventContext returns ctx.Err (context.Canceled or context.DeadlineExceeded), records a buffer-drop metric via [Metrics.RecordBufferDrop], and emits a diagnostic-log warn line so operators can distinguish caller-driven drops from queue-full drops.
Precedence: a disabled auditor (constructed with WithDisabled) short-circuits BEFORE the ctx check — calls return nil regardless of ctx state, matching the pre-#600 contract that disabled auditors are a silent no-op.
Race: when ctx is cancelled AND the async buffer is full at the same instant, Go's `select` picks either the cancel branch or the queue-full branch nondeterministically. The caller may see either ctx.Err() or ErrQueueFull; in both cases the entry is returned to the pool and the buffer-drop metric is incremented. Callers that need to distinguish should inspect the error with errors.Is.
Example ¶
ExampleAuditor_AuditEventContext demonstrates the ctx-aware emit path (#600). When a request-scoped context is cancelled or its deadline expires before the audit pipeline accepts the event, the call returns the ctx error and the event is dropped — useful for graceful-shutdown scenarios where you want to abandon partially completed work without waiting for the audit buffer to flush.
Note: trace-correlation plumbing (e.g. extracting a `trace_id` from ctx and emitting it as a framework field) is deferred to a post-v1.0 follow-up issue — for now consumers extract correlation values from ctx in their EventBuilder or before calling AuditEventContext.
package main
import (
"context"
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{"write": {Events: []string{"doc_create"}}},
Events: map[string]*audit.EventDef{
"doc_create": {Required: []string{"outcome"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
audit.WithSynchronousDelivery(),
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = auditor.Close() }()
// Already-cancelled ctx — call returns context.Canceled.
ctx, cancel := context.WithCancel(context.Background())
cancel()
emitErr := auditor.AuditEventContext(ctx, audit.NewEvent("doc_create", audit.Fields{
"outcome": "success",
}))
fmt.Println("emit error:", emitErr)
}
Output: emit error: context canceled
func (*Auditor) ClearOutputRoute ¶ added in v0.1.10
ClearOutputRoute removes the per-output event route for the named output, causing it to receive all globally-enabled events.
ClearOutputRoute is safe for concurrent use with event delivery.
func (*Auditor) Close ¶ added in v0.1.10
Close shuts down the auditor gracefully. Close MUST be called when the auditor is no longer needed; failing to call Close leaks the drain goroutine and loses all buffered events.
Close signals the drain goroutine to stop, waits up to the configured WithShutdownTimeout (default 5s) for pending events to flush, then closes all outputs in parallel.
Close is idempotent -- subsequent calls return nil (or the same error if an output failed to close on the first call).
Example ¶
package main
import (
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{"write": {Events: []string{"doc_create"}}},
Events: map[string]*audit.EventDef{
"doc_create": {Required: []string{"outcome"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
audit.WithSynchronousDelivery(),
)
if err != nil {
log.Fatal(err)
}
// Best practice: defer Close immediately after creation.
defer func() {
if err := auditor.Close(); err != nil {
log.Printf("audit close: %v", err)
}
}()
fmt.Println("auditor will be closed on function exit")
}
Output: auditor will be closed on function exit
func (*Auditor) DisableCategory ¶ added in v0.1.10
DisableCategory disables all events in the named category. The category MUST exist in the registered taxonomy. Per-event overrides via Auditor.EnableEvent take precedence over category state.
func (*Auditor) DisableEvent ¶ added in v0.1.10
DisableEvent disables a specific event type regardless of its category's state. The event type MUST exist in the registered taxonomy. Per-event overrides take precedence over category state.
func (*Auditor) EnableCategory ¶ added in v0.1.10
EnableCategory enables all events in the named category. The category MUST exist in the registered taxonomy. Per-event overrides via Auditor.DisableEvent take precedence over category state.
Example ¶
package main
import (
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{
"read": {Events: []string{"doc_read"}},
"write": {Events: []string{"doc_create"}},
},
Events: map[string]*audit.EventDef{
"doc_read": {Required: []string{"outcome"}},
"doc_create": {Required: []string{"outcome"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
audit.WithSynchronousDelivery(),
)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := auditor.Close(); err != nil {
log.Printf("audit close: %v", err)
}
}()
// "read" category is disabled by default. Enable it at runtime.
if err := auditor.EnableCategory("read"); err != nil {
fmt.Println("enable error:", err)
return
}
fmt.Println("read category enabled")
}
Output: read category enabled
func (*Auditor) EnableEvent ¶ added in v0.1.10
EnableEvent enables a specific event type regardless of its category's state. The event type MUST exist in the registered taxonomy. Per-event overrides take precedence over category state.
func (*Auditor) Handle ¶ added in v0.1.10
func (a *Auditor) Handle(eventType string) (*EventHandle, error)
Handle returns an EventHandle for the named event type. Call once at startup (for example during DI wiring), cache the returned handle, and emit via EventHandle.Audit per event — this avoids the per-call basicEvent allocation that NewEvent incurs via interface escape. Returns ErrHandleNotFound if the event type is not registered. For event types known at compile time, prefer generated typed builders from audit-gen.
When the auditor was constructed with WithDisabled, Handle returns a no-op EventHandle for any event type without validating the taxonomy — all subsequent Audit calls on the handle are silent no-ops, matching Auditor.AuditEvent on a disabled auditor. Metadata accessors (EventHandle.Description, EventHandle.Categories, EventHandle.FieldInfoMap) on a no-op handle return zero values.
For a side-by-side comparison of NewEvent, EventHandle, and generated builders with examples and benchmark numbers, see docs/event-emission-paths.md.
Example ¶
ExampleAuditor_Handle demonstrates the EventHandle metadata accessors (#597) — Description, Categories with severity, and FieldInfoMap with required/optional flags. Middleware and other consumers can introspect a handle without constructing an event.
package main
import (
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
sev := 7
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{
"security": {Events: []string{"auth_failure"}, Severity: &sev},
},
Events: map[string]*audit.EventDef{
"auth_failure": {
Description: "Failed authentication attempt",
Required: []string{"outcome", "actor_id"},
},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
audit.WithSynchronousDelivery(),
)
if err != nil {
log.Fatal(err)
}
defer func() { _ = auditor.Close() }()
h, err := auditor.Handle("auth_failure")
if err != nil {
fmt.Println("handle error:", err)
return
}
fmt.Println("description:", h.Description())
for _, c := range h.Categories() {
if c.Severity != nil {
fmt.Printf("category: %s (severity=%d)\n", c.Name, *c.Severity)
}
}
fmt.Println("outcome required:", h.FieldInfoMap()["outcome"].Required)
}
Output: description: Failed authentication attempt category: security (severity=7) outcome required: true
func (*Auditor) IsCategoryEnabled ¶ added in v0.1.10
IsCategoryEnabled reports whether events in the named category would be delivered. This accounts for both category-level state and per-event overrides. Returns false for disabled auditors or unknown categories.
func (*Auditor) IsDisabled ¶ added in v0.1.10
IsDisabled reports whether the auditor is a no-op (created with WithDisabled). Safe for concurrent use.
func (*Auditor) IsEventEnabled ¶ added in v0.1.10
IsEventEnabled reports whether the named event type would be delivered. This accounts for category state, per-event overrides, and the global filter. Returns false for disabled auditors or unknown event types.
func (*Auditor) IsSynchronous ¶ added in v0.1.10
IsSynchronous reports whether the auditor delivers events inline within Auditor.AuditEvent (created with WithSynchronousDelivery). Safe for concurrent use.
func (*Auditor) LastDeliveryAge ¶ added in v0.1.12
LastDeliveryAge returns the duration since the named output last successfully delivered a batch. Zero is returned when:
- the auditor is disabled;
- outputName is not configured (caller can disambiguate via Auditor.OutputNames);
- the output does not implement LastDeliveryReporter — telemetry is unavailable;
- the output has not yet completed a successful delivery.
All four cases collapse to the same return value (`0`) and are not distinguishable by the caller without a separate call to Auditor.OutputNames or Auditor.IsDisabled. Treat `0` as "no signal" for staleness purposes — a /healthz handler SHOULD consider an output healthy until it has produced at least one successful delivery, otherwise newly-started auditors fail their liveness probe before any traffic arrives.
We chose zero-as-sentinel over a `(time.Duration, bool)` tuple because every realistic /healthz handler wants to treat "no-signal" identically to "fresh" — both pass the probe — so a tuple return would force every caller to write the same `if !ok || age <= threshold` boilerplate. Use Auditor.OutputNames or Auditor.IsDisabled when an operator dashboard genuinely needs to disambiguate the four cases.
Negative durations: wall-clock time can step backwards on NTP correction, so time.Since applied to a stored timestamp may return a negative time.Duration. The canonical comparison `age > threshold` evaluates negative ages as "fresh" and passes the probe — accidentally correct, but documented for callers who maintain dashboards or alerts on the raw return value.
Designed for /healthz handlers. Iterating Auditor.OutputNames and calling LastDeliveryAge against a staleness threshold flips the probe to unhealthy when an output silently stops delivering.
The age is computed against time.Now at call time using wall-clock arithmetic. Wall-clock means the value can jump on system time changes; /healthz thresholds SHOULD be ≥ 10 s to absorb sub-second NTP slews. The reference example in examples/18-health-endpoint uses 30 s.
Concurrency: safe to call from any goroutine. Reads are atomic; no mutex.
func (*Auditor) Logger ¶ added in v0.1.12
Logger returns the diagnostic logger configured via WithDiagnosticLogger, or slog.Default if none was supplied. Useful for library wrappers that want to share the auditor's logger across components.
The logger is fixed at construction; runtime swap is not supported (the prior SetLogger API was removed in #696 — direct-Go consumers who want to redirect diagnostics should rebuild the auditor).
func (*Auditor) MustHandle ¶ added in v0.1.10
func (a *Auditor) MustHandle(eventType string) *EventHandle
MustHandle returns an EventHandle for the named event type. It panics with an error wrapping ErrHandleNotFound if the event type is not registered. Use Auditor.Handle to receive the error instead of panicking.
For a side-by-side comparison of NewEvent, EventHandle, and generated builders with examples and benchmark numbers, see docs/event-emission-paths.md.
Example ¶
package main
import (
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{"write": {Events: []string{"doc_create"}}},
Events: map[string]*audit.EventDef{
"doc_create": {Required: []string{"outcome"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
audit.WithSynchronousDelivery(), // deterministic for goleak under loaded CI runners
)
if err != nil {
log.Fatal(err)
}
defer func() {
if closeErr := auditor.Close(); closeErr != nil {
log.Printf("audit close: %v", closeErr)
}
}()
// Get a handle for zero-allocation audit calls.
docCreate := auditor.MustHandle("doc_create")
if err = docCreate.Audit(audit.Fields{"outcome": "success"}); err != nil {
fmt.Println("audit error:", err)
return
}
fmt.Println("handle event type:", docCreate.EventType())
}
Output: handle event type: doc_create
func (*Auditor) OutputNames ¶ added in v0.1.10
OutputNames returns a sorted list of all configured output names. Safe for concurrent use. Returns nil for disabled auditors with no outputs.
func (*Auditor) OutputRoute ¶ added in v0.1.10
func (a *Auditor) OutputRoute(outputName string) (EventRoute, error)
OutputRoute returns a copy of the current per-output event route for the named output. An unknown output name returns an error.
func (*Auditor) QueueCap ¶ added in v0.1.10
QueueCap returns the configured async intake queue capacity. Returns 0 for disabled or synchronous auditors. Safe for concurrent use.
func (*Auditor) QueueLen ¶ added in v0.1.10
QueueLen returns the number of events currently queued in the async intake queue. Returns 0 for disabled or synchronous auditors. Safe for concurrent use.
func (*Auditor) SetOutputRoute ¶ added in v0.1.10
func (a *Auditor) SetOutputRoute(outputName string, route *EventRoute) error
SetOutputRoute sets the per-output event route for the named output. The route is validated against the taxonomy; unknown categories or event types return an error. Mixed include/exclude routes return an error. An unknown output name returns an error.
SetOutputRoute is safe for concurrent use with event delivery.
Example ¶
package main
import (
"bytes"
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
var buf bytes.Buffer
out, err := audit.NewStdoutOutput(audit.StdoutConfig{Writer: &buf})
if err != nil {
log.Fatal(err)
}
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{
"write": {Events: []string{"user_create"}},
"security": {Events: []string{"auth_failure"}},
},
Events: map[string]*audit.EventDef{
"user_create": {Required: []string{"outcome"}},
"auth_failure": {Required: []string{"outcome"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
audit.WithNamedOutput(out, audit.WithRoute(&audit.EventRoute{})),
audit.WithSynchronousDelivery(),
)
if err != nil {
log.Fatal(err)
}
defer func() {
if closeErr := auditor.Close(); closeErr != nil {
log.Printf("audit close: %v", closeErr)
}
}()
// Restrict output to security events only at runtime.
if err := auditor.SetOutputRoute("stdout", &audit.EventRoute{
IncludeCategories: []string{"security"},
}); err != nil {
fmt.Println("route error:", err)
return
}
fmt.Println("route set to security only")
}
Output: route set to security only
type CEFFormatter ¶
type CEFFormatter struct {
// SeverityFunc maps event types to CEF severity. If nil,
// taxonomy-defined severity is used via [EventDef.ResolvedSeverity]
// (precomputed at taxonomy registration time and guaranteed to be
// in the valid range 0-10 — no per-event clamp).
//
// When non-nil, return values are clamped to 0-10 on every event
// to protect against out-of-range consumer returns. Set
// SeverityFunc only to override the taxonomy.
SeverityFunc func(eventType string) int
// DescriptionFunc maps event types to human-readable CEF
// descriptions. If nil, [EventDef.Description] is used when
// non-empty, falling back to the event type name.
DescriptionFunc func(eventType string) string
// FieldMapping maps audit field names to CEF extension keys. If nil,
// [DefaultCEFFieldMapping] is used. If non-nil, entries are merged
// with [DefaultCEFFieldMapping]: consumer entries override matching
// defaults, and defaults not present in FieldMapping remain active.
// Unmapped fields use their original audit field name as the
// extension key.
//
// To opt out of a default mapping for a specific field, use either
// of these supported patterns (pick whichever reads better at the
// call site):
//
// 1. Empty-string opt-out — pass the audit field name mapped to
// "" to explicitly drop the default. The field is then emitted
// with the raw audit field name as the CEF extension key:
//
// f := &audit.CEFFormatter{FieldMapping: map[string]string{
// "actor_id": "", // drop default actor_id → suser
// }}
//
// 2. Self-map — pass the audit field name mapped to itself. Same
// on-wire result (the raw audit field name becomes the
// extension key), but keeps the intent explicit at the call
// site:
//
// f := &audit.CEFFormatter{FieldMapping: map[string]string{
// "actor_id": "actor_id", // emit as actor_id=... not suser=...
// }}
FieldMapping map[string]string
// Vendor is the CEF header vendor field (e.g. "AxonOps"). If empty,
// the vendor position in the header is blank but the pipe
// delimiters are preserved. SHOULD be non-empty for
// standard-compliant CEF output.
Vendor string
// Product is the CEF header product field (e.g. "SchemaRegistry").
// If empty, the product position is blank. SHOULD be non-empty.
Product string
// Version is the CEF header product version field (e.g. "1.0").
// If empty, the version position is blank. SHOULD be non-empty.
Version string
// OmitEmpty controls whether zero-value fields are omitted from
// extensions.
OmitEmpty bool
// contains filtered or unexported fields
}
CEFFormatter serialises audit events in Common Event Format (CEF).
The output format is:
CEF:0|{Vendor}|{Product}|{Version}|{eventType}|{description}|{severity}|{extensions}
Header fields use pipe (|) as a delimiter. Extension values use key=value pairs separated by spaces.
Escaping ¶
Header fields escape backslash and pipe: \ -> \\, | -> \|. Newlines and carriage returns in headers are replaced with spaces. Extension values escape backslash, equals, newline, and CR: \ -> \\, = -> \=, newline -> \n (literal), CR -> \r (literal). All remaining C0 control characters (0x00-0x1F) are stripped.
Severity ¶
Severity is determined by [CEFFormatter.SeverityFunc] if set. If nil, the taxonomy-defined severity is used via EventDef.ResolvedSeverity: event Severity (if non-nil) → first category Severity in alphabetical order (if non-nil) → 5. Values are clamped to the valid CEF range 0-10.
Concurrency ¶
Safe for concurrent use by multiple goroutines, per the Formatter contract. Lazy field-mapping resolution is guarded by sync.Once, and per-call buffers are leased from a package-level sync.Pool. The noCopy marker prevents accidental copies that would duplicate the sync.Once state.
func (*CEFFormatter) Format ¶
func (cf *CEFFormatter) Format(ts time.Time, eventType string, fields Fields, def *EventDef, opts *FormatOptions) ([]byte, error)
Format serialises a single audit event as a CEF line. The returned slice is owned by the caller (defensive copy from the pooled buffer).
Internal callers in the drain pipeline use [CEFFormatter.formatBuf] to obtain the leased buffer directly and skip the copy.
func (*CEFFormatter) SetFrameworkFields ¶
func (cf *CEFFormatter) SetFrameworkFields(appName, host, timezone string, pid int)
SetFrameworkFields stores auditor-wide framework metadata for emission in every CEF event. Called once at construction time.
type CategoryDef ¶
type CategoryDef struct {
// Severity is the default CEF severity (0-10) for all events in
// this category. Nil means not set — events inherit the global
// default (5). A non-nil pointer to 0 means explicitly severity 0.
Severity *int
// Events lists the event type names belonging to this category.
Events []string
}
CategoryDef defines a taxonomy category with its member events and optional default severity.
type CategoryInfo ¶
type CategoryInfo struct {
Severity *int // category-level severity, nil if not set
Name string // category name, e.g., "write"
}
CategoryInfo describes a category an event type belongs to.
type DeliveryReporter ¶
type DeliveryReporter interface {
ReportsDelivery() bool
}
DeliveryReporter is an optional interface that Output implementations may satisfy to indicate they handle their own delivery metrics reporting. When satisfied and [DeliveryReporter.ReportsDelivery] returns true, the core auditor skips its default per-event [Metrics.RecordDelivery] calls for that output — the output is responsible for calling them after actual delivery.
Not to be confused with LastDeliveryReporter — that interface reports a single timestamp for /healthz staleness probes; this one controls per-event metrics dispatch (success / error / filtered).
type DestinationKeyer ¶
type DestinationKeyer interface {
DestinationKey() string
}
DestinationKeyer is an optional interface that Output implementations MAY satisfy to enable duplicate destination detection at construction time. When two outputs return the same key from DestinationKey, WithOutputs and WithNamedOutput return an error.
Returning an empty string from DestinationKey opts out of duplicate detection for that output.
Key format conventions by output type:
- File: absolute filesystem path
- Syslog: network address (host:port)
- Webhook: full URL
Outputs that do not implement this interface (e.g. StdoutOutput) are silently skipped during destination dedup.
type Event ¶
type Event interface {
// EventType returns the event type name (e.g., "user_create").
EventType() string
// Fields returns the event's field key-value pairs.
Fields() Fields
// Description returns the event's human-readable description from
// the taxonomy. Empty string for events constructed via [NewEvent]
// or [NewEventKV] (which are taxonomy-agnostic).
Description() string
// Categories returns the categories this event belongs to, with
// per-category severity. nil for events constructed via [NewEvent]
// or [NewEventKV]. Read-only; callers MUST NOT mutate the slice
// or its `*Severity` pointers.
Categories() []CategoryInfo
// FieldInfoMap returns the per-field metadata (name, sensitivity
// labels, required flag) keyed by field name. nil for events
// constructed via [NewEvent] or [NewEventKV]. Generated builders
// also expose a typed `FieldInfo() <Event>Fields` method for
// compile-time field access. Read-only; callers MUST NOT mutate
// the map or its values.
FieldInfoMap() map[string]FieldInfo
}
Event represents a typed audit event with its event type, fields, and taxonomy metadata.
Implementations are generated by audit-gen for compile-time field safety. For dynamic use without code generation, see NewEvent — note that basic events are taxonomy-agnostic and return zero values for the metadata methods (Description / Categories / FieldInfoMap); pair them with Auditor.Handle when consumers need metadata access.
The metadata methods support middleware and other consumers that receive an Event and need to introspect its declared shape (e.g. to redact sensitive fields, route by severity, or attach description text). Generated builders carry richer metadata than the interface exposes — concrete types add a typed `FieldInfo() <Event>Fields` method for compile-time field access; the interface's [FieldInfoMap] method returns the same data as a keyed map for dynamic introspection. This mirrors stdlib reflect.Type.Field (ordered) versus reflect.Type.FieldByName (keyed) — both shapes coexist and serve different access patterns.
Mutation contract ¶
The values returned by [Event.Categories] and [Event.FieldInfoMap] are READ-ONLY. Callers MUST NOT mutate the returned slice, map, or any pointer-typed field within them (e.g. `CategoryInfo.Severity`). Implementations are free to share cached values across calls — the receiver of an Event cannot tell whether it holds a snapshot or the live taxonomy state, so it must treat the data as immutable. To consume metadata mutably, copy first.
func MustNewEventKV ¶ added in v0.1.12
MustNewEventKV is like NewEventKV but panics if keysAndValues is invalid. Intended for literal call sites (tests, examples, package-level initialisation) where the input is known at compile time and two-line error handling is pure noise:
logger.AuditEvent(audit.MustNewEventKV("user_create",
"outcome", "success",
"actor_id", "alice",
))
Mirrors regexp.MustCompile / [template.Must] — the canonical Go pattern for configuration-time construction with compile-time-known input. Dynamic-input callers should use NewEventKV instead.
func NewEvent ¶
NewEvent creates an untyped audit event. Prefer generated typed builders from audit-gen for compile-time field safety. This function exists for consumers who construct events dynamically or do not use code generation.
The returned Event is taxonomy-agnostic: [Event.Description], [Event.Categories], and [Event.FieldInfoMap] return zero values (empty string and nil). Consumers that need metadata access (e.g. middleware that redacts based on field sensitivity labels) should use Auditor.Handle — the handle resolves taxonomy metadata once at construction and exposes it via parallel methods. Generated typed builders also carry full metadata.
Allocation note: each call allocates one basicEvent on the heap because the returned Event interface value forces the pointer to escape. For high-throughput callers whose event type is known but dynamic (from configuration, a database, or a plugin), prefer EventHandle — it pre-validates the event type once and emits without the per-call basicEvent allocation. See Auditor.Handle and Auditor.MustHandle.
For a side-by-side comparison of NewEvent, EventHandle, and generated builders with examples and benchmark numbers, see docs/event-emission-paths.md.
func NewEventKV ¶
NewEventKV creates an audit event from alternating key-value pairs, following the log/slog convention:
ev, err := audit.NewEventKV("user_create", "outcome", "success", "actor_id", "alice")
if err != nil {
// Programmer error: odd number of args or non-string key.
return err
}
NewEventKV returns a non-nil error wrapping ErrValidation if keysAndValues has an odd number of elements or any key is not a string. For literal call sites (tests, examples, known-good kv pairs) use MustNewEventKV instead — it preserves the pre-#590 panic-on-programmer-error contract and reads as a single-line expression at the call site.
The returned Event is taxonomy-agnostic — see the NewEvent godoc for the metadata-method behaviour and alternatives that carry full metadata.
Allocation note: NewEventKV allocates the intermediate Fields map plus the basicEvent returned by NewEvent — two heap allocations per call (plus any-boxing of non-string values). For high-throughput callers, prefer generated typed builders (zero caller-side allocations after warm-up) or EventHandle for dynamic event types.
For a side-by-side comparison of NewEvent, EventHandle, and generated builders with examples and benchmark numbers, see docs/event-emission-paths.md.
type EventBuilder ¶
type EventBuilder func(hints *Hints, transport *TransportMetadata) (eventType string, fields Fields, skip bool)
EventBuilder is a callback that transforms per-request Hints and TransportMetadata into an audit event. The middleware calls it after the handler returns (or panics).
Return values:
- eventType: the taxonomy event type name to pass to Auditor.AuditEvent
- fields: the audit event fields
- skip: if true, no audit event is emitted for this request
type EventDef ¶
type EventDef struct {
// Categories lists the taxonomy categories this event belongs to
// (e.g. ["write"], ["security", "access"]). Derived from the
// [Taxonomy.Categories] map during parsing — not set by consumers.
// Sorted alphabetically. May be empty for uncategorised events.
Categories []string
// Description is an optional human-readable explanation of what
// this event type represents. It is informational metadata only
// — it has no effect on validation, routing, or serialisation.
// When present, [audit-gen] emits it as a Go comment above the
// generated constant. Also used as the default CEF description.
Description string
// Severity is the event-level CEF severity (0-10). Nil means
// inherit from the category. A non-nil pointer to 0 means
// explicitly severity 0. Resolution: event → category → 5.
Severity *int
// Required lists field names that must be present in every
// [Auditor.AuditEvent] call for this event type. Missing required
// fields always produce an error regardless of validation mode.
Required []string
// Optional lists field names that may be present. In strict
// validation mode, any field not in Required or Optional
// produces an error.
Optional []string
// FieldLabels maps field names to their resolved sensitivity labels,
// represented as a set (map key = label name, value always struct{}).
// Populated at taxonomy registration time from all three label
// sources: explicit per-event annotation, global field name mapping,
// and regex patterns. Nil when no sensitivity config is defined.
// Read-only after construction — consumers MUST NOT modify this map.
FieldLabels map[string]map[string]struct{}
// FieldTypes maps custom (non-reserved) field names to their Go
// type name as declared in the taxonomy YAML `type:` annotation
// (e.g., "string", "int", "int64", "float64", "bool", "time.Time",
// "time.Duration"). Empty or missing entry defaults to "string".
// Reserved standard fields are NOT in this map — their Go type
// is authoritative from [standardFieldGoType]. Consumed by
// [cmd/audit-gen] to emit typed `Set<Field>(v Type)` setters.
// Read-only after construction.
FieldTypes map[string]string
// contains filtered or unexported fields
}
EventDef defines a single audit event type in the taxonomy.
func (*EventDef) ResolvedSeverity ¶
ResolvedSeverity returns the effective severity for this event type. The value is precomputed during taxonomy registration and is always in the range 0-10. Resolution chain: event Severity (if non-nil) → first category Severity in alphabetical order (if non-nil) → 5. For events in multiple categories, set event-level Severity to avoid depending on alphabetical category ordering.
type EventHandle ¶
type EventHandle struct {
// contains filtered or unexported fields
}
EventHandle is a pre-validated reference to a registered event type. It is the recommended emission path when the event type is known at startup but not at compile time — for example, event type names pulled from configuration, a database, or a plugin registry.
Obtain one handle per event type at startup via Auditor.Handle (returns an error) or Auditor.MustHandle (panics on unknown event type), cache it, and emit via EventHandle.Audit on each event. The handle validates the event type once at construction; per-event calls go straight into the audit pipeline without re-validating the name and without the basicEvent heap allocation paid by NewEvent.
For event types known at compile time, prefer generated typed builders from audit-gen — they add compile-time field safety and implement FieldsDonor for the zero-allocation drain-side fast path.
For a side-by-side comparison of NewEvent, EventHandle, and generated builders with examples and benchmark numbers, see docs/event-emission-paths.md.
func (*EventHandle) Audit ¶
func (e *EventHandle) Audit(fields Fields) error
Audit emits an audit event using this handle's bound event type. Zero per-event caller-side allocations (no basicEvent construction, no Event interface escape) — the internal audit path is called directly. For the zero-drain-side-allocations fast path (the FieldsDonor contract), emit a generated typed builder directly via Auditor.AuditEvent.
Audit is a convenience wrapper around EventHandle.AuditContext with context.Background. Prefer the ctx-aware variant when you have a request-scoped context (e.g. from an HTTP handler) — see Auditor.AuditEventContext for the cancellation-point semantics.
func (*EventHandle) AuditContext ¶ added in v0.1.12
func (e *EventHandle) AuditContext(ctx context.Context, fields Fields) error
AuditContext is the context.Context-aware variant of EventHandle.Audit. The handle's bound event type is preserved; only the cancellation-aware path differs. See Auditor.AuditEventContext for the ctx-checking semantics — AuditContext shares the same internal core via auditInternalCtx.
func (*EventHandle) AuditEvent ¶ added in v0.1.2
func (e *EventHandle) AuditEvent(evt Event) error
AuditEvent emits an Event (typically a generated builder) using the auditor bound to this handle. Unlike EventHandle.Audit, the event type is taken from evt.EventType(), not from the handle's bound type — the handle provides the auditor binding only. This method accepts any Event implementation, making it composable with code-generated typed builders.
Allocation note: this method takes the standard Auditor.AuditEvent path. Allocation behaviour depends on evt: generated builders that implement FieldsDonor reach 0 drain-side allocs per event; plain Event values (including NewEvent output) pay the defensive-copy cost described in docs/performance.md.
AuditEvent is a convenience wrapper around EventHandle.AuditEventContext with context.Background. Prefer the ctx-aware variant when you have a request-scoped context.
func (*EventHandle) AuditEventContext ¶ added in v0.1.12
func (e *EventHandle) AuditEventContext(ctx context.Context, evt Event) error
AuditEventContext is the context.Context-aware variant of EventHandle.AuditEvent. The same auditor instance is used; ctx is threaded through to Auditor.AuditEventContext.
func (*EventHandle) Categories ¶ added in v0.1.12
func (e *EventHandle) Categories() []CategoryInfo
Categories returns the event type's category memberships with per-category severity. Resolved once at handle construction; cached and shared across calls per the Event interface's read-only contract — callers MUST NOT mutate the slice or its `*Severity` pointers. To consume mutably, copy first.
func (*EventHandle) Description ¶ added in v0.1.12
func (e *EventHandle) Description() string
Description returns the event type's description from the taxonomy. Resolved once at handle construction; cached.
func (*EventHandle) EventType ¶
func (e *EventHandle) EventType() string
EventType returns the event type name this handle represents.
func (*EventHandle) FieldInfoMap ¶ added in v0.1.12
func (e *EventHandle) FieldInfoMap() map[string]FieldInfo
FieldInfoMap returns the event type's per-field metadata keyed by field name. Resolved once at handle construction; cached. The returned map is the cached one; callers MUST NOT mutate it.
type EventMetadata ¶
type EventMetadata struct {
// EventType is the taxonomy event type name (e.g. "user_create").
EventType string
// Severity is the resolved severity (0-10) for this event.
Severity int
// Category is the delivery-specific category. Empty for
// uncategorised events. When an event belongs to multiple
// categories, each delivery pass has a different Category.
Category string
// Timestamp is the wall-clock time recorded at drain time.
Timestamp time.Time
}
EventMetadata carries per-event context for outputs that need structured access to framework fields (e.g., for Loki labels or Elasticsearch index routing). The struct is constructed once per delivery pass in [deliverToOutputs] and passed by value to [MetadataWriter.WriteWithMetadata].
The struct is small (64 bytes on amd64), passed by value, and zero-allocation by design. All fields are read from existing local variables in the drain goroutine.
type EventRoute ¶
type EventRoute struct {
// IncludeCategories lists category names to allow. Events whose
// category is in this list are delivered. Mutually exclusive with
// ExcludeCategories and ExcludeEventTypes.
IncludeCategories []string
// IncludeEventTypes lists event type names to allow. Events whose
// type is in this list are delivered regardless of category.
// Mutually exclusive with ExcludeCategories and ExcludeEventTypes.
IncludeEventTypes []string
// ExcludeCategories lists category names to deny. Events whose
// category is in this list are skipped. Mutually exclusive with
// IncludeCategories and IncludeEventTypes.
ExcludeCategories []string
// ExcludeEventTypes lists event type names to deny. Events whose
// type is in this list are skipped regardless of category.
// Mutually exclusive with IncludeCategories and IncludeEventTypes.
ExcludeEventTypes []string
// MinSeverity sets a minimum severity threshold. Events with
// severity below this value are not delivered. Nil means no
// minimum filter. A non-nil pointer to 0 means "severity >= 0"
// (effectively no filter). Severity filtering is an AND condition
// with category/event type filtering.
MinSeverity *int
// MaxSeverity sets a maximum severity threshold. Events with
// severity above this value are not delivered. Nil means no
// maximum filter. Combined with MinSeverity to create a range.
MaxSeverity *int
// contains filtered or unexported fields
}
EventRoute restricts which events are delivered to a specific output. Routes operate in one of two mutually exclusive modes:
Include mode (allow-list): events are delivered only if their category is in [EventRoute.IncludeCategories] OR their event type is in [EventRoute.IncludeEventTypes]. The two fields form a union.
Exclude mode (deny-list): events are delivered unless their category is in [EventRoute.ExcludeCategories] OR their event type is in [EventRoute.ExcludeEventTypes]. The two fields form a union.
Setting both include and exclude fields on the same route is a bootstrap error. An empty route (all fields nil/empty) delivers all globally-enabled events.
Example (Exclude) ¶
package main
import (
"fmt"
"github.com/axonops/audit"
)
func main() {
// Exclude mode: all events except reads are delivered.
route := audit.EventRoute{
ExcludeCategories: []string{"read"},
}
fmt.Println("empty:", route.IsEmpty())
}
Output: empty: false
Example (Include) ¶
package main
import (
"fmt"
"github.com/axonops/audit"
)
func main() {
// Include mode: only security events are delivered to this output.
route := audit.EventRoute{
IncludeCategories: []string{"security"},
}
fmt.Println("empty:", route.IsEmpty())
}
Output: empty: false
func (*EventRoute) IsEmpty ¶
func (r *EventRoute) IsEmpty() bool
IsEmpty reports whether all route fields are empty, meaning the output receives all globally-enabled events.
type EventStatus ¶ added in v0.1.12
type EventStatus string
EventStatus is the outcome label for a delivery attempt recorded via [Metrics.RecordDelivery]. It is a typed string so consumers can pass it straight to Prometheus / OpenTelemetry label vectors with a zero-cost `string(status)` conversion on the hot path.
The underlying string values are a library contract: they are emitted as-is to downstream metric collectors. Existing consumer-side Prometheus queries and alert rules that match on `"success"` / `"error"` continue to work verbatim.
Adding a new EventStatus value is a minor-version compatible change. Removing or renaming an existing value is breaking.
const ( // EventSuccess records a successful delivery to an output. EventSuccess EventStatus = "success" // EventError records a non-retryable delivery failure. EventError EventStatus = "error" )
Defined EventStatus values. Production code emits only these two; the set is deliberately minimal because other outcome categories (filtered, dropped, validation failure) have dedicated Metrics methods ([Metrics.RecordOutputFiltered], [Metrics.RecordBufferDrop], [Metrics.RecordValidationError]).
type FieldInfo ¶
type FieldInfo struct {
Name string // field name, e.g., "email"
Labels []LabelInfo // sensitivity labels on this field
Required bool // true if the field must be present
}
FieldInfo describes a single field on an audit event type.
type Fields ¶
Fields is the map type for audit event fields. Consumers pass field values as Fields to Auditor.AuditEvent and generated event builders.
Fields is a defined type (not an alias) so it can carry convenience methods. Callers constructing fields from a plain map must convert explicitly: audit.Fields(m).
Comparable pattern: net/url.Values, net/http.Header.
Supported value types ¶
The library guarantees faithful rendering across both built-in formatters (JSON and CEF) for these value types only:
- string
- int, int32, int64
- float64
- bool
- time.Time
- time.Duration
- []string
- map[string]string
- nil (renders as null in JSON; absent in CEF)
Behaviour for values outside this vocabulary depends on the auditor's ValidationMode (configured via WithValidationMode):
- ValidationStrict: Auditor.AuditEvent returns a ValidationError wrapping ErrUnknownFieldType and the event is dropped.
- ValidationWarn: the unsupported value is coerced via fmt.Sprintf("%v", v) and a warning is logged through the diagnostic logger (WithDiagnosticLogger).
- ValidationPermissive: the unsupported value is coerced silently.
Coercion is functional but produces formatter-hostile output for composite types (struct dumps, "{}" for empty maps). Consumers should pass values in the supported vocabulary; the validation mode is a backstop, not a feature.
Reserved standard fields (ReservedStandardFieldNames) carry an additional declared Go type (queryable via ReservedStandardFieldType). WithStandardFieldDefaults enforces that declared type at construction time. Per-event values supplied via Fields are NOT type-checked against the reserved type — only the supported-vocabulary check above runs.
type FieldsDonor ¶ added in v0.1.12
type FieldsDonor interface {
Event
// contains filtered or unexported methods
}
FieldsDonor is an optional extension interface that the audit pipeline checks via type assertion in Auditor.AuditEvent. When an Event also implements FieldsDonor, the auditor takes ownership of the Fields map returned by [Event.Fields] — no defensive copy.
Contract ¶
Implementers MUST NOT mutate or retain the returned Fields after [Event.Fields] is called by the auditor:
- The auditor merges standard-field defaults INTO the returned map and the formatters then read from it across multiple outputs.
- Re-using the same Event instance for a second Auditor.AuditEvent call is undefined behaviour. The first call's defaults remain in the map; subsequent calls would observe stale state.
Sentinel ¶
The unexported [donateFields] sentinel method prevents third-party implementation outside the audit-gen toolchain. Generated builders emit the sentinel; consumer-defined Event types and NewEvent stay on the defensive-copy slow path.
See docs/adr/0001-fields-ownership-contract.md for the design rationale and benchmarks (#497).
Example ¶
ExampleFieldsDonor demonstrates how to verify that an event satisfies the audit.FieldsDonor extension interface — the trigger for the auditor's zero-allocation fast path on the drain side. Generated builders from cmd/audit-gen satisfy this interface via the unexported donateFields() sentinel; no third party can satisfy it (by design).
Events constructed via audit.NewEvent or audit.NewEventKV do NOT satisfy FieldsDonor and stay on the defensive-copy slow path. This is intentional — the donor contract requires a no-mutate, no-retain guarantee that only the audit-gen toolchain enforces.
For the fast-path / slow-path ownership model see https://github.com/axonops/audit/blob/main/docs/performance.md.
package main
import (
"fmt"
"github.com/axonops/audit"
)
func main() {
evt := audit.NewEvent("user_create", audit.Fields{
"outcome": "success",
"actor_id": "alice",
})
_, isDonor := evt.(audit.FieldsDonor)
fmt.Println("NewEvent is a FieldsDonor:", isDonor)
}
Output: NewEvent is a FieldsDonor: false
type FormatOptions ¶
type FormatOptions struct {
// ExcludedLabels is the set of sensitivity labels to exclude.
// Set once at construction time; immutable after that.
ExcludedLabels map[string]struct{}
// FieldLabels maps field names to their resolved sensitivity labels.
// Set by the library per-event from [EventDef.FieldLabels] before
// calling Format. Implementations MUST NOT retain this pointer.
FieldLabels map[string]map[string]struct{}
}
FormatOptions carries optional per-output context to the formatter. A nil *FormatOptions means no special handling — all fields are emitted. When non-nil, the formatter skips fields whose sensitivity labels overlap with ExcludedLabels.
The library sets FieldLabels per-event before calling [Formatter.Format]. Implementations MUST NOT retain the opts pointer or modify its fields beyond the duration of the Format call.
func (*FormatOptions) IsExcluded ¶
func (o *FormatOptions) IsExcluded(fieldName string) bool
IsExcluded reports whether fieldName carries any label in the excluded set. Custom Formatter implementations should call this to honor sensitivity exclusions.
type Formatter ¶
type Formatter interface {
// Format serialises a single audit event into a wire-format byte
// slice. Implementations MUST append a newline terminator; the
// library passes the result directly to [Output.Write].
//
// ts is the wall-clock time recorded at drain time (not
// submission). eventType is the registered event type name.
// fields contains the caller-supplied key-value pairs. def is the
// [EventDef] for eventType; it is never nil when called by the
// library. opts carries per-output sensitivity exclusion context;
// nil means no field exclusion. Use [FormatOptions.IsExcluded] to
// check whether a field should be skipped. Implementations MUST
// NOT retain the opts pointer beyond the Format call.
//
// A non-nil error causes the event to be dropped and
// [Metrics.RecordSerializationError] to be called.
Format(ts time.Time, eventType string, fields Fields, def *EventDef, opts *FormatOptions) ([]byte, error)
}
Formatter serialises an audit event into a wire-format byte slice. Implementations MUST append a newline terminator. The library provides JSONFormatter and CEFFormatter.
Concurrency ¶
A single Auditor's drain loop calls Format from one goroutine at a time, but a formatter instance MAY be shared across multiple Auditors via WithFormatter (multi-tenant or multi-pipeline deployments) and MAY be called from caller goroutines in synchronous delivery mode. Implementations MUST be safe for concurrent use.
Stateless formatters satisfy this trivially. Formatters that cache derived state (resolved field mappings, compiled templates, metric handles) SHOULD guard the state with sync.Once for one-shot initialisation or sync.RWMutex / sync/atomic for mutable caches. The package-level sync.Pool pattern used by the built-in JSONFormatter and CEFFormatter is the recommended shape for per-call buffer reuse.
Precedent: log/slog.Handler.Handle, net/http.Handler.ServeHTTP, and encoding/json.Marshaler all require concurrent-safe implementations by the same reasoning.
type FrameworkContext ¶ added in v0.1.12
type FrameworkContext struct {
// AppName is the auditor-wide application name. Used as the
// default RFC 5424 APP-NAME when the syslog per-output config
// omits `app_name`. May be empty — outputs fall back to their
// own defaults in that case.
AppName string
// Host is the auditor-wide host identifier. Used as the default
// RFC 5424 HOSTNAME when the syslog per-output config omits
// `hostname`. May be empty.
Host string
// Timezone is the auditor-wide timezone label (e.g. "UTC",
// "Europe/London"). Populated by outputconfig.Load from the
// auditor's WithTimezone option (or [time.Local] default).
// May be empty.
Timezone string
// PID is the process ID at auditor construction time.
// Populated by outputconfig.Load from os.Getpid(). Cached values
// (e.g. Loki's pre-formatted pid label) should be derived once at
// construction.
PID int
// DiagnosticLogger receives operational warnings emitted by the
// output (TLS handshake failures, retry exhaustion, drop-rate
// limit triggers, etc.). When nil, output authors fall back to
// [slog.Default] at use site:
//
// lg := fctx.DiagnosticLogger
// if lg == nil { lg = slog.Default() }
//
// Do NOT mutate fctx to backfill defaults — FrameworkContext is a
// value type and mutation is a footgun. The nil-default contract
// is applied at every call site.
//
// Replaces the post-construction DiagnosticLoggerReceiver
// interface dropped in #696.
DiagnosticLogger *slog.Logger
// OutputMetrics receives per-output delivery counters
// (RecordSuccess / RecordError / RecordDrop / RecordBufferDrop /
// RecordRetry). When nil, output authors fall back to
// [NoOpOutputMetrics] at use site (zero-value-safe).
//
// Replaces the post-construction OutputMetricsReceiver interface
// dropped in #696.
OutputMetrics OutputMetrics
// CoreMetrics receives auditor-wide counters (queue depth,
// drop rate). When nil, output authors fall back to [NoOpMetrics]
// at use site (zero-value-safe).
//
// Distinct from OutputMetrics: CoreMetrics is the metrics value
// the auditor itself uses; OutputMetrics is per-output and
// provided by outputconfig.WithOutputMetricsFactory.
CoreMetrics Metrics
}
FrameworkContext carries auditor-wide framework metadata into output constructors so the first connection / first request can use the correct app_name, host, timezone, and pid. Construction- time cascades (e.g., syslog RFC 5424 APP-NAME defaulting from the top-level app_name when the per-output YAML key is omitted) are driven through this value.
Populated by [outputconfig.Load] by parsing the top-level `app_name` and `host` fields from the outputs YAML. Direct-Go consumers who construct outputs themselves pass the zero value (FrameworkContext{}) unless they want to pre-populate cascade defaults for their use case.
Introduced in #583 to solve the sequencing problem where framework fields were propagated to outputs AFTER the initial dial in [syslog.New], so the first syslog session used the wrong APP-NAME.
type FrameworkFieldSetter ¶
FrameworkFieldSetter is implemented by formatters that emit auditor-wide framework metadata (app_name, host, timezone, pid) in serialised output. The library calls SetFrameworkFields once at construction time, after all options are applied and before the first Format call.
JSONFormatter and CEFFormatter implement this interface. Third-party formatters that do not implement it silently omit these fields.
type HMACConfig ¶
type HMACConfig struct {
// Enabled controls whether HMAC is computed for this output.
// Default: false. Must be explicitly true.
Enabled bool
// Salt carries the salt identifier and the raw salt bytes. See
// [HMACSalt] for field-level documentation.
Salt HMACSalt
// Algorithm is the HMAC hash algorithm. Must be one of the
// NIST-approved values: HMAC-SHA-256, HMAC-SHA-384, HMAC-SHA-512,
// HMAC-SHA3-256, HMAC-SHA3-384, HMAC-SHA3-512.
Algorithm string
}
HMACConfig holds per-output HMAC configuration. When Enabled is true, every event delivered to the output has an HMAC appended. The HMAC is computed over the final serialised payload (after field stripping and event_category append).
The Go shape mirrors the YAML shape under outputs.<name>.hmac:
hmac:
enabled: true
salt:
version: "2026-Q1"
value: "${HMAC_SALT}"
algorithm: "HMAC-SHA-256"
func (HMACConfig) GoString ¶
func (c HMACConfig) GoString() string
GoString implements fmt.GoStringer to prevent salt leakage via %#v.
func (HMACConfig) String ¶
func (c HMACConfig) String() string
String returns a safe representation that never includes the salt value.
type HMACSalt ¶ added in v0.1.12
type HMACSalt struct {
// Version is a user-defined identifier for the salt, emitted in
// the output alongside the HMAC digest. Supports salt rotation —
// consumers use this to look up the correct salt for
// verification. Required when [HMACConfig.Enabled] is true. Must
// match `^[A-Za-z0-9._:-]+$` (for unambiguous authenticated-byte
// representation on the wire — see #473) and be at most 64
// characters.
Version string
// Value is the raw salt bytes. MUST be at least [MinSaltLength]
// (16 bytes / 128 bits). The built-in [HMACSalt.String] and
// [HMACSalt.GoString] methods redact this value; consumers
// implementing their own formatting MUST NOT log or include it in
// error messages.
Value []byte
}
HMACSalt groups the salt identifier and salt bytes for an HMACConfig. The grouping matches the nested YAML shape under outputs.<name>.hmac.salt, so a consumer reading the library's godoc and writing YAML sees the same structure in both places.
HMACSalt implements fmt.Stringer and fmt.GoStringer to redact the raw Value from %v, %+v, and %#v format verbs. Consumers SHOULD still treat Value as secret and avoid passing it to any unbounded writer.
func (HMACSalt) GoString ¶ added in v0.1.12
GoString implements fmt.GoStringer to prevent salt leakage via %#v.
type Hints ¶
type Hints struct {
// Extra holds arbitrary domain-specific fields. It is initialised
// lazily by the handler. Keys and values are passed through to
// the [EventBuilder] callback.
Extra map[string]any
// EventType is the audit event type name (e.g. "user_create").
// If empty, the [EventBuilder] decides the event type.
EventType string
// Outcome is the high-level result: "success", "failure", "denied", etc.
Outcome string
// ActorID identifies the authenticated principal (user ID, service account, etc.).
ActorID string
// ActorType categorises the actor: "user", "service", "api_key", etc.
ActorType string
// AuthMethod describes how the actor authenticated: "bearer", "mtls", "session", etc.
AuthMethod string
// Role is the actor's role or permission level at the time of the request.
Role string
// TargetType categorises the resource being acted upon: "document", "user", etc.
TargetType string
// TargetID identifies the specific resource being acted upon.
TargetID string
// Reason is a human-readable justification for the action, if applicable.
Reason string
// Error holds an error message when the request fails.
Error string
}
Hints carries mutable, per-request audit metadata through the request context. Handlers retrieve it with HintsFromContext and populate domain-specific fields (actor, target, outcome). The middleware reads these fields after the handler returns and passes them to the EventBuilder callback.
Each request receives its own *Hints allocation; there is no shared mutable state between concurrent requests. The middleware does NOT pool *Hints because consumer handlers are allowed to capture r.Context() into spawned goroutines that outlive ServeHTTP and read HintsFromContext lazily; pooling Hints would silently expose those goroutines to recycled state (#501).
func HintsFromContext ¶
HintsFromContext retrieves the Hints from the request context. Returns nil if the request was not wrapped by Middleware.
Example ¶
package main
import (
"fmt"
"net/http"
"github.com/axonops/audit"
)
func main() {
// Inside an HTTP handler wrapped by Middleware:
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hints := audit.HintsFromContext(r.Context())
if hints != nil {
hints.ActorID = "user-42"
hints.Outcome = "success"
hints.TargetType = "document"
hints.TargetID = "doc-99"
}
w.WriteHeader(http.StatusOK)
})
_ = handler // register with your router
fmt.Println("handler with hints")
}
Output: handler with hints
type JSONFormatter ¶
type JSONFormatter struct {
// Timestamp controls the timestamp format. Empty defaults to
// [TimestampRFC3339Nano].
Timestamp TimestampFormat
// OmitEmpty controls whether zero-value fields are omitted.
OmitEmpty bool
// contains filtered or unexported fields
}
JSONFormatter serialises audit events as line-delimited JSON.
Fields are emitted in deterministic order: framework fields first (timestamp, event_type, severity, duration_ms if present as time.Duration), then required fields (sorted), then optional fields (sorted), then any extra fields (sorted). Each event is terminated by a newline.
time.Duration values are converted to int64 milliseconds. Timestamps are rendered according to [JSONFormatter.Timestamp] (default TimestampRFC3339Nano).
Concurrency ¶
Safe for concurrent use by multiple goroutines, per the Formatter contract. All per-call buffers are leased from a package-level sync.Pool; the struct itself holds only write-once configuration set at construction.
func (*JSONFormatter) Format ¶
func (jf *JSONFormatter) Format(ts time.Time, eventType string, fields Fields, def *EventDef, opts *FormatOptions) ([]byte, error)
Format serialises a single audit event as a JSON line. The returned slice is owned by the caller (defensive copy from the pooled buffer).
Internal callers in the drain pipeline use [JSONFormatter.formatBuf] to obtain the leased buffer directly and skip the copy.
func (*JSONFormatter) SetFrameworkFields ¶
func (jf *JSONFormatter) SetFrameworkFields(appName, host, timezone string, pid int)
SetFrameworkFields stores auditor-wide framework metadata for emission in every JSON event. Called once at construction time.
type LabelInfo ¶
type LabelInfo struct {
Name string // label name, e.g., "pii"
Description string // human-readable explanation
}
LabelInfo describes a sensitivity label defined in the taxonomy.
type LastDeliveryReporter ¶ added in v0.1.12
type LastDeliveryReporter interface {
LastDeliveryNanos() int64
}
LastDeliveryReporter is an optional interface that Output implementations may satisfy to expose the timestamp of their most recent successful delivery. Outputs implementing this interface enable Auditor.LastDeliveryAge, used by /healthz handlers to detect silently-failing async outputs whose own buffer is dropping events while the core auditor queue stays low.
Not to be confused with DeliveryReporter — that interface controls per-event metrics dispatch (success / error / filtered); this one reports a single timestamp for staleness probes.
LastDeliveryNanos returns the wall-clock nanos of the last successful end-to-end delivery (NOT the moment [Output.Write] returned — for async outputs that distinction is the whole point), or 0 if no delivery has yet succeeded. Wall-clock means the value can jump on system time changes; /healthz thresholds SHOULD be ≥ 10 s to absorb sub-second NTP slews. The reference example in examples/18-health-endpoint uses 30 s — see that example's README for picking a threshold for your workload.
The returned value is NOT guaranteed monotonic across calls — wall-clock time can step backwards on NTP correction, daylight saving transitions, or operator clock changes. Callers reading two successive values MUST NOT assume `b >= a`. The canonical consumer (Auditor.LastDeliveryAge) computes time.Since which already absorbs negative differences as zero.
Concurrency: implementations MUST be safe for concurrent use from any goroutine. The canonical implementation is a single sync/atomic.Int64 updated on every successful delivery and loaded on every [LastDeliveryNanos] call.
type MetadataWriter ¶
type MetadataWriter interface {
WriteWithMetadata(data []byte, meta EventMetadata) error
}
MetadataWriter is an optional interface that Output implementations may satisfy to receive structured event metadata alongside pre-serialised bytes. When an output implements MetadataWriter, the library calls WriteWithMetadata instead of [Output.Write].
Implementations MUST NOT retain meta or take its address after returning. The library passes meta by value on the stack; retaining it forces heap allocation. The caller must not assume the value remains valid after return.
IMPORTANT — buffer ownership: the same retention contract documented on [Output.Write] applies to data. The library MAY reuse data's underlying array after WriteWithMetadata returns; implementations MUST copy the bytes before retaining them.
type Metrics ¶
type Metrics interface {
// RecordSubmitted records that an event was submitted to the
// pipeline via [Auditor.AuditEvent]. Called once per AuditEvent
// call, before any filtering or buffering. This is the "total
// events in" counter.
//
// Cardinality: single counter (no labels).
RecordSubmitted()
// RecordDelivery records an event delivery attempt to the named
// output. status is a typed enum — see [EventStatus] for the
// defined values.
//
// Cardinality: 2-dimensional vector (output × status).
// The output label set is bounded by the number of configured
// outputs. status has two values ([EventSuccess], [EventError]).
RecordDelivery(output string, status EventStatus)
// RecordOutputError records a write error on the named output.
//
// Cardinality: 1-dimensional vector (output). Bounded by the
// number of configured outputs.
RecordOutputError(output string)
// RecordOutputFiltered records that a per-output event route filter
// prevented an event from being delivered to the named output.
// This is distinct from [Metrics.RecordFiltered], which records
// global category/event filter drops before any output is reached.
//
// Cardinality: 1-dimensional vector (output).
RecordOutputFiltered(output string)
// RecordValidationError records that [Auditor.AuditEvent] rejected an
// event due to a validation failure: unknown event type, missing
// required fields, or unknown fields in strict mode. The
// eventType parameter is the event type string that was passed to
// AuditEvent.
//
// Cardinality: 1-dimensional vector (event_type). HIGH cardinality
// if the taxonomy grows large or if unknown event types are
// common. Consumers may aggregate into a single counter without
// the event_type label to cap the vector size.
RecordValidationError(eventType string)
// RecordFiltered records that an event was silently discarded by
// the global category/event filter. This is distinct from
// [Metrics.RecordOutputFiltered] which tracks per-output route
// filtering.
//
// Cardinality: 1-dimensional vector (event_type). HIGH cardinality
// — see [Metrics.RecordValidationError].
RecordFiltered(eventType string)
// RecordSerializationError records that the configured [Formatter]
// returned an error (or panicked) when serialising an event. The
// event is dropped when this occurs.
//
// Cardinality: 1-dimensional vector (event_type). HIGH cardinality
// — see [Metrics.RecordValidationError].
RecordSerializationError(eventType string)
// RecordBufferDrop records that an event was dropped because the
// main async queue was full.
//
// Cardinality: single counter (no labels).
RecordBufferDrop()
// RecordQueueDepth records the current depth and capacity of the
// core intake queue. Called from the drain loop, sampled every 64
// events processed. depth is len(channel), capacity is
// cap(channel).
//
// Cardinality: gauge (depth) and an associated gauge or constant
// (capacity). No per-call labels. Consumers may record the
// capacity once at startup and emit depth per call.
RecordQueueDepth(depth, capacity int)
}
Metrics is an optional instrumentation interface that consumers implement to collect audit pipeline telemetry. Pass an implementation via WithMetrics; pass nil to disable metrics collection.
The library never imports a concrete metrics library (Prometheus, OpenTelemetry, etc.). Consumers wire their own. The examples/17-capstone Prometheus adapter shows a complete implementation in under 50 lines using a table-driven registration pattern; copy it as a starting point.
Consumers SHOULD embed NoOpMetrics in their implementation to absorb new methods added in future versions without breaking builds.
Ownership: Metrics vs OutputMetrics ¶
Metrics records pipeline-level counters that span the entire auditor:
- RecordSubmitted — total events entering the pipeline
- RecordDelivery — per-output delivery outcome (for non-self-reporting outputs)
- RecordBufferDrop — core intake queue overflow
- RecordQueueDepth — core intake queue pressure gauge
OutputMetrics records per-output buffer operations inside each async output:
- RecordDrop — per-output buffer overflow
- RecordFlush — per-output batch delivery
- RecordError — per-output non-retryable delivery failure
- RecordRetry — per-output retry attempt
- RecordQueueDepth — per-output buffer pressure gauge
For outputs that implement DeliveryReporter (webhook, loki, file, syslog), the output itself calls RecordDelivery after actual delivery. The core auditor skips RecordDelivery for these outputs to avoid phantom success counting.
Cardinality guidance ¶
Each method notes the Prometheus / OpenTelemetry label-vector dimensionality implied by its arguments. "High cardinality" flags methods whose label space scales with caller-supplied identifiers (event types, output names) — consumers with many event types should budget accordingly when wiring label vectors.
Forward compatibility ¶
Adding a method to Metrics in a v1.x release is a breaking interface change. The library adds new metrics via separate optional interfaces detected by type assertion on the passed Metrics value, mirroring the pattern used by DeliveryReporter on outputs and by [file.RotationRecorder] / [syslog.ReconnectRecorder] on OutputMetrics. Consumers who embed NoOpMetrics retain no-op implementations for every base-interface method; extensions are additive. See ADR 0005 (docs/adr/0005-metrics-interface-shape.md) for the full policy.
type NoOpMetrics ¶
type NoOpMetrics struct{}
NoOpMetrics is a Metrics implementation where every method is a no-op. Embed it in your own struct to override only the methods you care about:
type MyMetrics struct {
audit.NoOpMetrics
drops atomic.Int64
}
func (m *MyMetrics) RecordBufferDrop() { m.drops.Add(1) }
func (NoOpMetrics) RecordBufferDrop ¶
func (NoOpMetrics) RecordBufferDrop()
RecordBufferDrop is a no-op.
func (NoOpMetrics) RecordDelivery ¶ added in v0.1.12
func (NoOpMetrics) RecordDelivery(string, EventStatus)
RecordDelivery is a no-op.
func (NoOpMetrics) RecordFiltered ¶
func (NoOpMetrics) RecordFiltered(string)
RecordFiltered is a no-op.
func (NoOpMetrics) RecordOutputError ¶
func (NoOpMetrics) RecordOutputError(string)
RecordOutputError is a no-op.
func (NoOpMetrics) RecordOutputFiltered ¶
func (NoOpMetrics) RecordOutputFiltered(string)
RecordOutputFiltered is a no-op.
func (NoOpMetrics) RecordQueueDepth ¶ added in v0.1.10
func (NoOpMetrics) RecordQueueDepth(int, int)
RecordQueueDepth is a no-op.
func (NoOpMetrics) RecordSerializationError ¶
func (NoOpMetrics) RecordSerializationError(string)
RecordSerializationError is a no-op.
func (NoOpMetrics) RecordSubmitted ¶ added in v0.1.10
func (NoOpMetrics) RecordSubmitted()
RecordSubmitted is a no-op.
func (NoOpMetrics) RecordValidationError ¶
func (NoOpMetrics) RecordValidationError(string)
RecordValidationError is a no-op.
type NoOpOutputMetrics ¶ added in v0.1.10
type NoOpOutputMetrics struct{}
NoOpOutputMetrics is an OutputMetrics implementation where every method is a no-op. Embed it in your own struct to override only the methods you care about.
func (NoOpOutputMetrics) RecordDrop ¶ added in v0.1.10
func (NoOpOutputMetrics) RecordDrop()
RecordDrop is a no-op.
func (NoOpOutputMetrics) RecordError ¶ added in v0.1.10
func (NoOpOutputMetrics) RecordError()
RecordError is a no-op.
func (NoOpOutputMetrics) RecordFlush ¶ added in v0.1.10
func (NoOpOutputMetrics) RecordFlush(int, time.Duration)
RecordFlush is a no-op.
func (NoOpOutputMetrics) RecordQueueDepth ¶ added in v0.1.10
func (NoOpOutputMetrics) RecordQueueDepth(int, int)
RecordQueueDepth is a no-op.
func (NoOpOutputMetrics) RecordRetry ¶ added in v0.1.10
func (NoOpOutputMetrics) RecordRetry(int)
RecordRetry is a no-op.
type NoopSanitizer ¶ added in v0.1.12
type NoopSanitizer struct{}
NoopSanitizer is the zero-value Sanitizer that returns inputs unchanged. Embed it in custom Sanitizers to override only the method you care about — the http.ResponseWriter adapter pattern.
type RedactPasswords struct {
audit.NoopSanitizer
}
func (RedactPasswords) SanitizeField(key string, value any) any {
if key == "password" { return "[redacted]" }
return value
}
func (NoopSanitizer) SanitizeField ¶ added in v0.1.12
func (NoopSanitizer) SanitizeField(_ string, value any) any
SanitizeField returns value unchanged.
func (NoopSanitizer) SanitizePanic ¶ added in v0.1.12
func (NoopSanitizer) SanitizePanic(val any) any
SanitizePanic returns val unchanged.
type Option ¶
Option configures a Auditor during construction via New.
Options fall into three classes (#593 B-45):
Required options — New returns a sentinel error if the option is absent: WithTaxonomy (ErrTaxonomyRequired), WithAppName (ErrAppNameRequired), WithHost (ErrHostRequired). These inputs have no library-supplied default.
Validated-on-call options — optional to call, but reject empty arguments when called: WithFormatter (nil rejected; omitting yields a default JSONFormatter), WithTimezone (empty rejected; omitting defaults to the local timezone reported by time.Now().Location().String()).
Optional options — accept nil / unset with a documented default: WithMetrics — nil or unset disables metrics collection. WithDiagnosticLogger — nil or unset uses slog.Default. WithStandardFieldDefaults — nil or unset uses no defaults.
Remaining options configure behaviour via value types (WithQueueSize, WithShutdownTimeout, WithValidationMode, WithOmitEmpty, WithDisabled, WithOutputs, WithNamedOutput, WithSynchronousDelivery) and have their own documented zero-value semantics.
The split mirrors the net/http convention — http.Client.Transport is optional with http.DefaultTransport as the documented nil-default, but the Handler on http.Server is required.
func WithAppName ¶
WithAppName sets the application name emitted as a framework field in every serialised event.
Required. New returns ErrAppNameRequired if WithAppName is unset (unless WithDisabled is also applied). The value must be non-empty and at most 255 bytes.
func WithDiagnosticLogger ¶ added in v0.1.10
WithDiagnosticLogger sets the log/slog.Logger used for library diagnostics (lifecycle messages, buffer drops, format errors).
Optional. When not set or when l is nil, slog.Default is used. Pass slog.New(slog.DiscardHandler) to silence all library output.
The logger is fixed at construction; runtime swap is not supported (the prior Auditor.SetLogger API was removed in #696). To redirect diagnostics later, rebuild the auditor.
func WithDisabled ¶
func WithDisabled() Option
WithDisabled creates a no-op auditor that discards all events without validation or delivery. Auditor.AuditEvent returns nil immediately. This is the explicit opt-out for audit logging — the default is enabled, because silent audit disablement is worse than noisy audit failure.
func WithFormatter ¶
WithFormatter sets the event serialisation formatter.
Optional to call; if WithFormatter is not called, a JSONFormatter with default settings is used. If WithFormatter is called, f MUST be non-nil — the option returns an error for a nil formatter since there is no sane default to substitute at that point. Use this to configure a CEFFormatter or a custom Formatter implementation.
Example ¶
package main
import (
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
cef := &audit.CEFFormatter{
Vendor: "MyCompany",
Product: "MyApp",
Version: "1.0",
SeverityFunc: func(eventType string) int {
if eventType == "auth_failure" {
return 8
}
return 5
},
}
auditor, err := audit.New(
audit.WithTaxonomy(&audit.Taxonomy{
Version: 1,
Categories: map[string]*audit.CategoryDef{"security": {Events: []string{"auth_failure"}}},
Events: map[string]*audit.EventDef{
"auth_failure": {Required: []string{"outcome"}},
},
}),
audit.WithAppName("test-app"),
audit.WithHost("test-host"),
audit.WithFormatter(cef),
audit.WithSynchronousDelivery(),
)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := auditor.Close(); err != nil {
log.Printf("audit close: %v", err)
}
}()
fmt.Println("CEF formatter configured")
}
Output: CEF formatter configured
func WithHost ¶
WithHost sets the hostname emitted as a framework field in every serialised event.
Required. New returns ErrHostRequired if WithHost is unset (unless WithDisabled is also applied). The value must be non-empty and at most 255 bytes.
func WithMetrics ¶
WithMetrics sets the metrics recorder for the auditor.
Optional. If m is nil, or if WithMetrics is not called, metrics are silently discarded (no metrics collection). Implementations MUST be safe for concurrent calls from the drain goroutine.
func WithNamedOutput ¶
func WithNamedOutput(output Output, opts ...OutputOption) Option
WithNamedOutput adds a single named output with optional per-output configuration. Use WithRoute, WithOutputFormatter, WithExcludeLabels, and WithHMAC to customise behaviour.
WithNamedOutput MUST NOT be combined with WithOutputs; if WithOutputs was already applied, WithNamedOutput returns an error.
Output names MUST be unique across all outputs; duplicate names cause New to return an error. Duplicate destinations are also detected via DestinationKeyer. Routes are validated against the taxonomy after all options have been applied.
func WithOmitEmpty ¶
func WithOmitEmpty() Option
WithOmitEmpty enables omission of empty, nil, and zero-value fields from serialised output. When enabled, only non-zero fields are serialised. Consumers operating under compliance regimes that require all registered fields SHOULD NOT use this option.
func WithOutputs ¶
WithOutputs sets the output destinations for the auditor. Events are fanned out to all provided outputs. Each output receives all globally-enabled events (no per-output filtering). Use WithNamedOutput to configure per-output event routes or formatters.
WithOutputs MUST NOT be combined with WithNamedOutput; mixing the two returns an error. Duplicate output destinations are also detected: if two outputs implement DestinationKeyer and return the same key, WithOutputs returns an error. If no outputs are configured, events are validated and filtered but silently discarded.
func WithQueueSize ¶ added in v0.1.10
WithQueueSize sets the async intake queue capacity for the auditor. Zero or negative values are ignored (the default of DefaultQueueSize applies). Values above MaxQueueSize cause New to return an error wrapping ErrConfigInvalid.
func WithSanitizer ¶ added in v0.1.12
WithSanitizer registers a Sanitizer with the auditor. The Sanitizer's [Sanitizer.SanitizeField] is invoked once per field on every [Auditor.Audit] / Auditor.AuditEvent call (NOT only the middleware path); [Sanitizer.SanitizePanic] is invoked on the middleware panic-recovery path before the panic is re-raised.
Passing a nil Sanitizer is a no-op (unset state). When unset, the per-event hot path performs a single nil-check and pays no further overhead. See the Sanitizer godoc for the concurrency, ownership, and return-type contracts that implementations MUST satisfy.
Use Sanitizer to scrub PII, mask credentials, hash identifiers, or replace internal error messages before they reach output sinks AND before middleware-recovered panic values flow to outer panic handlers (Sentry, panic loggers, parent recovery middleware).
Example:
type RedactPasswords struct{ audit.NoopSanitizer }
func (RedactPasswords) SanitizeField(key string, value any) any {
if key == "password" { return "[redacted]" }
return value
}
auditor, err := audit.New(
audit.WithTaxonomy(tax),
audit.WithAppName("svc"),
audit.WithHost("h1"),
audit.WithSanitizer(RedactPasswords{}),
)
func WithShutdownTimeout ¶ added in v0.1.10
WithShutdownTimeout sets the maximum time Auditor.Close waits for pending events to flush. Zero or negative values are ignored (the default of DefaultShutdownTimeout applies). Values above MaxShutdownTimeout cause New to return an error wrapping ErrConfigInvalid.
func WithStandardFieldDefaults ¶
WithStandardFieldDefaults sets deployment-wide default values for reserved standard fields. Defaults are applied in Auditor.AuditEvent before validation — a default satisfies required: true constraints. Per-event values always override defaults (key existence check, not zero value). When called multiple times, the last call wins.
Optional. Nil or empty map means "no defaults".
Each value's Go type MUST match the reserved field's declared type reported by ReservedStandardFieldType. On mismatch, New returns an error wrapping ErrConfigInvalid so the misconfiguration surfaces at startup rather than at the first AuditEvent. Numeric port fields (`source_port`, `dest_port`, `file_size`) require int; timestamps (`start_time`, `end_time`) require time.Time; the remaining 26 reserved fields require string. Pre-#595 callers passing `map[string]string` migrate by changing the literal map type; values that are already strings for string-typed fields keep working unchanged.
func WithSynchronousDelivery ¶
func WithSynchronousDelivery() Option
WithSynchronousDelivery configures the auditor to deliver events inline within Auditor.AuditEvent instead of via the async channel and drain goroutine. Events are immediately available in outputs after AuditEvent returns.
This mode is useful for testing (no Close-before-assert ceremony) and for simple deployments (CLI tools, Lambda functions) where async complexity is unwanted. Auditor.Close is still safe to call but is not required before reading output.
Caller-observable contract ¶
- AuditEvent returns ONLY after every output has received the event. The caller's goroutine blocks for the sum of all outputs' Write durations.
- A panic inside an output's Write is RECOVERED — it is NOT propagated to the caller. The auditor logs the panic at error level, records a per-output drop metric, and continues fan-out to subsequent outputs. AuditEvent returns nil even if one or more outputs panicked.
- After Auditor.Close, AuditEvent returns ErrClosed synchronously without invoking any output. Close itself is idempotent: repeated calls return nil with no side effects.
func WithTaxonomy ¶
WithTaxonomy registers the event taxonomy for validation. This option is required; New returns an error if no taxonomy is provided. WithTaxonomy SHOULD be called exactly once per New call. Calling it more than once replaces the taxonomy and resets all runtime category and event overrides established by the previous call.
WithTaxonomy makes a deep copy of t; mutations to t after this call have no effect on the auditor. When t was returned by ParseTaxonomyYAML, redundant re-validation is skipped.
func WithTimezone ¶
WithTimezone sets the timezone name emitted as a framework field in every serialised event.
Optional to call. If omitted, the timezone defaults to the local timezone as reported by time.Now().Location().String(); the timezone field is therefore always populated on every event. To suppress the timezone field entirely, supply a custom Formatter whose [Formatter.SetFrameworkFields] discards the timezone value. The built-in JSONFormatter and CEFFormatter write the timezone framework field unconditionally and cannot suppress it via FormatOptions.IsExcluded. If called, tz MUST be non-empty (the option returns an error for an empty string since there is no sane default to substitute at that point). At most 64 bytes.
func WithValidationMode ¶
func WithValidationMode(m ValidationMode) Option
WithValidationMode sets how Auditor.AuditEvent handles unknown fields. Must be one of ValidationStrict, ValidationWarn, or ValidationPermissive. An invalid mode causes New to return an error wrapping ErrConfigInvalid.
type Output ¶
type Output interface {
// Write sends a single serialised audit event to the output.
// data is a complete, newline-terminated byte slice. Write is
// called from a single goroutine; concurrent calls from the
// library will not occur. Implementers MAY assume single-caller
// access.
//
// IMPORTANT — buffer ownership: the library MAY reuse data's
// underlying array after Write returns. Implementations MUST NOT
// retain data, or any slice that aliases its backing array, past
// the Write call. If the bytes are needed beyond the call (for
// example, to enqueue onto an asynchronous worker channel),
// implementations MUST copy them — for example with
// append([]byte(nil), data...). All first-party async outputs
// (file, syslog, webhook, loki) already copy on enqueue.
//
// This contract enables the library's drain pipeline to deliver
// pooled buffer bytes without per-event allocation (#497). Violating
// it causes cross-event data corruption that is silent in production
// and impossible to detect after the fact.
Write(data []byte) error
// Close flushes any buffered data and releases resources. The
// library guarantees Write will not be called after Close. Close
// is called exactly once by [Auditor.Close].
Close() error
// Name returns a human-readable identifier for the output,
// used in log messages and metrics labels.
//
// Name MUST NOT return an empty string. Empty names corrupt
// metrics labels, hide outputs in error messages, and break
// duplicate-name detection. [WithOutputs] and [WithNamedOutput]
// reject empty-name outputs at construction time.
Name() string
}
Output is the interface that audit event destinations MUST implement. All outputs receive pre-serialised bytes (JSON, CEF, or a custom format chosen via WithFormatter). Built-in implementations are provided by the file, syslog, webhook, loki, and stdout packages.
func WrapOutput ¶
WrapOutput wraps an Output with a consumer-chosen name. The returned output delegates all methods to the inner output except [Output.Name], which returns the provided name. This function is for OutputFactory implementors — regular consumers use WithOutputs or WithNamedOutput directly.
The returned output always satisfies DestinationKeyer, DeliveryReporter, MetadataWriter, and LastDeliveryReporter regardless of the inner output. When the inner output does not implement these interfaces, the wrapper returns zero-value behaviour: empty string for DestinationKey, false for ReportsDelivery, delegation to Write for WriteWithMetadata, and 0 for LastDeliveryNanos.
type OutputFactory ¶
type OutputFactory func(name string, rawConfig []byte, fctx FrameworkContext) (Output, error)
OutputFactory creates a named Output from raw YAML configuration bytes and a FrameworkContext.
name is the consumer-chosen output name from the YAML config (e.g. "compliance_file"). The factory SHOULD use this to set the output's identity via WrapOutput or equivalent.
rawConfig is the YAML bytes of the type-specific configuration block (e.g. the content under the "file:" key). The factory MUST NOT retain rawConfig after returning.
fctx carries every construction-time value the output needs: AppName / Host / Timezone / PID for framework field defaults, DiagnosticLogger for operational warnings, OutputMetrics for per-output delivery counters, and CoreMetrics for the auditor-wide recorder. Each field documents its zero-value default — factories MUST tolerate a zero-value FrameworkContext and apply nil defaults at use site rather than mutating fctx.
The 3-parameter shape (collapsed in #696 from the prior 5-parameter signature) places all construction-time inputs in fctx so the public surface stays stable as new construction-time data is added.
func LookupOutputFactory ¶
func LookupOutputFactory(typeName string) OutputFactory
LookupOutputFactory returns the registered factory for the given type name, or nil if no factory has been registered for that type.
func StdoutFactory ¶ added in v0.1.12
func StdoutFactory() OutputFactory
StdoutFactory returns an OutputFactory that creates a StdoutOutput writing to os.Stdout. Register it with RegisterOutputFactory to enable the YAML `type: stdout` form:
audit.RegisterOutputFactory("stdout", audit.StdoutFactory())
Blank-importing the github.com/axonops/audit/outputs convenience package registers this factory for you alongside file/syslog/ webhook/loki. Prior to #578 the registration happened automatically via an init() in this package; that was dropped to eliminate hidden global mutation at import time.
type OutputMetrics ¶ added in v0.1.10
type OutputMetrics interface {
// RecordDrop records that an event was dropped because the
// output's internal async buffer was full.
RecordDrop()
// RecordFlush records a successful batch flush to the output
// destination. batchSize is the number of events in the batch.
// dur is the wall-clock time of the flush operation.
RecordFlush(batchSize int, dur time.Duration)
// RecordError records a non-retryable delivery error.
RecordError()
// RecordRetry records a retry attempt. attempt is 1-indexed:
// 1 means first retry (second delivery attempt), 2 means second
// retry, etc.
RecordRetry(attempt int)
// RecordQueueDepth records the current depth and capacity of the
// output's internal async buffer. depth is the number of events
// waiting to be flushed, capacity is the buffer size.
RecordQueueDepth(depth, capacity int)
}
OutputMetrics is an optional per-output instrumentation interface for async buffer telemetry. Each output receives its own instance at construction via [FrameworkContext.OutputMetrics] (typically produced per-output by an OutputMetricsFactory supplied to outputconfig.WithOutputMetricsFactory), scoped to that output's identity.
Unlike Metrics (which tracks pipeline-level events), OutputMetrics tracks per-output buffer operations: drops, flushes, retries, errors, and queue depth. See the Metrics godoc for the ownership table.
Output-specific extensions (e.g. file rotation, syslog reconnection) are detected via type assertion on the OutputMetrics value. The returned OutputMetrics MAY optionally implement output-specific extension interfaces (e.g. [file.RotationRecorder], [syslog.ReconnectRecorder]). If detected, the output uses the extended methods automatically.
Consumers SHOULD embed NoOpOutputMetrics for forward compatibility.
type OutputMetricsFactory ¶ added in v0.1.10
type OutputMetricsFactory func(outputType, outputName string) OutputMetrics
OutputMetricsFactory creates a scoped OutputMetrics for a named output. outputType is the output type name (e.g. "file", "syslog", "webhook", "loki"). outputName is the consumer-chosen YAML key name (e.g. "compliance_archive", "security_feed"). The factory is called once per output at construction time.
Example Prometheus implementation:
func(outputType, outputName string) audit.OutputMetrics {
return &outputMetrics{
drops: dropsVec.WithLabelValues(outputType, outputName),
}
}
type OutputOption ¶
type OutputOption func(*outputEntryBuilder)
OutputOption configures a single output registered via WithNamedOutput. Use WithRoute, WithOutputFormatter, WithExcludeLabels, and WithHMAC to customise per-output behaviour.
func WithExcludeLabels ¶ added in v0.1.12
func WithExcludeLabels(labels ...string) OutputOption
WithExcludeLabels specifies sensitivity labels whose fields should be stripped from events before delivery to this output. When non-empty, the taxonomy MUST define a SensitivityConfig and every label MUST be defined within it; New returns an error if either condition is violated. An empty call means no field stripping. Framework fields are never stripped.
func WithHMAC ¶ added in v0.1.12
func WithHMAC(cfg *HMACConfig) OutputOption
WithHMAC configures per-output HMAC integrity. The config is validated eagerly during New option application — invalid configs (short salt, unknown algorithm) cause New to return an error. Nil means no HMAC for this output.
func WithOutputFormatter ¶ added in v0.1.12
func WithOutputFormatter(f Formatter) OutputOption
WithOutputFormatter overrides the auditor's default formatter for this output. Nil means the auditor's default formatter is used.
The "Output" prefix disambiguates from the auditor-level WithFormatter option; the two options set different defaults (auditor-wide vs per-output).
func WithRoute ¶ added in v0.1.12
func WithRoute(r *EventRoute) OutputOption
WithRoute sets the per-output event route. The route restricts which events are delivered to this output. Nil means all globally-enabled events are delivered.
type PostField ¶
type PostField struct {
// JSONKey is the JSON object key used when appending to JSON output.
JSONKey string
// CEFKey is the extension key used when appending to CEF output.
CEFKey string
// Value is the string value to emit for this field. Values are
// escaped automatically (JSON via [WriteJSONString], CEF via cefEscapeExtValue).
Value string
}
PostField represents a field appended to serialised bytes after format caching. This is an advanced/internal API used by the drain goroutine for delivery-specific context (event_category, HMAC). Custom formatter implementors may use AppendPostFields; regular consumers do not need this type.
type ReservedFieldType ¶ added in v0.1.12
type ReservedFieldType uint8
ReservedFieldType identifies the Go-level value type that a reserved standard field accepts. Consumers use it via ReservedStandardFieldType to validate per-field defaults and to drive type-aware tooling (linters, IDE plugins, code generators).
The library guarantees a stable enum identity for v1.x: existing constants do not change value, but new types may be added. Callers SHOULD include a default branch when switching on a value.
const ( // ReservedFieldString accepts Go string values. ReservedFieldString ReservedFieldType = iota // ReservedFieldInt accepts Go int values. Distinct from // ReservedFieldInt64 because some reserved fields (port numbers) // are bounded and idiomatically represented as int. ReservedFieldInt // ReservedFieldInt64 accepts Go int64 values. ReservedFieldInt64 // ReservedFieldFloat64 accepts Go float64 values. ReservedFieldFloat64 // ReservedFieldBool accepts Go bool values. ReservedFieldBool // ReservedFieldTime accepts Go [time.Time] values. ReservedFieldTime // ReservedFieldDuration accepts Go [time.Duration] values. ReservedFieldDuration )
Defined ReservedFieldType values. Values are stable within a major version. New values are added in minor releases.
func ReservedStandardFieldType ¶ added in v0.1.12
func ReservedStandardFieldType(name string) (ReservedFieldType, bool)
ReservedStandardFieldType reports the declared Go value type for a reserved standard field. The second return value is false if name is not a reserved standard field — see ReservedStandardFieldNames for the canonical list.
Consumers use this for type-aware linting, IDE assistance, or to validate their own configuration before passing it to WithStandardFieldDefaults.
func (ReservedFieldType) String ¶ added in v0.1.12
func (t ReservedFieldType) String() string
String returns the canonical string label for a ReservedFieldType. The label matches the Go source-level name of the underlying type (`string`, `int`, `int64`, `float64`, `bool`, `time.Time`, `time.Duration`) so it is suitable for embedding in error messages and YAML schemas.
type SSRFBlockedError ¶ added in v0.1.12
type SSRFBlockedError struct {
// IP is the address that was blocked. Uses [netip.Addr] (not
// [net.IP]) for value-comparable, zero-alloc semantics —
// mutating it is impossible.
IP netip.Addr
// Reason classifies why the address was blocked. Stable string
// value; suitable for use as a metric label.
Reason SSRFReason
// contains filtered or unexported fields
}
SSRFBlockedError is returned by CheckSSRFIP and CheckSSRFAddress when an address matches the SSRF block list. It wraps ErrSSRFBlocked for broad discrimination via errors.Is and exposes structured fields for per-reason metrics via errors.As:
var ssrfErr *audit.SSRFBlockedError
if errors.As(err, &ssrfErr) {
metricSSRFBlocked.With("reason", string(ssrfErr.Reason)).Inc()
log.Warn("blocked", "ip", ssrfErr.IP, "reason", ssrfErr.Reason)
}
func (*SSRFBlockedError) Error ¶ added in v0.1.12
func (e *SSRFBlockedError) Error() string
Error returns the human-readable error message. The text is identical to the pre-typed-error format for backwards compatibility.
func (*SSRFBlockedError) Unwrap ¶ added in v0.1.12
func (e *SSRFBlockedError) Unwrap() []error
Unwrap returns ErrSSRFBlocked so errors.Is matches the sentinel. Returns a slice to match the errors.Join contract and to mirror ValidationError's shape for consistency.
The returned slice is a defensive copy — callers may retain or mutate it without affecting the SSRFBlockedError or subsequent Unwrap calls. The copy is a 16-byte allocation on the error-discrimination path; Unwrap is only invoked by errors.Is / errors.As, which are off the audit hot path (#590).
type SSRFOption ¶
type SSRFOption func(*ssrfConfig)
SSRFOption configures SSRF protection behaviour for NewSSRFDialControl.
func AllowPrivateRanges ¶
func AllowPrivateRanges() SSRFOption
AllowPrivateRanges permits connections to RFC 1918 private address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), IPv6 ULA (fc00::/7), and loopback (127.0.0.0/8, ::1). This is intended for private network deployments where output receivers run on internal infrastructure, and for testing with net/http/httptest which binds to 127.0.0.1.
Cloud metadata (169.254.169.254, fd00:ec2::254), RFC 6598 Shared Address Space (100.64.0.0/10, CGNAT), and deprecated site-local IPv6 (fec0::/10) remain blocked even when private ranges are allowed.
type SSRFReason ¶ added in v0.1.12
type SSRFReason string
SSRFReason identifies the classification that caused an address to be blocked by SSRF protection. String values are stable and suitable for metric-label use.
const ( // SSRFReasonCloudMetadata — a published cloud instance metadata // endpoint (AWS IMDS at 169.254.169.254, AWS IMDSv2 over IPv6 at // fd00:ec2::254). Blocked even when [AllowPrivateRanges] is set. SSRFReasonCloudMetadata SSRFReason = "cloud_metadata" // SSRFReasonCGNAT — RFC 6598 Shared Address Space (100.64.0.0/10). // Used by CGNAT and some cloud providers for internal routing. // Blocked even when [AllowPrivateRanges] is set. SSRFReasonCGNAT SSRFReason = "cgnat" // SSRFReasonDeprecatedSiteLocal — deprecated RFC 3513 IPv6 // site-local range (fec0::/10). Not classified by Go's // [net.IP.IsPrivate]; blocked explicitly. Always blocked. SSRFReasonDeprecatedSiteLocal SSRFReason = "deprecated_site_local" // SSRFReasonLinkLocal — IPv4 169.254.0.0/16 or IPv6 fe80::/10. // Always blocked. SSRFReasonLinkLocal SSRFReason = "link_local" // SSRFReasonMulticast — IPv4 224.0.0.0/4 or IPv6 ff00::/8. // Always blocked. SSRFReasonMulticast SSRFReason = "multicast" // SSRFReasonUnspecified — 0.0.0.0 or ::. Always blocked. SSRFReasonUnspecified SSRFReason = "unspecified" // SSRFReasonLoopback — 127.0.0.0/8 or ::1. Blocked unless // [AllowPrivateRanges] is set. SSRFReasonLoopback SSRFReason = "loopback" // SSRFReasonPrivate — RFC 1918 private ranges or IPv6 ULA // (fc00::/7). Blocked unless [AllowPrivateRanges] is set. SSRFReasonPrivate SSRFReason = "private" )
SSRFReason constants. String values are stable for use as metric labels (snake_case per Prometheus convention).
type Sanitizer ¶ added in v0.1.12
type Sanitizer interface {
// SanitizeField returns a scrubbed version of the value for the
// given field key. Return value unchanged when no scrub is needed.
SanitizeField(key string, value any) any
// SanitizePanic returns a scrubbed version of a recovered panic
// value. Called once per middleware-recovered panic; the result
// flows to BOTH the audit event AND the re-raise.
SanitizePanic(val any) any
}
Sanitizer scrubs sensitive content from audit events and from re-raised middleware panic values. Register one with WithSanitizer; the same instance is consulted on every [Auditor.Audit] / Auditor.AuditEvent call AND on the middleware panic-recovery path.
Concurrency contract ¶
Implementations MUST be safe for concurrent use by multiple goroutines. Both methods may be invoked concurrently from any number of caller goroutines and from the middleware-handler goroutine.
Ownership contract ¶
Implementations MUST NOT retain references to the value passed in after returning. The audit pipeline takes ownership of the returned values; passed-in values may be backed by pooled memory that is recycled after the call.
Return-type contract ¶
[Sanitizer.SanitizeField] SHOULD return a value of the supported Fields vocabulary (string, int, int32, int64, float64, bool, time.Time, time.Duration, []string, map[string]string, or nil). Returning an unsupported type causes the value to be coerced via fmt.Sprintf when emitted, matching the behaviour of warn / permissive ValidationMode. To avoid allocations on the common case where no scrub is needed, return the original `value` argument unchanged.
Note: validation runs BEFORE the Sanitizer (so that strict mode rejects malformed events without paying scrub cost). A Sanitizer that returns an unsupported type AFTER strict validation has passed is NOT re-validated — the value flows to formatters and is coerced via fmt.Sprintf there. Consumers running in ValidationStrict mode who want absolute type-policy enforcement MUST ensure their Sanitizer preserves the supported vocabulary; the library deliberately does not pay for a second validation pass to catch sanitiser type drift.
[Sanitizer.SanitizeField] cannot remove a field — it operates on values, not keys. To remove a field entirely, configure per-output WithExcludeLabels in your output options.
Failure modes ¶
If [Sanitizer.SanitizeField] panics, the offending field's value is replaced with the SanitizerPanicSentinel string and the field key is appended to the framework field "sanitizer_failed_fields" ([]string). Other fields in the same event continue to be sanitised and the event is emitted.
If [Sanitizer.SanitizePanic] panics during middleware panic-recovery, the original (unsanitised) panic value is used in BOTH the audit event AND the re-raise (fail-open). The framework field "sanitizer_failed" (bool) is set to true so SIEM tooling can route on the failure signal.
In both failure modes, a diagnostic-level message is logged via the auditor's WithDiagnosticLogger. The diagnostic log records ONLY the field key (for SanitizeField) or value type (for SanitizePanic); it never logs the raw value the Sanitizer was meant to scrub.
type SensitivityConfig ¶
type SensitivityConfig struct {
// Labels maps label names (e.g., "pii", "financial") to their
// definitions. Label names MUST be non-empty and match the
// pattern `^[a-z][a-z0-9_]*$` for code generation safety.
// [ValidateTaxonomy] rejects any name that does not conform.
Labels map[string]*SensitivityLabel
}
SensitivityConfig holds all sensitivity label definitions for a taxonomy. It is optional; a nil SensitivityConfig means no sensitivity labels are defined and the feature is fully disabled with zero overhead.
type SensitivityLabel ¶
type SensitivityLabel struct {
// Description is an optional human-readable explanation of what
// this label represents.
Description string
// Fields lists field names that are globally assigned this label
// across all events. A field listed here receives this label in
// every event where it appears, regardless of per-event annotation.
Fields []string
// Patterns lists regex patterns. Any field name matching a pattern
// is assigned this label. Patterns are compiled once at parse time.
Patterns []string
// contains filtered or unexported fields
}
SensitivityLabel defines a single sensitivity label with optional global field mappings and regex patterns. Labels are defined in the taxonomy's sensitivity section and can be associated with fields via three mechanisms: explicit per-event annotation, global field name mapping, and regex patterns.
type StdoutConfig ¶
type StdoutConfig struct {
// Writer is the destination for audit events. When nil, [os.Stdout]
// is used. The writer does not need to be safe for concurrent use;
// StdoutOutput serialises writes internally.
Writer io.Writer
}
StdoutConfig holds configuration for StdoutOutput.
type StdoutOutput ¶
type StdoutOutput struct {
// contains filtered or unexported fields
}
StdoutOutput writes serialised audit events to an io.Writer, defaulting to os.Stdout. It is intended for development and debugging; production deployments SHOULD use [FileOutput] or another persistent output. The underlying writer can be os.Stdout, os.Stderr, or any io.Writer supplied via StdoutConfig or the convenience constructors NewStdout, NewStderr, NewWriter.
StdoutOutput does NOT close the underlying writer on [Close] because the writer is typically os.Stdout, which must not be closed.
StdoutOutput is safe for concurrent use.
func NewStderr ¶ added in v0.1.12
func NewStderr() (*StdoutOutput, error)
NewStderr returns a StdoutOutput that writes to os.Stderr. Useful when audit events must be visible on stderr (e.g., when stdout is reserved for primary application output).
func NewStdout ¶ added in v0.1.12
func NewStdout() (*StdoutOutput, error)
NewStdout returns a StdoutOutput that writes to os.Stdout. Shorthand for NewStdoutOutput(StdoutConfig{}). Non-panicking replacement for the pre-#578 Stdout() helper.
func NewStdoutOutput ¶
func NewStdoutOutput(cfg StdoutConfig) (*StdoutOutput, error)
NewStdoutOutput creates a new StdoutOutput from the given config. If [StdoutConfig.Writer] is nil, os.Stdout is used. Prefer the convenience constructors NewStdout, NewStderr, NewWriter unless you need the StdoutConfig struct for some reason.
Example ¶
package main
import (
"bytes"
"fmt"
"log"
"github.com/axonops/audit"
)
func main() {
// Create a stdout output for development/debugging. When Writer is
// nil, os.Stdout is used. Here we use a bytes.Buffer for testing.
var buf bytes.Buffer
out, err := audit.NewStdoutOutput(audit.StdoutConfig{
Writer: &buf,
})
if err != nil {
log.Fatal(err)
}
defer func() { _ = out.Close() }()
fmt.Println("stdout output:", out.Name())
}
Output: stdout output: stdout
func NewWriter ¶ added in v0.1.12
func NewWriter(w io.Writer) (*StdoutOutput, error)
NewWriter returns a StdoutOutput that writes to the given io.Writer. Useful for capturing audit events in a bytes.Buffer for tests, or for routing to any other destination that satisfies io.Writer. Passing nil causes the output to write to os.Stdout.
func (*StdoutOutput) Close ¶
func (s *StdoutOutput) Close() error
Close marks the output as closed. Subsequent calls to [Write] return ErrOutputClosed. Close does NOT close the underlying writer. Close is idempotent and safe for concurrent use with StdoutOutput.Write.
func (*StdoutOutput) LastDeliveryNanos ¶ added in v0.1.12
func (s *StdoutOutput) LastDeliveryNanos() int64
LastDeliveryNanos returns the wall-clock UnixNano of the most recent successful StdoutOutput.Write, or 0 if no write has yet succeeded. Implements LastDeliveryReporter (#753).
The Load is intentionally lock-free even though StdoutOutput.Write stores while holding s.mu — atomic.Int64 provides the happens-before relationship; the mutex on the Write side is for the closed-flag check and the underlying writer call, not for the timestamp.
func (*StdoutOutput) Name ¶
func (s *StdoutOutput) Name() string
Name returns the human-readable identifier for this output.
func (*StdoutOutput) Write ¶
func (s *StdoutOutput) Write(data []byte) error
Write sends a serialised audit event to the underlying writer. Write returns ErrOutputClosed if the output has been closed. Write is safe for concurrent use.
type TLSPolicy ¶
type TLSPolicy struct {
// AllowTLS12 permits TLS 1.2 connections in addition to TLS 1.3.
// When false (the default), MinVersion is set to TLS 1.3.
AllowTLS12 bool
// AllowWeakCiphers disables cipher suite filtering when AllowTLS12
// is true. By default, only cipher suites from [tls.CipherSuites]
// (the non-insecure list) are permitted. Setting this to true allows
// Go's full default suite selection, which may include weaker ciphers.
// Has no effect when AllowTLS12 is false, because TLS 1.3 cipher
// suites are not configurable in Go.
AllowWeakCiphers bool
}
TLSPolicy controls TLS version and cipher suite policy for output connections. The zero value enforces TLS 1.3 only with Go's default (secure) cipher suites.
func (*TLSPolicy) Apply ¶
Apply sets TLS version and cipher suite policy on cfg. If cfg is nil, a fresh tls.Config is created. Apply does not modify RootCAs, Certificates, ServerName, or any other pre-existing field. A nil receiver is treated as the zero value (TLS 1.3 only).
The returned warnings slice contains human-readable messages for security-sensitive configurations (e.g. weak ciphers enabled).
type Taxonomy ¶
type Taxonomy struct {
// Categories maps category names to their definitions. An event
// type may appear in multiple categories or in none (uncategorised
// events are always globally enabled).
Categories map[string]*CategoryDef
// Events maps event type names to their definitions. Every event
// type listed in Categories MUST have a corresponding entry here.
// Pointers are used to avoid per-event heap escapes when passing
// definitions through the drain path.
Events map[string]*EventDef
// Sensitivity defines the sensitivity label configuration. Nil
// means no sensitivity labels are defined; the feature is fully
// disabled with zero overhead.
Sensitivity *SensitivityConfig
// Version is the taxonomy schema version. MUST be > 0. Currently
// only version 1 is supported; higher values cause [WithTaxonomy]
// to return an error wrapping [ErrTaxonomyInvalid].
Version int
// SuppressEventCategory controls whether the `event_category` field
// is omitted from serialised output. The zero value (false) means
// the category IS emitted — matching the YAML default when
// `emit_event_category` is absent. Set to true to suppress.
SuppressEventCategory bool
// contains filtered or unexported fields
}
Taxonomy defines the complete set of audit event types, their categories, required and optional fields, and which categories are enabled by default. Consumers register a taxonomy at bootstrap via WithTaxonomy.
The framework does not hardcode any event types, field names, or categories. The only events the framework injects are "startup" and "shutdown" lifecycle events, which are added automatically if not already present.
func DevTaxonomy ¶
DevTaxonomy creates a permissive development taxonomy where every listed event type accepts any fields with no required fields. All events are placed in a single "dev" category.
DevTaxonomy is for prototyping and testing only. It accepts any event type with any fields and MUST NOT be used in production. New emits a log/slog warning when a DevTaxonomy is used.
func ParseTaxonomyYAML ¶
ParseTaxonomyYAML parses a YAML document into a *Taxonomy. The input MUST be a single YAML document containing a valid taxonomy definition. Unknown keys are rejected.
The returned Taxonomy is fully migrated, validated, and precomputed. Passing it to WithTaxonomy skips redundant re-validation.
Input errors (empty, multi-document, invalid syntax) wrap ErrInvalidInput. Taxonomy validation errors wrap ErrTaxonomyInvalid. On error, nil is returned.
ParseTaxonomyYAML accepts []byte only — no file paths, no readers. Use embed.FS or os.ReadFile in the caller to load from disk.
Trust model ¶
The taxonomy is developer-owned input. Callers typically embed it at compile time via embed.FS or load it from a path the developer controls; the library treats the document as trusted. ParseTaxonomyYAML imposes no input-size cap because, at the developer-trust boundary, such a cap would be ceremony rather than defense — a YAML alias bomb amplifies regardless of input size, and `goccy/go-yaml` does not expose an alias-budget guard. Memory usage scales linearly with the number of event types, field definitions, and sensitivity patterns.
Sensitivity precompute is O(events × fields × labels × patterns): taxonomies with many events AND many sensitivity patterns AND many fields per event will see noticeable parse-time cost. This is a per-load cost, not a per-event cost — the precomputed Taxonomy is then read in O(1) on the hot path.
Example ¶
package main
import (
"fmt"
"github.com/axonops/audit"
)
func main() {
// In production code, use //go:embed to load the YAML file.
data := []byte(`
version: 1
categories:
write:
- user_create
security:
- auth_failure
events:
user_create:
fields:
outcome: {required: true}
actor_id: {required: true}
auth_failure:
fields:
outcome: {required: true}
`)
tax, err := audit.ParseTaxonomyYAML(data)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("version:", tax.Version)
fmt.Println("events:", len(tax.Events))
}
Output: version: 1 events: 2
Example (SensitivityLabels) ¶
ExampleParseTaxonomyYAML_sensitivityLabels demonstrates defining sensitivity labels in a taxonomy and inspecting the resolved field labels after parsing.
package main
import (
"fmt"
"github.com/axonops/audit"
)
func main() {
data := []byte(`
version: 1
sensitivity:
labels:
pii:
description: "Personally identifiable information"
fields: [email]
patterns: ["_email$"]
financial:
fields: [card_number]
categories:
write:
- user_create
events:
user_create:
fields:
outcome: {required: true}
email: {}
card_number: {}
contact_email: {}
`)
tax, err := audit.ParseTaxonomyYAML(data)
if err != nil {
fmt.Println("error:", err)
return
}
def := tax.Events["user_create"]
for _, field := range []string{"email", "card_number", "contact_email", "outcome"} {
if labels, ok := def.FieldLabels[field]; ok {
names := make([]string, 0, len(labels))
for l := range labels {
names = append(names, l)
}
fmt.Printf("%s: %v\n", field, names)
} else {
fmt.Printf("%s: no labels\n", field)
}
}
}
Output: email: [pii] card_number: [financial] contact_email: [pii] outcome: no labels
Example (Validation) ¶
package main
import (
"errors"
"fmt"
"github.com/axonops/audit"
)
func main() {
// ParseTaxonomyYAML returns an error wrapping audit.ErrTaxonomyInvalid
// when the taxonomy is structurally inconsistent — here, a category
// references an event type that is not defined in the events map.
data := []byte(`
version: 1
categories:
ops:
- deploy
- nonexistent_event
events:
deploy:
fields:
outcome: {required: true}
`)
_, err := audit.ParseTaxonomyYAML(data)
if errors.Is(err, audit.ErrTaxonomyInvalid) {
fmt.Println("taxonomy validation failed")
}
}
Output: taxonomy validation failed
type TimestampFormat ¶
type TimestampFormat string
TimestampFormat controls how timestamps are rendered in serialised output. Unrecognised values default to TimestampRFC3339Nano.
const ( // TimestampRFC3339Nano renders timestamps as RFC 3339 with // nanosecond precision (e.g. "2006-01-02T15:04:05.999999999Z07:00"). // This is the default. TimestampRFC3339Nano TimestampFormat = "rfc3339nano" // TimestampUnixMillis renders timestamps as Unix epoch // milliseconds (e.g. 1709222400000). TimestampUnixMillis TimestampFormat = "unix_ms" )
type TransportMetadata ¶
type TransportMetadata struct {
// ClientIP is the client's IP address, extracted from the
// rightmost X-Forwarded-For entry, X-Real-IP, or RemoteAddr.
ClientIP string
// TransportSecurity describes the TLS state: "none", "tls", or "mtls".
TransportSecurity string
// Method is the HTTP method (GET, POST, etc.).
Method string
// Path is the request URL path.
Path string
// UserAgent is the request's User-Agent header value.
UserAgent string
// RequestID is the request identifier, taken from the X-Request-Id
// header or generated as a v4 UUID.
RequestID string
// Duration is the wall-clock time the handler took to execute.
Duration time.Duration
// StatusCode is the HTTP status code written by the handler.
StatusCode int
}
TransportMetadata contains HTTP transport-level fields captured automatically by the middleware. These are read-only values passed to the EventBuilder callback; handlers do not need to set them.
TransportMetadata is pool-managed: the pointer passed to EventBuilder is valid only for the duration of that callback. Copy any field values you need to retain; do not store the pointer itself, pass it to goroutines, or place it into the returned Fields map — the pool reset will zero every field before the next request sees the struct. See #501.
type ValidationError ¶
type ValidationError struct {
// contains filtered or unexported fields
}
ValidationError is returned by Auditor.AuditEvent for event validation failures. It wraps both ErrValidation and a specific sentinel (ErrUnknownEventType, ErrMissingRequiredField, ErrUnknownField, or ErrReservedFieldName). Use errors.Is to match broadly or narrowly, and errors.As to access the structured error:
var ve *audit.ValidationError
if errors.As(err, &ve) { log.Println(ve.Error()) }
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
Error returns the human-readable error message. The text is identical to the pre-sentinel format for backwards compatibility.
func (*ValidationError) Unwrap ¶
func (e *ValidationError) Unwrap() []error
Unwrap returns the sentinel errors that this validation error wraps. Always includes ErrValidation; also includes the specific sentinel when set.
The returned slice is a defensive copy — callers may retain or mutate it without affecting the ValidationError or subsequent Unwrap calls. The copy is a 16-byte allocation on the error-discrimination path; Unwrap is only invoked by errors.Is / errors.As, which are off the audit hot path (#590).
type ValidationMode ¶
type ValidationMode string
ValidationMode controls how Auditor.AuditEvent handles unknown fields (fields not listed in the event's Required or Optional lists).
Source Files
¶
- audit.go
- config.go
- control.go
- doc.go
- drain.go
- droplimit.go
- errors.go
- event.go
- fanout.go
- filter.go
- format.go
- format_cef.go
- format_cef_escape.go
- format_json.go
- hmac.go
- introspect.go
- metrics.go
- middleware.go
- migrate.go
- options.go
- options_output.go
- output.go
- postfield.go
- registry.go
- route.go
- sanitizer.go
- sensitivity.go
- ssrf.go
- std_fields.go
- stdout.go
- taxonomy.go
- taxonomy_internal.go
- taxonomy_yaml.go
- tls_policy.go
- transport.go
- validate_fields.go
- validate_taxonomy.go
- yaml_errors.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package audittest provides test helpers for consumers of the audit library.
|
Package audittest provides test helpers for consumers of the audit library. |
|
cmd
|
|
|
audit-gen
module
|
|
|
audit-validate
module
|
|
|
file
module
|
|
|
internal
|
|
|
testhelper
Package testhelper provides shared test utilities for the core audit module.
|
Package testhelper provides shared test utilities for the core audit module. |
|
iouring
module
|
|
|
loki
module
|
|
|
outputconfig
module
|
|
|
outputs
module
|
|
|
secrets
module
|
|
|
env
module
|
|
|
file
module
|
|
|
openbao
module
|
|
|
vault
module
|
|
|
splunk
module
|
|
|
syslog
module
|
|
|
tests
|
|
|
bdd/cmd/file-emfile-runner
command
Command file-emfile-runner is a hermetic test helper invoked by the BDD scenario "File output records RecordError when fd limit is exhausted on rotation" (#748).
|
Command file-emfile-runner is a hermetic test helper invoked by the BDD scenario "File output records RecordError when fd limit is exhausted on rotation" (#748). |
|
bdd/cmd/file-enospc-runner
command
Command file-enospc-runner is a hermetic test helper for the BDD scenario "File output records RecordError on ENOSPC" (#748).
|
Command file-enospc-runner is a hermetic test helper for the BDD scenario "File output records RecordError on ENOSPC" (#748). |
|
bdd/steps
Package steps provides Godog step definitions for audit BDD tests.
|
Package steps provides Godog step definitions for audit BDD tests. |
|
bdd/steps/genfixture
Package genfixture provides typed event builders generated by audit-gen from the BDD test taxonomy.
|
Package genfixture provides typed event builders generated by audit-gen from the BDD test taxonomy. |
|
bdd/steps/rfc5424
Package rfc5424 is a minimal RFC 5424 syslog parser used by the audit BDD harness to drive structural assertions on received messages (#572).
|
Package rfc5424 is a minimal RFC 5424 syslog parser used by the audit BDD harness to drive structural assertions on received messages (#572). |
|
webhook
module
|