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
- func Drivers() []string
- func RegisterDriver(name string, f Factory)
- func WithInboundTrace(ctx context.Context, parent SpanContext) context.Context
- func WithSession(ctx context.Context, sessionID string) context.Context
- func WithSpan(ctx context.Context, sc SpanContext) context.Context
- type AppBridgePayload
- type AppLoadPayload
- type CapturePolicy
- type Closer
- type Emitter
- type ErrorInfo
- type Event
- type EventKind
- type Factory
- type FanOut
- type LogPayload
- type NopEmitter
- type Phase
- type PromptGetPayload
- type Recorder
- func (r *Recorder) AppBridge(ctx context.Context, sc SpanContext, p AppBridgePayload)
- func (r *Recorder) AppLoad(ctx context.Context, sc SpanContext, p AppLoadPayload)
- func (r *Recorder) Log(ctx context.Context, sc SpanContext, p LogPayload)
- func (r *Recorder) PromptGet(ctx context.Context, sc SpanContext, prompt string) func(messages, bytes int, err error)
- func (r *Recorder) ResourceRead(ctx context.Context, sc SpanContext, uri string) func(mime string, bytes int, err error)
- func (r *Recorder) ServerLifecycle(ctx context.Context, sc SpanContext, p ServerLifecyclePayload)
- func (r *Recorder) TaskEvent(ctx context.Context, sc SpanContext, phase Phase, p TaskProgressPayload, ...)
- func (r *Recorder) ToolCall(ctx context.Context, sc SpanContext, tool, transport string) func(input, output json.RawMessage, err error)
- type RecorderOption
- type Redactor
- type ResourceReadPayload
- type RingBuffer
- type SSESink
- type ServerLifecyclePayload
- type SpanContext
- type TaskProgressPayload
- type ToolCallPayload
- type ValueKind
- type ValueShape
Constants ¶
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.
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 ¶
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 ¶
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).
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 ¶
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 ¶
NewFanOut returns a FanOut over the given drivers. A nil driver is dropped. With no drivers it behaves as a NopEmitter.
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.
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) 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 ¶
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 ¶
Addr returns the sink's resolved listen address, including the OS-assigned port when the construction address used port 0.
func (*SSESink) Close ¶
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 ¶
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 ¶
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 ¶
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 ¶
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).