obs

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: May 29, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package obs is Dockyard's observability protocol — obs/v1 (RFC §11).

obs/v1 is a headless, canonical, versioned event stream. The app runtime EMITS obs/v1 events; the inspector (RFC §12) and the post-V1 multi-server console are pure CLIENTS of that contract and never read runtime internals (P2, CLAUDE.md §1, §6). If a subsystem needs a signal observed, it adds an obs/v1 event — never a back channel.

The contract

obs/v1 is a public, documented, third-party-consumable contract (RFC §11.3, CLAUDE.md §8). The serialized shape of Event is stable from V1: a change to the JSON shape is a versioned change (SchemaVersion bumps), documented, never silent. The wire shape is pinned by golden tests.

The emitter seam

The runtime depends only on the Emitter interface. obs follows the interface + factory + driver pattern mandated by CLAUDE.md §4.4: a driver registers a factory in its init block via RegisterDriver, and Open constructs an Emitter by driver name. Phase 15 ships the ring-buffer driver (RingBuffer, driver name "ringbuffer"); Phase 16 adds the out-of-band SSE sink and the optional OTel adapter behind the same seam, and bridges the MCP logging capability into obs/v1 log events.

Non-blocking

Emit paths are non-blocking: the runtime never blocks on a slow consumer (CLAUDE.md §8). The ring-buffer driver is a bounded ring — a full buffer drops its oldest event, it never stalls an emitter. A multi-driver FanOut is likewise bounded and drop-on-pressure. Every emitter and the ring buffer are reusable concurrent artifacts: safe for concurrent Emit from many goroutines and concurrent reads (CLAUDE.md §5).

Capture policy

Tool input/output capture defaults to shape + size only — never full content (CLAUDE.md §7). Shape computes the structural fingerprint and byte size of a JSON value; full-content capture is opt-in and redaction-aware and is left as a designed-but-deferred hook (CapturePolicy) — see RFC §11.2 and the Phase 15 plan's scope boundary.

Index

Constants

View Source
const DefaultRingCapacity = 1024

DefaultRingCapacity is the RingBuffer capacity used when none is specified — enough recent history for an interactive inspector session without unbounded memory growth.

View Source
const SchemaVersion = "dockyard.obs/v1"

SchemaVersion is the obs/v1 schema identifier carried by every Event. It is a public, versioned contract (RFC §11.3): a change to the Event JSON shape bumps this value, is documented, and is never silent (CLAUDE.md §8). The golden tests in this package pin the serialized shape so an accidental change fails CI.

Variables

This section is empty.

Functions

func Drivers

func Drivers() []string

Drivers returns the names of all registered emitter drivers, sorted.

func RegisterDriver

func RegisterDriver(name string, f Factory)

RegisterDriver registers an emitter-driver factory under name. It is called from a driver package's init block (blank-import registration, CLAUDE.md §4.4). Registering the same name twice panics — a duplicate registration is a programming error caught at process start. The ring-buffer driver registers itself under "ringbuffer"; Phase 16's SSE and OTel drivers register under their own names behind this same seam.

func WithInboundTrace

func WithInboundTrace(ctx context.Context, parent SpanContext) context.Context

WithInboundTrace returns a copy of ctx carrying parent as the trace identity extracted by a transport-layer propagator from an inbound request. A handler-edge call site that begins a new unit of work calls NewTraceFromContext (not NewTrace) so the unit of work inherits this parent when one is present. A zero-value parent is a no-op — leaves ctx unchanged — so a transport that finds no traceparent simply skips the call.

func WithSession

func WithSession(ctx context.Context, sessionID string) context.Context

WithSession returns a copy of ctx carrying an MCP session identity. Every obs/v1 event emitted from a Recorder on ctx then carries SessionID equal to sessionID — the correlation seam by which a transport-layer session identity reaches the emit sites (R5 — depth-audit remediation; D-120).

runtime/server stamps this on the tool-handler context (alongside WithSpan) from req.Session.ID(); the same threading lands on the resource-handler edge. A nil/empty sessionID is a no-op and leaves ctx unchanged — an out-of-request emit site (a server.lifecycle event, an out-of-request log record) is correctly emitted with no session.

func WithSpan

func WithSpan(ctx context.Context, sc SpanContext) context.Context

