obs

package
v1.0.205 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

guard.go — CHAOS-24 background-worker panic containment.

Go terminates the process on an unrecovered panic in ANY goroutine. Culvert is an in-line security appliance, so a panic in a long-lived background worker (a feed sync parsing third-party data, a health poller, a log drain) is a total gateway outage — every in-flight tunnel dropped — triggered by input the operator does not control. package main already contains the crash-recording plane (crashguard.go: recordCrash + the culvert_crash_records_total{component} counter + the system-actor audit entry), but internal/* leaf packages cannot import package main (ADR-0003), so the workers that live in internal/ had no way to reach it.

This file is the seam. It mirrors the SetSink pattern already used for log lines: internal packages call Guard/SafeCall, and package main publishes the real handler once at startup (SetPanicSink → recordCrash), so a recovered worker panic lands in exactly the same metric/audit/log pipeline as a recovered proxy or admin panic. No new observability surface is introduced.

CONTAINMENT SEMANTICS — the rule that makes this safe:

Guard the loop BODY (one iteration), never the goroutine.

Recovering at the goroutine level would let the worker RETURN, converting a loud crash into a silent permanent stall — strictly worse, and in two cases actively dangerous:

  • reqlog's drain goroutine owns a BLOCKING queue (persist.go: a full queue parks the caller rather than dropping the durable audit record). If that goroutine exits, every request goroutine eventually blocks forever in Add: the proxy wedges with no crash, no restart, and no alert.
  • a fencing-lease keepalive that stops renewing while still believing it holds write authority is a split-brain vector (see ha_lease.go, where a panicking renew round is charged against the confirmed-validity window and self-fences rather than being retried blindly).

So the contract is: contain the iteration, report it, keep the loop alive — and where "keep going" is not the safe answer, the CALLER inspects SafeCall's return and fails closed explicitly.

Package obs is the shared logging facade for internal/* packages (ADR-0003). It lets leaf packages emit log lines and sanitise user input without importing package main. package main wires the sink once at startup (SetSink) so these lines flow into the same rotating/JSON logger; before that, and in unit tests, the default sink writes to stderr.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Debugf added in v1.0.16

func Debugf(format string, args ...any)

Debugf formats with a DEBUG prefix and sends the line to the configured sink, but only while debug logging is enabled (SetDebugEnabled).

func Guard added in v1.0.190

func Guard(component string)

Guard is the deferred one-liner for a worker loop body:

func (w *worker) tick() {
	defer obs.Guard("threatfeed")
	...
}

It recovers, reports, and returns normally so the surrounding loop continues to its next iteration. Guard must NOT be deferred at the top of a long-lived goroutine — that would let the goroutine exit on panic (see the file header).

func Printf

func Printf(format string, args ...any)

Printf formats at INFO level and sends the line to the configured sink.

func ReportPanic added in v1.0.190

func ReportPanic(component string, v any)

ReportPanic hands an already-recovered value to the sink. Use it when the caller must run its own cleanup in the same deferred function that recovers — recover() only works when called DIRECTLY by the deferred func, so such a caller cannot delegate to Guard. internal/reqlog's drain does this: it has to discard a poisoned write buffer before reporting.

func SafeCall added in v1.0.190

func SafeCall(component string, fn func()) (panicked bool)

SafeCall runs fn with a panic guard and reports whether it panicked. Use it when the loop needs to KNOW a round failed — a fail-closed worker must not treat a panicking round as a successful one (ha_lease.go charges a panicking keepalive round against the lease-validity window instead of retrying it blindly, so panic containment can never manufacture a split brain).

Callers that simply want the loop to continue can ignore the return value.

func Sanitize

func Sanitize(s string) string

Sanitize strips control characters from s to prevent log injection (CWE-117).

This is an INDEPENDENT copy of package main's sanitizeLog (proxy.go), kept separate on purpose: CodeQL's CWE-117 query recognises the inline strings.ReplaceAll sanitiser at each call site, and delegating across a package boundary risks losing that recognition. The two copies are tiny and each behaviour-tested; the property (no control byte survives) is the contract.

func SetDebugEnabled added in v1.0.16

func SetDebugEnabled(on bool)

SetDebugEnabled publishes whether debug-level lines should emit. Called by package main's SetLogLevel on every level change.

func SetPanicSink added in v1.0.190

func SetPanicSink(fn func(component string, v any))

SetPanicSink publishes the handler for recovered worker panics. Call once at startup, before workers run. A nil fn is ignored (the default WARN sink stays), so a mis-wired startup can never silence panic reporting entirely.

func SetSink

func SetSink(fn func(line string))

SetSink publishes the log destination. Call once at startup, before serving traffic. A nil fn is ignored. Subsequent calls replace the sink atomically.

func Warnf

func Warnf(format string, args ...any)

Warnf formats with a WARN prefix and sends the line to the configured sink. Note: unlike package main's logWarnf, this does not apply main's runtime log-level filter (that state lives in main); internal warnings always emit.

Types

This section is empty.

Jump to

Keyboard shortcuts

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