observability

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: May 1, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package observability provides OpenTelemetry tracing for the ox CLI.

Creates one trace per CLI command invocation. All HTTP requests within a command share the same trace ID, appearing as children of the root span in Honeycomb. This gives CLI↔server timing visibility.

Index

Constants

View Source
const (
	// AttrOXVersion is the ox CLI/daemon version string. Always set on the
	// root command span so traces can be sliced by release without joining
	// against build metadata.
	AttrOXVersion = "ox.version"

	// AttrHostOS is the operating system the binary is running on
	// (linux/darwin/windows). Always set on the root command span.
	AttrHostOS = "host.os"

	// AttrHostArch is the CPU architecture the binary was built for
	// (amd64/arm64/...). Cheap to capture and useful for narrowing
	// platform-specific regressions.
	AttrHostArch = "host.arch"

	// AttrCLIExitCode is the integer exit code the CLI process returned.
	// Set unconditionally from main() after Execute returns, on both
	// success (0) and error (non-zero) paths.
	AttrCLIExitCode = "cli.exit_code"

	// AttrCLIArgs is the redacted command-line arguments. Flag NAMES are
	// preserved; flag VALUES and positional arguments are replaced with
	// "<REDACTED>" by RedactArgs. The intent is to make "which flags do
	// users pass to ox code search" queryable without ever exporting user
	// content.
	AttrCLIArgs = "ox.command.args"

	// AttrResultCount is the size of the result set returned by a command
	// that produces results (search, query, list). Set best-effort by
	// individual commands via SetResultCount; not every command has one.
	AttrResultCount = "result.count"

	// AttrAgentEnv identifies the AI coworker that invoked ox, sourced
	// from the AGENT_ENV environment variable that adapter hooks set
	// (claude-code, aider, gemini, codex, ...). Only present when the
	// command is being driven by an AI coworker — human-driven
	// invocations leave this attribute off the span entirely so a
	// `ox.agent.env exists` query in the trace backend cleanly isolates
	// agent traffic from human traffic.
	AttrAgentEnv = "ox.agent.env"

	// AttrCommandSubcommand is the resolved subcommand path for commands
	// that go through a dispatcher. Cobra's PreRunE creates the root
	// span before the dispatcher resolves what was actually invoked, so
	// every dispatcher hit would otherwise share one bland span name
	// (e.g. "ox agent"). RenameRootSpan attaches this attribute alongside
	// the renamed span so backends can group by it even if a future
	// refactor changes the span-name scheme — and so high-cardinality
	// suffixes like hook phase names don't bloat span-name buckets.
	AttrCommandSubcommand = "ox.command.subcommand"
)

Span attribute keys for ox CLI traces.

These are exported as constants so call sites in cmd/ox don't typo a key and end up with two attributes for the same concept. New keys belong here.

Naming follows two conventions:

  • OTel semantic conventions where they exist (host.os, cli.exit_code).
  • The "ox.*" namespace for everything else, mirroring how the daemon scopes its task spans.
View Source
const RedactedPlaceholder = "<REDACTED>"

RedactedPlaceholder is the string that replaces every redacted token in the output of RedactArgs. Exposed as a constant so tests and downstream code can match against it without hardcoding the literal.

Variables

This section is empty.

Functions

func CommandName

func CommandName(parts ...string) string

CommandName extracts a readable command path for span naming. e.g., "ox login", "ox agent prime", "ox session stop"

func Enabled

func Enabled() bool

Enabled returns true if OTel tracing is initialized.

func Init

func Init(ctx context.Context, serviceName, apiEndpoint string, attrs ...attribute.KeyValue) error

Init sets up the OTel TracerProvider with OTLP/HTTP export to the SageOx OTLP proxy at {apiEndpoint}/api/v1/otlp/v1/traces.

Extra attrs are merged into the OTel resource alongside service.name. The daemon uses this to set client.id, client.class, os.type, etc.

Safe to call with empty apiEndpoint — tracing is disabled (noop).

func RedactArgs

func RedactArgs(args []string) []string

RedactArgs returns a copy of args suitable for tracing or telemetry, preserving flag *names* but replacing every flag *value* and every positional argument with RedactedPlaceholder.

The goal is to make `ox.command.args` queryable ("which flags are users passing to ox code search?") without leaking any user content ("what is the user searching for?"). The conservative default is to redact aggressively: free-form input belongs in command-specific attributes (e.g. ox.code.search.query) where redaction can be tuned to the field, not at the args level.

Behavior:

  • "--name=value" -> "--name=<REDACTED>"
  • "--name", "value" -> "--name", "<REDACTED>"
  • "--name", "-5" -> "--name", "<REDACTED>" (closes the "value starts with -" leak)
  • "-n", "value" -> "-n", "<REDACTED>"
  • "positional" -> "<REDACTED>"
  • "--bool", "--other" -> "--bool", "--other" (chained long flags are preserved as flag NAMES)
  • "--", "rest..." -> "--", "<REDACTED>", "<REDACTED>", ...

