Documentation
¶
Overview ¶
Package obsx is the observability layer for apic services, bundling structured logging, metrics, and tracing behind a small package-level API. It emits JSON request and audit logs through a buffered, periodically flushed stdout writer (with an optional slog-based hook and a value-redacting handler for sensitive fields), assigns request IDs, and records metrics via a swappable MetricsProvider whose default is expvar-backed (request counts, latency histograms, WebSocket frame counters, plus auth-attempt and rate-limit-rejection helpers). When an OpenTelemetry TelemetryProvider is configured it also creates OTEL instruments and a per-request tracing middleware with W3C trace-context propagation; with telemetry disabled that middleware is a zero-cost passthrough.
Index ¶
- Constants
- Variables
- func EmitAuthAttempt(ctx context.Context, method string, ok bool)
- func EmitGQLError(ctx context.Context, errorType string)
- func EmitGQLQuery(ctx context.Context, operationName string, durationMs float64, ok bool)
- func EmitGQLSubActive(ctx context.Context, delta int64)
- func EmitMCPRequest(ctx context.Context, method string)
- func EmitMCPToolCall(ctx context.Context, tool string, durationMs float64, ok bool)
- func EmitPanic(ctx context.Context)
- func EmitRateLimitReject(ctx context.Context, route string)
- func EmitWSConnect(ctx context.Context, path string)
- func EmitWSDisconnect(ctx context.Context, path string, durationMs float64)
- func EmitWSFrame(ctx context.Context, path, direction string, n int)
- func EmitWSUpgradeRejected(ctx context.Context, path, reason string)
- func FlushLogs()
- func IsSensitiveKey(key string, fragments []string) bool
- func LogAudit(event string, fields map[string]any)
- func LogMCP(event string, fields map[string]any)
- func LogWS(event string, fields map[string]any)
- func Meter(name string) metric.Meter
- func Middleware(next http.Handler) http.Handler
- func NewRequestID() string
- func ObserveWSFrame(direction string, opcode byte, n int)
- func RedactSensitiveKeys(fields map[string]any, fragments []string) map[string]any
- func RequestIDFrom(r *http.Request) string
- func SetAppInfo(name, version string)
- func SetLogger(l *slog.Logger)
- func SetMetrics(m MetricsProvider)
- func SetTelemetry(tp *TelemetryProvider)
- func TelemetryEnabled() bool
- func Tracer(name string) trace.Tracer
- func TracingMiddleware(next http.Handler) http.Handler
- func VarsHandler(auth func(*http.Request) bool) http.Handler
- type ExpvarMetrics
- type MetricsProvider
- type RedactingHandler
- type TelemetryProvider
Constants ¶
const RedactedValue = "[REDACTED]"
RedactedValue is the sentinel substituted for any map value whose key matched the sensitive-key list. Exported so tests and downstream tooling can compare against it without re-declaring the string.
Variables ¶
var ( // AppName is the application name included in logs. AppName string // AppVersion is the application version included in logs. AppVersion string )
var DefaultSensitiveKeyFragments = []string{
"authorization",
"authorization_code",
"bearer",
"ca_pem",
"cert_pem",
"cipher",
"code_challenge",
"code_verifier",
"csrf",
"dsn",
"encryption",
"hash",
"jwt",
"key_pem",
"password",
"pem",
"private_key",
"salt",
"secret",
"token",
}
DefaultSensitiveKeyFragments is the case-insensitive substring list used by RedactSensitiveKeys and LogAudit to identify map keys whose values must be replaced with RedactedValue before emission. Generators and applications can extend this list at init time, or supply their own list to RedactSensitiveKeys directly.
The list is intentionally conservative: false positives (over-redaction) are far cheaper than false negatives (leaking a secret to an audit channel). All matches are case-insensitive substring matches: a key "user_password_hash" matches both "password" and "hash".
Adding to this slice from package code at init time is safe; mutating it concurrently with LogAudit / RedactSensitiveKeys calls is not. Audit-call convention: when emitting an OAuth/OIDC authorization code into an audit record, the canonical key is "authorization_code" — never the bare "code". The bare form would substring-collide with benign fields like "status_code" and "error_code" and force the redaction policy to either over-redact debugging signals or leak credentials. The explicit "authorization_code" fragment below is redundant with "authorization" (case-insensitive substring) but is recorded independently so the policy intent is unambiguous and so a future narrower matcher cannot silently drop the protection (A-NEW-9).
var (
ErrInvalidConfig = errors.New("obsx: invalid configuration")
)
ErrInvalidConfig is returned when obsx receives an invalid configuration. Kept at package level per repo conventions; message uses a lowercase, package-prefixed, diagnostic form so log output reads naturally.
Functions ¶
func EmitAuthAttempt ¶
EmitAuthAttempt records an auth attempt via MetricsProvider and OTEL.
A-P1.3: the default ExpvarMetrics provider IGNORES the labels map (`pkg/obsx/metrics.go:52` — labels are passed and discarded). Pre-fix every authed request built a fresh `map[string]string{"method":..., "status":...}` even when the operator never wired a label-aware provider. Pass nil for the default path; the OTel branch below builds attributes directly via `attribute.String` and never needed the intermediate map.
func EmitGQLError ¶
EmitGQLError records a GraphQL error by type.
func EmitGQLQuery ¶
EmitGQLQuery records a GraphQL query execution. The operation name is supplied by the (possibly untrusted) GraphQL client and is sanitized by safeOperationLabel before being used as a metric label to bound cardinality. APPSEC-12.
func EmitGQLSubActive ¶
EmitGQLSubActive adjusts the active GraphQL subscription gauge.
func EmitMCPRequest ¶
EmitMCPRequest records an MCP JSON-RPC request.
func EmitMCPToolCall ¶
EmitMCPToolCall records an MCP tool invocation.
func EmitRateLimitReject ¶
EmitRateLimitReject records a rate limit rejection.
A-P1.3: same rationale as EmitAuthAttempt — the default Expvar provider drops labels; pass nil to skip the per-request map alloc.
func EmitWSConnect ¶
EmitWSConnect records a new WebSocket connection.
func EmitWSDisconnect ¶
EmitWSDisconnect records a WebSocket disconnection with duration.
func EmitWSFrame ¶
EmitWSFrame records a WebSocket frame via MetricsProvider and OTEL.
func EmitWSUpgradeRejected ¶
EmitWSUpgradeRejected records a WebSocket upgrade rejection from the apic-emitted pre-handshake gate (GAP-0075). The reason label distinguishes the wsx.Limiter sentinel that fired so operators can attribute rejections without spelunking through logs.
Reason values are short, stable identifiers expected by dashboards: "total_cap", "per_ip_cap", "rate", or an operator-defined string for caller-extension scenarios. Path is the WebSocket route as configured (e.g. "/ws/feed").
func FlushLogs ¶
func FlushLogs()
FlushLogs flushes any buffered JSON log records to os.Stdout. Consumers MUST defer FlushLogs() in main() (and call it immediately before any os.Exit on a fatal path) so log lines buffered by the obsx logger are not lost on shutdown. It is safe to call concurrently and repeatedly.
func IsSensitiveKey ¶
IsSensitiveKey reports whether the lower-cased form of key contains any substring from fragments. fragments must already be lower-cased; this is the same convention DefaultSensitiveKeyFragments enforces.
func LogAudit ¶
LogAudit emits a JSON audit log entry. The audit event identifier is written under the "audit_event" key, and the rest of fields is emitted alongside. Before emission, every value whose key (case-insensitive) contains a substring from DefaultSensitiveKeyFragments is replaced with RedactedValue. This guards against accidental secret leaks from call sites that forget to scrub credentials, JWTs, TLS keys, etc. before logging.
Callers that need a custom redaction list (e.g. to add app-specific secret-key prefixes) should call RedactSensitiveKeys explicitly and then call LogAudit with the redacted map; LogAudit always re-applies the default list on top, so an app-specific list is a strict superset of the defaults.
func Meter ¶
Meter returns a named meter. Returns a cached no-op meter when telemetry is disabled. Same rationale as Tracer above.
func Middleware ¶
Middleware adds a request id, emits JSON logs on start/end, exposes basic counters and latency histograms, and recovers handler panics into a structured 500 (F1). Without this recovery a panic hits net/http's per-connection recover (aborted connection, no structured body, no metric, no request id).
func NewRequestID ¶
func NewRequestID() string
NewRequestID creates a new request id string. The encoding is "<base36-counter>-<base36-unix-nanos>". Implemented with stack-allocated scratch buffers so the only heap allocation is the returned string itself (a single allocation per call, vs. the prior format/concat path which allocated three).
func ObserveWSFrame ¶
ObserveWSFrame records a WS message/frame size.
func RedactSensitiveKeys ¶
RedactSensitiveKeys returns a copy of fields with any value whose key (case-insensitive) contains a substring in fragments replaced by RedactedValue. When fragments is nil the function uses DefaultSensitiveKeyFragments. When fields is nil the function returns nil. The returned map is always a fresh allocation -- callers can safely store it without aliasing the caller's map.
This is the single centralised redaction helper used by LogAudit and by application code (see geode-ui internal/server/audit.go). Keeping it in one place ensures every channel that emits audit-style records uses the same redaction rules.
func RequestIDFrom ¶
RequestIDFrom returns the existing request id from headers if present, otherwise generates a new one.
func SetAppInfo ¶
func SetAppInfo(name, version string)
SetAppInfo configures the application identity for logging.
func SetMetrics ¶
func SetMetrics(m MetricsProvider)
SetMetrics configures the package-level metrics provider. If m is nil, the call is a no-op.
func SetTelemetry ¶
func SetTelemetry(tp *TelemetryProvider)
SetTelemetry configures OTEL telemetry. Pass nil to disable. When non-nil, OTEL instruments are created eagerly.
func TelemetryEnabled ¶
func TelemetryEnabled() bool
TelemetryEnabled returns true when a TelemetryProvider is configured.
func Tracer ¶
Tracer returns a named tracer. Returns a cached no-op tracer when telemetry is disabled.
A-P1.1 (2026-05-28): the fallback path used to call `tracenoop.NewTracerProvider().Tracer(name)` on every request, allocating a provider + tracer pair before returning. With telemetry off (the default deployment), the no-op tracer is the same object on every call — cache it with sync.Once and reuse. The `name` argument is intentionally ignored for the no-op tracer: tracenoop's tracer ignores name internally, so returning a single shared tracer is observationally identical.
func TracingMiddleware ¶
TracingMiddleware creates an OTEL span per HTTP request with W3C trace context propagation. When telemetry is disabled (nil provider), it is a zero-cost passthrough.
Types ¶
type ExpvarMetrics ¶
type ExpvarMetrics struct {
// contains filtered or unexported fields
}
ExpvarMetrics implements MetricsProvider using expvar.
Per-name *expvar.Int counters are cached in a sync.Map so steady-state IncCounter / ObserveHistogram calls hit the hot path without taking any locks. The mutex is only used for the first-time publish race.
func (*ExpvarMetrics) IncCounter ¶
func (e *ExpvarMetrics) IncCounter(name string, _ map[string]string)
func (*ExpvarMetrics) ObserveHistogram ¶
func (e *ExpvarMetrics) ObserveHistogram(name string, value float64, _ map[string]string)
type MetricsProvider ¶
type MetricsProvider interface {
IncCounter(name string, labels map[string]string)
ObserveHistogram(name string, value float64, labels map[string]string)
}
MetricsProvider abstracts counter and histogram metrics.
type RedactingHandler ¶
type RedactingHandler struct {
// contains filtered or unexported fields
}
RedactingHandler is a slog.Handler middleware that replaces the value of any record attribute whose key matches the sensitive-key allowlist with RedactedValue, before delegating to the wrapped handler (F5). It applies the same policy as RedactSensitiveKeys / LogAudit so every log channel redacts consistently — not just LogAudit callers. Group-qualified attributes and nested map values are walked recursively.
func NewRedactingHandler ¶
func NewRedactingHandler(next slog.Handler, fragments []string) *RedactingHandler
NewRedactingHandler wraps next so attribute values under sensitive keys are redacted. When fragments is nil the DefaultSensitiveKeyFragments allowlist is used; a non-nil list REPLACES the defaults (callers wanting both should pass append(append([]string(nil), DefaultSensitiveKeyFragments...), extra...)).
type TelemetryProvider ¶
type TelemetryProvider struct {
TracerProvider trace.TracerProvider
MeterProvider metric.MeterProvider
}
TelemetryProvider wraps OpenTelemetry's TracerProvider and MeterProvider. Deployers configure SDK exporters externally; apic uses the OTEL API only.
func Telemetry ¶
func Telemetry() *TelemetryProvider
Telemetry returns the current TelemetryProvider, or nil if disabled.