obsx

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

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. Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Index

Constants

View Source
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

View Source
var (

	// AppName is the application name included in logs.
	AppName string
	// AppVersion is the application version included in logs.
	AppVersion string
)
View Source
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).

View Source
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

func EmitAuthAttempt(ctx context.Context, method string, ok bool)

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

func EmitGQLError(ctx context.Context, errorType string)

EmitGQLError records a GraphQL error by type.

func EmitGQLQuery

func EmitGQLQuery(ctx context.Context, operationName string, durationMs float64, ok bool)

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

func EmitGQLSubActive(ctx context.Context, delta int64)

EmitGQLSubActive adjusts the active GraphQL subscription gauge.

func EmitMCPRequest

func EmitMCPRequest(ctx context.Context, method string)

EmitMCPRequest records an MCP JSON-RPC request.

func EmitMCPToolCall

func EmitMCPToolCall(ctx context.Context, tool string, durationMs float64, ok bool)

EmitMCPToolCall records an MCP tool invocation.

func EmitPanic

func EmitPanic(ctx context.Context)

EmitPanic records a panic recovery event.

func EmitRateLimitReject

func EmitRateLimitReject(ctx context.Context, route string)

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

func EmitWSConnect(ctx context.Context, path string)

EmitWSConnect records a new WebSocket connection.

func EmitWSDisconnect

func EmitWSDisconnect(ctx context.Context, path string, durationMs float64)

EmitWSDisconnect records a WebSocket disconnection with duration.

func EmitWSFrame

func EmitWSFrame(ctx context.Context, path, direction string, n int)

EmitWSFrame records a WebSocket frame via MetricsProvider and OTEL.

func EmitWSUpgradeRejected

func EmitWSUpgradeRejected(ctx context.Context, path, reason string)

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

func IsSensitiveKey(key string, fragments []string) bool

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

func LogAudit(event string, fields map[string]any)

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 LogMCP

func LogMCP(event string, fields map[string]any)

LogMCP emits a JSON log entry for MCP events.

func LogWS

func LogWS(event string, fields map[string]any)

LogWS emits a JSON log entry for WebSocket events.

func Meter

func Meter(name string) metric.Meter

Meter returns a named meter. Returns a cached no-op meter when telemetry is disabled. Same rationale as Tracer above.

func Middleware

func Middleware(next http.Handler) http.Handler

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

func ObserveWSFrame(direction string, opcode byte, n int)

ObserveWSFrame records a WS message/frame size.

func RedactSensitiveKeys

func RedactSensitiveKeys(fields map[string]any, fragments []string) map[string]any

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

func RequestIDFrom(r *http.Request) string

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 SetLogger

func SetLogger(l *slog.Logger)

SetLogger configures the package-level logger. If l is nil, the call is a no-op.

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

func Tracer(name string) trace.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

func TracingMiddleware(next http.Handler) http.Handler

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.

func VarsHandler

func VarsHandler(auth func(*http.Request) bool) http.Handler

VarsHandler returns /debug/vars gated by an auth function. If auth returns false, 401.

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.

func Metrics

func Metrics() MetricsProvider

Metrics returns the current MetricsProvider.

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...)).

func (*RedactingHandler) Enabled

func (h *RedactingHandler) Enabled(ctx context.Context, l slog.Level) bool

Enabled delegates to the wrapped handler.

func (*RedactingHandler) Handle

func (h *RedactingHandler) Handle(ctx context.Context, r slog.Record) error

Handle redacts the record's attributes then delegates.

func (*RedactingHandler) WithAttrs

func (h *RedactingHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs redacts the bound attrs then delegates so persisted attrs are also covered.

func (*RedactingHandler) WithGroup

func (h *RedactingHandler) WithGroup(name string) slog.Handler

WithGroup delegates; group qualification does not change key matching.

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.

Directories

Path Synopsis
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.

Jump to

Keyboard shortcuts

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