We do NOT have a flag schema here, so we cannot distinguish a boolean flag from a value-taking flag. The discriminator we use is whether the next token starts with "--" — a true long flag begins with "--" by convention, so this lets us preserve chained boolean long flags (`--verbose --json`) without keeping anything that could plausibly be a value. Tokens starting with a single "-" (e.g. "-5", "-k", "-foo") are treated as values for the preceding flag and redacted.

Known residual: a value that *literally* begins with "--" (e.g. `--message --secret-token` where `--secret-token` is meant as a string value, not a flag) will be preserved verbatim. This is statistically vanishingly rare for real secrets and was an explicit trade-off to keep `--verbose --json`-style chained long flags queryable. A follow-up PR can take a flag schema from cobra at the call site and disambiguate exactly.

Pure function. Returns a fresh slice — the input is never mutated.

func RenameRootSpan

func RenameRootSpan(name, subcommand string)

RenameRootSpan rewrites the name of the root command span and attaches an ox.command.subcommand attribute. Used by command dispatchers — most notably the `ox agent <agent_id> <cmd>` dispatcher in cmd/ox/agent.go — that need to refine the span name once they have resolved the actual invocation.

Why this exists: cobra's PersistentPreRunE creates the root span before the dispatcher's RunE runs, so the span starts life with a bland name like "ox agent" because cobra only sees the agentCmd. Without renaming, every dispatcher hit ends up in one giant bucket in the trace backend and you can't answer "which dispatcher path is slow / erroring?".

The subcommand string is also attached as an attribute (AttrCommandSubcommand) so dashboards can group by it independently of the span name. Empty subcommand strings are not attached.

Should be called BEFORE any early-return paths in the dispatcher (unknown agent_id, validation errors, help) so even fast failures end up under the right span name.

Safe to call when tracing is disabled.

func SetCommandAttrs

func SetCommandAttrs(version string, args []string)

SetCommandAttrs sets the universal attributes that should be present on every CLI root span: ox.version, host.os, host.arch, ox.command.args, and (when set) ox.agent.env.

Safe to call when tracing is disabled (no-op if there is no root span). Args are redacted via RedactArgs before being attached.

Should be called once per command, immediately after StartCommand. The CLI bootstrap in internal/cli.NewContext does this; downstream code does not need to.

func SetCommandStatus

func SetCommandStatus(err error)

SetCommandStatus records whether the command succeeded or failed on the root span. Call before Shutdown.

func SetExitCode

func SetExitCode(code int)

SetExitCode records the process exit code on the root command span. Called from main() after rootCmd.Execute returns, so the attribute is present on both success and error paths — even when PersistentPostRunE never ran (cobra skips PostRunE on RunE errors).

On non-zero exit codes the span status is also marked Error so backends can filter for failed commands without inspecting the exit code field.

Safe to call when tracing is disabled.

func SetResultCount

func SetResultCount(n int)

SetResultCount attaches a result.count attribute to the root command span. Best-effort: callers in commands that produce a result set (search, query, list) call this once they know the count. Commands without a natural result set should not call it.

Safe to call when tracing is disabled.

func Shutdown

func Shutdown(ctx context.Context)

Shutdown ends the root span and flushes pending exports. Blocks up to 3s for flush. Safe to call if Init was never called.

func StartCommand

func StartCommand(ctx context.Context, commandName string) (context.Context, trace.Span)

StartCommand creates a root span for a CLI command invocation. All HTTP requests within this command will be children of this span.

func TraceParent

func TraceParent() string

TraceParent returns the W3C traceparent header value from the current root span. Returns empty string if tracing is not active.

Types

type DaemonTracer

type DaemonTracer struct {
	// contains filtered or unexported fields
}

DaemonTracer provides per-task trace creation for the daemon. Unlike the CLI (one root span per command), the daemon creates many independent traces — one per scheduled task execution.

All methods are nil-safe: callers never need to check whether tracing is enabled.

func NewDaemonTracer

func NewDaemonTracer() *DaemonTracer

NewDaemonTracer returns a tracer for daemon tasks. Must be called after Init() has set up the TracerProvider. Returns nil if tracing is not enabled.

func (*DaemonTracer) StartSubtask

func (dt *DaemonTracer) StartSubtask(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span)

StartSubtask creates a child span under the current span in ctx.

func (*DaemonTracer) StartTask

func (dt *DaemonTracer) StartTask(ctx context.Context, taskName string, attrs ...attribute.KeyValue) (context.Context, trace.Span)

StartTask creates a new root span for a daemon task. Each call produces an independent trace (no parent), so the daemon generates many short traces over its lifetime rather than one long one.

Example:

ctx, span := dt.StartTask(ctx, "daemon:gc_check")
defer span.End()

Jump to

Keyboard shortcuts

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