WithSpan returns a copy of ctx carrying sc as the in-flight span. A subsystem that opens a span (a tools/call, a resources/read) stamps it here so any obs/v1 event emitted from *inside* that unit of work — most notably a handler-emitted `log` event via the MCP-logging bridge — can derive a child span of the enclosing one rather than minting an unrelated fresh trace.

This is the correlation seam the Wave 6 checkpoint item S1 closes: before it, a `log` event emitted inside a tool handler was not trace-correlated to its `tool.call`. A zero-value sc leaves ctx unchanged. It mirrors WithSession: the context is the carrier, the emit site reads it.

Types

type AppBridgePayload

type AppBridgePayload struct {
	// ResourceURI is the ui:// URI of the App whose bridge this concerns.
	ResourceURI string `json:"resource_uri"`
	// BridgeReady reports whether the ui/initialize handshake completed.
	BridgeReady bool `json:"bridge_ready"`
}

AppBridgePayload is the payload of a KindAppBridge event — the ui/initialize bridge handshake state. Dockyard sees only its half of the iframe bridge (brief 05 §2.5): "served the resource, handshake received or not".

type AppLoadPayload

type AppLoadPayload struct {
	// AppID is the App's identifier (its ui:// URI is ResourceURI).
	AppID string `json:"app_id,omitempty"`
	// ResourceURI is the ui:// URI of the served App resource.
	ResourceURI string `json:"resource_uri"`
	// MIME is the served MIME type — text/html;profile=mcp-app for an App.
	MIME string `json:"mime,omitempty"`
	// Bytes is the size of the served HTML bundle.
	Bytes int `json:"bytes"`
}

AppLoadPayload is the payload of a KindAppLoad event — a ui:// App resource served to a host (RFC §7, brief 05 §3.2).

type CapturePolicy

type CapturePolicy int

CapturePolicy controls how much of a tool's input/output an emitted event carries. The default is shape + size only — never full content (CLAUDE.md §7, RFC §11.2): tool arguments and results can carry secrets and PII, so obs/v1 captures the structural fingerprint and byte size by default.

CapturePolicyFull is the designed-but-deferred opt-in hook for full-content capture. Phase 15 ships the shape+size default fully wired; full-content capture is opt-in and MUST be redaction-aware before any content is captured — that redaction pipeline is out of Phase 15 scope (see the Phase 15 plan's scope boundary). An emitter that receives CapturePolicyFull without a configured redactor falls back to shape+size — full capture is never the silent default.

const (
	// CapturePolicyShape captures only the structural shape and byte size of a
	// value — the safe, content-free default.
	CapturePolicyShape CapturePolicy = iota
	// CapturePolicyFull is the opt-in full-content capture mode. It is honoured
	// only when a redaction-aware [Redactor] is configured; otherwise an
	// emitter degrades to CapturePolicyShape. Phase 15 defines the hook; the
	// redaction pipeline is later scope.
	CapturePolicyFull
)

type Closer

type Closer interface {
	// Close releases the emitter's resources. It is idempotent.
	Close() error
}

Closer is implemented by an Emitter that holds resources (a driver with a background goroutine, an open socket). [Close] closes every driver in a FanOut that implements it.

type Emitter

type Emitter interface {
	// Emit records an event. It is non-blocking and never panics. A malformed
	// event is dropped silently — a buggy emit site never crashes a request.
	Emit(ctx context.Context, e Event)
}

Emitter is the obs/v1 emit seam. The runtime depends ONLY on this interface; the inspector, the SSE sink, and the OTel adapter are drivers behind it (CLAUDE.md §4.4). An Emitter is a reusable concurrent artifact: a single value is safe for Emit from many goroutines (CLAUDE.md §5).

Emit MUST be non-blocking: an emitter never blocks the runtime on a slow consumer (CLAUDE.md §8). A driver that cannot keep up drops events; it must not stall the caller. Emit takes a context for cancellation propagation and trace correlation, but it returns no error — observability never fails a request (P2).

func Open

func Open(driver, cfg string) (Emitter, error)

Open constructs an Emitter using the named driver. The driver package must be imported so its init block has registered the factory. Open returns an error if no such driver is registered.

type ErrorInfo

type ErrorInfo struct {
	// Type is a stable, low-cardinality error class — it maps to OTel
	// error.type. Example: "handler_error", "validation_error".
	Type string `json:"type"`
	// Message is the human-readable error detail.
	Message string `json:"message"`
	// Retryable hints whether retrying the operation could succeed.
	Retryable bool `json:"retryable,omitempty"`
	// Silent flags a protocol-masked failure: a failure the MCP transport
	// would otherwise hide (e.g. an error swallowed on the stdio pipe). This is
	// a first-class signal — the inspector surfaces it prominently.
	Silent bool `json:"silent,omitempty"`
}

ErrorInfo describes a failure carried by an Event. It lowers cleanly onto the OTel error.type attribute (RFC §11.3); the Phase 16 OTel adapter consumes it without obs needing an OTel dependency.

type Event

type Event struct {
	// SchemaVersion is always [SchemaVersion] for obs/v1 emitters. A consumer
	// keys parsing on it.
	SchemaVersion string `json:"schema_version"`
	// ID uniquely identifies this event. It is a crypto-random 128-bit hex
	// string (see newEventID) — distinct from the W3C trace/span IDs.
	ID string `json:"id"`
	// Timestamp is when the event was recorded, in UTC.
	Timestamp time.Time `json:"timestamp"`

	// ServerID is the stable identity of the emitting server.
	ServerID string `json:"server_id"`
	// SessionID is the MCP session the event belongs to, when known.
	SessionID string `json:"session_id,omitempty"`

	// TraceID is the W3C Trace Context trace-id (16 bytes, 32 lowercase hex)
	// correlating a whole call chain. A Dockyard server's spans nest natively
	// under a calling Harbor agent's execute_tool span (RFC §11.2).
	TraceID string `json:"trace_id"`
	// SpanID is the W3C Trace Context span-id (8 bytes, 16 lowercase hex) of
	// this unit of work.
	SpanID string `json:"span_id"`
	// ParentSpanID is the span-id of the enclosing span, when there is one.
	ParentSpanID string `json:"parent_span_id,omitempty"`

	// Kind classifies the event (see [EventKind]).
	Kind EventKind `json:"kind"`
	// Phase is the lifecycle position (see [Phase]).
	Phase Phase `json:"phase"`
	// Payload is the kind-specific typed payload, JSON-encoded. The concrete
	// Go shapes are in payload.go; a consumer decodes per Kind.
	Payload json.RawMessage `json:"payload,omitempty"`

	// DurationMS is the elapsed milliseconds of a completed unit of work. It is
	// set on Phase=end events and omitted otherwise.
	DurationMS *int64 `json:"duration_ms,omitempty"`
	// Error is set when the unit of work failed. ErrorInfo.Silent flags a
	// protocol-masked failure — the class of bug stdio transport hides
	// (brief 05 §2.2, the Sentry insight).
	Error *ErrorInfo `json:"error,omitempty"`
}

Event is the canonical obs/v1 observability event — the ONLY type the inspector and the post-V1 console consume (RFC §11.2). No raw runtime or SDK type leaks through it (P2/P3). The JSON shape is a stable, versioned contract pinned by golden tests; field order in the struct is the documented wire order.

type EventKind

type EventKind string

EventKind classifies an Event. The kinds cover the tool, resource, prompt, app, task, host-compat, log, and server-lifecycle surfaces of a Dockyard server (RFC §11.2, brief 05 §3.1). The set is closed for obs/v1: a new kind is a versioned addition.

const (
	// KindToolCall is the tools/call lifecycle of a contract-first tool.
	KindToolCall EventKind = "tool.call"
	// KindResourceRead is a resources/read of a registered resource.
	KindResourceRead EventKind = "resource.read"
	// KindPromptGet is a prompts/get of a registered prompt.
	KindPromptGet EventKind = "prompt.get"
	// KindAppLoad is a ui:// App resource served to a host (RFC §7).
	KindAppLoad EventKind = "app.load"
	// KindAppBridge is the ui/initialize bridge handshake — bridge up/down.
	KindAppBridge EventKind = "app.bridge"
	// KindUserAction is an action dispatched from an App UI.
	KindUserAction EventKind = "app.user_action"
	// KindHostCompat is a detected host capability or incompatibility.
	KindHostCompat EventKind = "host.compat"
	// KindLog bridges an MCP notifications/message log record (RFC §11.3 — the
	// bridge itself is Phase 16; the kind is part of the obs/v1 contract now).
	KindLog EventKind = "log"
	// KindServerLifecycle is a server start/stop or capability negotiation.
	KindServerLifecycle EventKind = "server.lifecycle"
	// KindTaskProgress is a long-running task lifecycle/progress event (RFC §8;
	// Tasks is V1 scope so task events are part of obs/v1 — brief 05 Q-8).
	KindTaskProgress EventKind = "task.progress"
)

type Factory

type Factory func(cfg string) (Emitter, error)

Factory constructs an Emitter for a driver-specific configuration string. It is the obs analogue of store.Factory (RFC §13): a driver registers one via RegisterDriver in its init block, and Open constructs an emitter by name.

type FanOut

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

FanOut is an Emitter that fans an event out to several drivers — the bounded fan-out CLAUDE.md §8 mandates. Each driver receives every event; a slow driver cannot stall a fast one because every driver's Emit is itself non-blocking (the ring buffer drops; the SSE sink, Phase 16, drops). FanOut is safe for concurrent use and for concurrent Close.

func NewFanOut

func NewFanOut(drivers ...Emitter) *FanOut

NewFanOut returns a FanOut over the given drivers. A nil driver is dropped. With no drivers it behaves as a NopEmitter.

func (*FanOut) Close

func (f *FanOut) Close() error

Close closes every driver that implements Closer. It joins errors so one failing driver does not mask another.

func (*FanOut) Emit

func (f *FanOut) Emit(ctx context.Context, e Event)

Emit forwards e to every driver. It is non-blocking provided every driver's Emit is non-blocking, which the Emitter contract requires.

type LogPayload

type LogPayload struct {
	// Level is the RFC 5424 severity (debug, info, warning, error, …).
	Level string `json:"level"`
	// Logger is the optional logger name from the MCP log record.
	Logger string `json:"logger,omitempty"`
	// Message is the log message.
	Message string `json:"message"`
}

LogPayload is the payload of a KindLog event — the obs/v1 carrier for an MCP notifications/message log record. The actual MCP logging→obs/v1 bridge is Phase 16 (RFC §11.3); the payload shape is part of the obs/v1 contract now so Phase 16 is purely a new event source.

type NopEmitter

type NopEmitter struct{}

NopEmitter is an Emitter that discards every event. It is the safe default when a runtime is constructed without observability configured: a subsystem can always call Emit without a nil check. The zero value is ready to use.

func (NopEmitter) Emit

Emit discards e.

type Phase

type Phase string

Phase is the lifecycle position of an Event. A start/end pair brackets a duration; progress marks an intermediate point of a long-running task; emit is a point-in-time event with no paired counterpart (brief 05 §3.1).

const (
	// PhaseStart opens a lifecycle — e.g. a tools/call has begun.
	PhaseStart Phase = "start"
	// PhaseEnd closes a lifecycle; the event carries DurationMS.
	PhaseEnd Phase = "end"
	// PhaseProgress is an intermediate point of a long-running task.
	PhaseProgress Phase = "progress"
	// PhaseEmit is a point-in-time event with no paired counterpart.
	PhaseEmit Phase = "emit"
)

type PromptGetPayload

type PromptGetPayload struct {
	// Prompt is the registered prompt name.
	Prompt string `json:"prompt"`
	// Messages is the count of messages in the rendered GetPromptResult.
	Messages int `json:"messages,omitempty"`
	// Bytes is the JSON-serialised size of the rendered messages — a size
	// guardrail signal mirroring [ResourceReadPayload.Bytes].
	Bytes int `json:"bytes,omitempty"`
}

PromptGetPayload is the payload of a KindPromptGet event — a prompts/get invocation of a registered MCP Prompt (Phase 28; runtime/server.AddPrompt).

Prompts in MCP are templates the host pulls (rather than tools the model pushes); the obs/v1 carrier mirrors the resource.read shape — name + size guardrail — rather than the tool.call full input/output capture, because a prompt's "input" is a small string-argument map and its "output" is a rendered message list rather than a typed contract.

type Recorder

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

Recorder is the headless emit helper a subsystem uses to record obs/v1 events without hand-assembling an Event. It binds a server identity and an Emitter once; every event it builds carries SchemaVersion, a fresh event ID, a UTC timestamp, and the server identity automatically — so a call site supplies only what is genuinely call-specific.

A Recorder is the ONLY thing runtime/server, runtime/apps, and runtime/tasks touch to observe: they EMIT through it; nothing reads another subsystem's internals (P2, CLAUDE.md §6). A Recorder is a reusable concurrent artifact and safe for use from many goroutines (CLAUDE.md §5).

A nil *Recorder is valid and discards every event — a subsystem constructed without observability calls the same methods unconditionally.

func NewRecorder

func NewRecorder(emitter Emitter, serverID string, opts ...RecorderOption) *Recorder

NewRecorder binds emitter and serverID into a Recorder. A nil emitter is promoted to NopEmitter so the returned Recorder is always safe to call.

func (*Recorder) AppBridge

func (r *Recorder) AppBridge(ctx context.Context, sc SpanContext, p AppBridgePayload)

AppBridge records a point-in-time app.bridge event — the ui/initialize bridge handshake state.

func (*Recorder) AppLoad

func (r *Recorder) AppLoad(ctx context.Context, sc SpanContext, p AppLoadPayload)

AppLoad records a point-in-time app.load event: a ui:// App resource served to a host (RFC §7).

func (*Recorder) Log

func (r *Recorder) Log(ctx context.Context, sc SpanContext, p LogPayload)

Log records a point-in-time obs/v1 log event — the obs/v1 carrier for an MCP notifications/message log record. It is the emit side of the Phase 16 MCP logging → obs/v1 bridge (RFC §11.3): server.LogBridge calls Log so a server log record surfaces as an obs/v1 log event, in addition to the standard MCP notifications/message a Dockyard server still emits. obs/v1 is a one-way event stream, never a back channel (P2, CLAUDE.md §6).

func (*Recorder) PromptGet

func (r *Recorder) PromptGet(ctx context.Context, sc SpanContext, prompt string) func(messages, bytes int, err error)

PromptGet records the start of a prompts/get and returns a function that records its end with the rendered message count and serialized byte size. It mirrors [ResourceRead]'s shape: a prompts/get is a host-pulled template render whose interesting observability signals are name + size, not a typed input/output capture (Phase 28; runtime/server.AddPrompt).

The returned closure is safe to call exactly once.

func (*Recorder) ResourceRead

func (r *Recorder) ResourceRead(ctx context.Context, sc SpanContext, uri string) func(mime string, bytes int, err error)

ResourceRead records the start of a resources/read and returns a function that records its end with the served MIME type and byte size.

func (*Recorder) ServerLifecycle

func (r *Recorder) ServerLifecycle(ctx context.Context, sc SpanContext, p ServerLifecyclePayload)

ServerLifecycle records a point-in-time server.lifecycle event.

func (*Recorder) TaskEvent

func (r *Recorder) TaskEvent(ctx context.Context, sc SpanContext, phase Phase, p TaskProgressPayload, err error)

TaskEvent records a task lifecycle/progress event. phase is one of PhaseStart (task created), PhaseProgress (an intermediate point), or PhaseEnd (terminal). On a terminal failure pass err.

func (*Recorder) ToolCall

func (r *Recorder) ToolCall(ctx context.Context, sc SpanContext, tool, transport string) func(input, output json.RawMessage, err error)

ToolCall records the start of a tools/call and returns a function that records its end. The two events share a span; the caller invokes the returned function once the tool returns, passing the typed input/output (for shape capture) and any error. Usage:

end := rec.ToolCall(ctx, sc, "search", "stdio")
out, err := handler(ctx, in)
end(inputJSON, outputJSON, err)

The returned closure is safe to call exactly once.

type RecorderOption

type RecorderOption func(*Recorder)

RecorderOption tunes a Recorder at construction.

func WithCapturePolicy

func WithCapturePolicy(p CapturePolicy) RecorderOption

WithCapturePolicy sets the tool input/output capture policy. The default is CapturePolicyShape — shape + size only (CLAUDE.md §7). CapturePolicyFull is honoured only when WithRedactor also supplies a redactor.

func WithRedactor

func WithRedactor(rd Redactor) RecorderOption

WithRedactor supplies the redaction-aware Redactor that CapturePolicyFull requires. Without it, full-content capture degrades to shape+size.

type Redactor

type Redactor interface {
	// Redact returns a redacted copy of raw safe for capture in an obs/v1
	// event. It must never panic and must never return more data than raw.
	Redact(raw json.RawMessage) json.RawMessage
}

Redactor is the redaction-aware hook full-content capture requires. A Redactor receives a raw JSON value and returns a redaction-applied copy safe to embed in an event. Phase 15 defines the interface so CapturePolicyFull has a contract to bind to; the concrete redactor implementation is later scope. Until one is supplied, full-content capture degrades to shape+size.

type ResourceReadPayload

type ResourceReadPayload struct {
	// URI is the resource URI that was read.
	URI string `json:"uri"`
	// MIME is the served content's MIME type, when known.
	MIME string `json:"mime,omitempty"`
	// Bytes is the size of the served content — a size guardrail signal.
	Bytes int `json:"bytes"`
}

ResourceReadPayload is the payload of a KindResourceRead event.

type RingBuffer

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

RingBuffer is the in-memory, bounded ring-buffer Emitter — the obs/v1 driver Phase 15 ships and the source the inspector pulls recent history from (RFC §11.3, brief 05 §3.3).

It is non-blocking by construction: Emit never blocks a caller. When the buffer is full the OLDEST event is overwritten — a slow or absent consumer can never stall the runtime (CLAUDE.md §8). The number of events dropped this way is counted and exposed via RingBuffer.Dropped so an inspector can show "N events lost".

A RingBuffer is a reusable concurrent artifact: Emit, Recent, Len, and Dropped are all safe under concurrent use (CLAUDE.md §5).

func NewRingBuffer

func NewRingBuffer(capacity int) *RingBuffer

NewRingBuffer returns a RingBuffer holding at most capacity recent events. A capacity <= 0 is promoted to DefaultRingCapacity.

func (*RingBuffer) Cap

func (r *RingBuffer) Cap() int

Cap returns the ring's fixed capacity.

func (*RingBuffer) Dropped

func (r *RingBuffer) Dropped() int64

Dropped returns the number of events overwritten because the buffer was full — the "events lost" signal. It is monotonically non-decreasing.

func (*RingBuffer) Emit

func (r *RingBuffer) Emit(_ context.Context, e Event)

Emit records e. It is non-blocking: it takes the buffer lock only for the duration of a slice write, never for I/O, and never waits on a consumer. A malformed event is dropped silently — a buggy emit site never corrupts the history or crashes a request (P2). When the buffer is full the oldest event is overwritten.

func (*RingBuffer) Len

func (r *RingBuffer) Len() int

Len returns the number of events currently retained.

func (*RingBuffer) Recent

func (r *RingBuffer) Recent(n int) []Event

Recent returns the up-to-n most recent events, oldest first. n <= 0 returns every retained event. The returned slice is a fresh copy the caller owns — it is never aliased to the ring's storage, so a concurrent Emit cannot mutate it (the reusable-artifact rule, CLAUDE.md §5).

type SSESink

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

SSESink is the out-of-band, localhost-bound Server-Sent-Events obs/v1 emitter driver (RFC §11.3). It streams the live obs/v1 event stream to dev tooling — the inspector consumes it in Wave 8 — over its OWN loopback HTTP listener.

It is out-of-band by design: when the MCP transport is stdio, obs events go out THIS separate SSE channel and never touch os.Stdout/os.Stdin, so a stdio server's JSON-RPC pipe is never corrupted (brief 05 §2.2, §3.3). The sink holds no reference to the MCP transport; it cannot write to the protocol pipe.

SSESink is a reusable concurrent artifact: Emit is safe from many goroutines, and many HTTP subscribers can connect and disconnect concurrently. Emit is NON-BLOCKING: each subscriber has a bounded send queue; a slow or stalled subscriber has events dropped rather than stalling the emit path (CLAUDE.md §8). The number of dropped events is exposed via SSESink.Dropped.

func NewSSESink

func NewSSESink(addr string) (*SSESink, error)

NewSSESink constructs a localhost SSE sink listening on addr and starts its HTTP listener. An empty addr uses [defaultSSEAddr] (an OS-assigned loopback port). A non-loopback addr is rejected with [errSSENonLoopback] — the sink is dev-mode-only and localhost-bound (CLAUDE.md §7).

The returned sink is ready for Emit immediately; the live listen address (with the resolved port) is available via SSESink.Addr.

func (*SSESink) Addr

func (s *SSESink) Addr() string

Addr returns the sink's resolved listen address, including the OS-assigned port when the construction address used port 0.

func (*SSESink) Close

func (s *SSESink) Close() error

Close shuts the SSE sink down: it stops the HTTP listener and signals every connected subscriber's handler to return. It is idempotent (CLAUDE.md §5 — the Closer contract) and safe to call concurrently with Emit. After Close, Emit is a no-op.

func (*SSESink) Dropped

func (s *SSESink) Dropped() int64

Dropped returns the total number of per-subscriber event drops caused by a full subscriber queue — the "events lost to a slow SSE consumer" signal. It is monotonically non-decreasing.

func (*SSESink) Emit

func (s *SSESink) Emit(_ context.Context, e Event)

Emit fans e out to every connected subscriber. It is NON-BLOCKING: each subscriber has a bounded queue and a full queue means the event is DROPPED for that subscriber — a slow or stalled consumer never stalls the runtime (CLAUDE.md §8). A malformed event is dropped silently (P2). Emit takes the sink lock only for the duration of a non-blocking channel send per subscriber, never for I/O.

func (*SSESink) Handler

func (s *SSESink) Handler() http.Handler

Handler returns the SSE HTTP handler. The sink already serves it on its own listener at /obs/v1/stream; Handler is exported so the inspector (Wave 8) can additionally mount the live stream on its own localhost mux.

func (*SSESink) Subscribers

func (s *SSESink) Subscribers() int

Subscribers returns the current count of connected SSE subscribers.

type ServerLifecyclePayload

type ServerLifecyclePayload struct {
	// State is the lifecycle transition: "starting" | "stopped".
	State string `json:"state"`
	// ServerName is the human-facing server name.
	ServerName string `json:"server_name,omitempty"`
	// Version is the server's semantic version.
	Version string `json:"version,omitempty"`
	// Transport is the transport the server is serving over, when known.
	Transport string `json:"transport,omitempty"`
	// Tools is the count of registered tools.
	Tools int `json:"tools,omitempty"`
}

ServerLifecyclePayload is the payload of a KindServerLifecycle event.

type SpanContext

type SpanContext struct {
	// TraceID is the 16-byte W3C trace-id as 32 lowercase hex characters. It is
	// constant for a whole call chain.
	TraceID string
	// SpanID is the 8-byte W3C span-id as 16 lowercase hex characters,
	// identifying this unit of work.
	SpanID string
	// ParentID is the SpanID of the enclosing span, or "" at the root.
	ParentID string
}

SpanContext is a W3C Trace Context span identity. It is the correlation handle a subsystem threads through a unit of work: a start event and its paired end event share the same SpanContext, and a child unit of work derives a SpanContext whose ParentID is the enclosing span.

func ChildOrNewTrace

func ChildOrNewTrace(ctx context.Context) SpanContext

ChildOrNewTrace returns a child span of the in-flight span carried by ctx, or a fresh root trace when ctx carries none. It is the one-call form of the "correlate if you can, else start a trace" pattern an emit site nested inside a possibly-instrumented unit of work uses.

func InboundTraceFromContext

func InboundTraceFromContext(ctx context.Context) (sc SpanContext, ok bool)

InboundTraceFromContext returns the inbound parent trace stamped by WithInboundTrace, and ok=false when ctx carries none.

func NewTrace

func NewTrace() SpanContext

NewTrace begins a new W3C trace: a fresh trace-id and a fresh root span-id, with no parent. Use it at the entry edge of a call chain that did not arrive with an inbound W3C traceparent.

func NewTraceFromContext

func NewTraceFromContext(ctx context.Context) SpanContext

NewTraceFromContext is the handler-edge counterpart of NewTrace: when ctx carries an inbound trace stamped by WithInboundTrace, the returned span inherits the parent's TraceID (preserving the call-chain identity) and is a child of the parent's span; otherwise it falls back to NewTrace — a fresh root trace. Use it at any handler-edge call site that opens a new obs/v1 unit of work, so a cross-process call from a Harbor agent (or any other W3C-compliant propagator) naturally nests Dockyard's spans under the caller's (R5; D-122).

func SpanFromContext

func SpanFromContext(ctx context.Context) (sc SpanContext, ok bool)

SpanFromContext returns the in-flight SpanContext stamped by WithSpan, and ok=false when ctx carries none. An emit site that wants to nest under an enclosing span calls SpanFromContext and, when ok, uses sc.Child(); when not ok it begins a fresh trace with NewTrace.

func (SpanContext) Child

func (sc SpanContext) Child() SpanContext

Child derives a child span within the same trace: the trace-id is preserved, a fresh span-id is generated, and the parent's span-id becomes the child's ParentID. A zero-value receiver (no trace yet) is promoted to a fresh root trace so a caller need not special-case the entry edge.

func (SpanContext) IsZero

func (sc SpanContext) IsZero() bool

IsZero reports whether sc carries no trace identity.

type TaskProgressPayload

type TaskProgressPayload struct {
	// TaskID is the task identifier.
	TaskID string `json:"task_id"`
	// Status is the task's lifecycle status at this point.
	Status string `json:"status,omitempty"`
	// Message is an optional human-readable progress note.
	Message string `json:"message,omitempty"`
	// Tool is the task-augmented tool name, when the task wraps a tools/call.
	Tool string `json:"tool,omitempty"`
}

TaskProgressPayload is the payload of a KindTaskProgress event — a long-running task lifecycle/progress point (RFC §8).

type ToolCallPayload

type ToolCallPayload struct {
	// Tool is the registered tool name.
	Tool string `json:"tool"`
	// Transport is the MCP transport the call arrived on: stdio | http | inmem.
	Transport string `json:"transport,omitempty"`
	// Client is the client name from the initialize handshake, when known.
	Client string `json:"client,omitempty"`

	// InputShape is the structural fingerprint of the tool input (see [Shape]).
	// It is the default, content-free capture.
	InputShape *ValueShape `json:"input_shape,omitempty"`
	// OutputShape is the structural fingerprint of the tool output.
	OutputShape *ValueShape `json:"output_shape,omitempty"`

	// Input is the full tool input. It is nil under the default shape+size
	// policy; it is populated only when full-content capture is opted in and
	// redaction has been applied (CapturePolicyFull).
	Input json.RawMessage `json:"input,omitempty"`
	// Output is the full tool output, under the same opt-in policy as Input.
	Output json.RawMessage `json:"output,omitempty"`

	// ContractOK reports whether the input/output validated against the
	// generated contract schema (P1). A nil value means "not checked".
	ContractOK *bool `json:"contract_ok,omitempty"`
}

ToolCallPayload is the payload of a KindToolCall event. Tool input/output capture defaults to shape + size only — Input/Output are nil unless full-content capture is explicitly opted in and redaction-aware (CLAUDE.md §7, RFC §11.2). InputShape/OutputShape and the byte counts are the always-present default capture.

type ValueKind

type ValueKind string

ValueKind is the JSON structural category of a captured value (see Shape).

const (
	// KindNull is a JSON null or an absent value.
	KindNull ValueKind = "null"
	// KindBool is a JSON boolean.
	KindBool ValueKind = "bool"
	// KindNumber is a JSON number.
	KindNumber ValueKind = "number"
	// KindString is a JSON string.
	KindString ValueKind = "string"
	// KindArray is a JSON array.
	KindArray ValueKind = "array"
	// KindObject is a JSON object.
	KindObject ValueKind = "object"
)

type ValueShape

type ValueShape struct {
	// Kind is the JSON structural category.
	Kind ValueKind `json:"kind"`
	// Bytes is the size of the value's JSON encoding — the size guardrail
	// signal the inspector surfaces.
	Bytes int `json:"bytes"`
	// Fields is the sorted set of top-level keys, for an object value. It is
	// the field NAMES only — never the values. Nil for non-objects.
	Fields []string `json:"fields,omitempty"`
	// Len is the element count, for an array value. Nil for non-arrays.
	Len *int `json:"len,omitempty"`
}

ValueShape is the content-free structural fingerprint of a JSON value: its kind, byte size, and — for objects and arrays — its top-level field names or element count. It deliberately carries NO values, only structure, so it is always safe to emit even under the default capture policy (CLAUDE.md §7).

func Shape

func Shape(raw json.RawMessage) ValueShape

Shape computes the content-free ValueShape of a raw JSON value. A nil or empty input yields a null shape. Malformed JSON yields a shape whose Kind is the best-effort guess from the leading byte and whose Bytes is the raw length — Shape never fails: observability must not fail a request (P2).

Directories

Path Synopsis
Package otel is the optional OpenTelemetry export adapter for obs/v1 — the OTelEmitter (RFC §11.3, brief 05 §3.4).
Package otel is the optional OpenTelemetry export adapter for obs/v1 — the OTelEmitter (RFC §11.3, brief 05 §3.4).

Jump to

Keyboard shortcuts

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