observers

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: BSD-2-Clause Imports: 13 Imported by: 0

Documentation

Overview

Package observers provides pipeline observers: components that watch the frames flowing through a pipeline to derive turn, latency and startup metrics, or to log the stream, without modifying it. Register them via pipeline.WorkerConfig.Observers.

Every handover between two processors is reported, not only what reaches the ends of the pipeline, so an observer sees where each frame came from. Each observer here is safe for concurrent use: a pipeline's processors each run on their own goroutine.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DebugFrameFilter added in v0.1.0

type DebugFrameFilter struct {
	// Frame is an instance of the frame type to log; its type is what is
	// matched, not its contents.
	Frame frames.Frame
	// Match, when set, logs the frame only where the processor at Endpoint
	// satisfies it. Leave it nil to log the frame wherever it travels.
	Match func(processor.Processor) bool
	// Endpoint is the end of the handover Match decides on.
	Endpoint FrameEndpoint
}

DebugFrameFilter narrows what a DebugLog observer reports to one kind of frame, optionally only where one end of the handover is a particular kind of processor.

type DebugLog added in v0.1.0

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

DebugLog logs the frames going by with their contents, for working out what a pipeline is actually doing. Every exported field of a frame is rendered, so it serves any frame type without knowing anything about it.

func NewDebugLog added in v0.1.0

func NewDebugLog(cfg DebugLogConfig) *DebugLog

NewDebugLog builds a DebugLog observer.

func (*DebugLog) OnPushFrame added in v0.1.0

func (o *DebugLog) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer.

type DebugLogConfig added in v0.1.0

type DebugLogConfig struct {
	// Logger is the destination; slog.Default() when nil.
	Logger *slog.Logger
	// Frames selects what to log. An empty list logs every frame, which on a
	// real pipeline is a great many.
	Frames []DebugFrameFilter
	// ExcludeFields names the frame fields left out of the log. A nil slice
	// leaves out the binary payloads (Audio, Image, Images); an empty non-nil
	// slice leaves out nothing.
	ExcludeFields []string
}

DebugLogConfig configures a DebugLog observer.

type FrameEndpoint added in v0.1.0

type FrameEndpoint int

FrameEndpoint selects which end of a handover a filter decides on.

const (
	// SourceEndpoint decides on the processor pushing the frame.
	SourceEndpoint FrameEndpoint = iota
	// DestinationEndpoint decides on the processor receiving it.
	DestinationEndpoint
)

type FunctionCallMetrics added in v0.1.0

type FunctionCallMetrics struct {
	// FunctionName is the name of the tool that was called.
	FunctionName string
	// StartTime is the wall-clock time the call started at.
	StartTime time.Time
	// Duration is the time from the call starting to its result arriving.
	Duration time.Duration
}

FunctionCallMetrics is how long one tool call took to run.

type LLMLog added in v0.1.0

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

LLMLog logs what a language-model service was given and what it produced: the context it was asked to answer, the tokens it generated, the bounds of each response, and the tool calls it made along with their results.

Only frames pushed to or from a model service are reported, so the same frame types traveling elsewhere in the pipeline are left alone.

func NewLLMLog added in v0.1.0

func NewLLMLog(cfg LLMLogConfig) *LLMLog

NewLLMLog builds an LLMLog observer.

func (*LLMLog) OnPushFrame added in v0.1.0

func (o *LLMLog) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer.

type LLMLogConfig added in v0.1.0

type LLMLogConfig struct {
	// Logger is the destination; slog.Default() when nil.
	Logger *slog.Logger
}

LLMLogConfig configures an LLMLog observer.

type LatencyBreakdown added in v0.1.0

type LatencyBreakdown struct {
	// TTFB is what each service took to produce anything at all, in the order
	// the measurements were reported.
	TTFB []TTFBBreakdown
	// TextAggregation is the first text-aggregation measurement of the cycle,
	// which is what grouping the model's tokens into sentences cost before
	// synthesis could start. It is nil when none was reported.
	TextAggregation *TextAggregationBreakdown
	// UserTurnStart is when the user's turn ended in the audio: the moment the
	// speech itself stopped, before the detector had confirmed it. The zero
	// value means no VAD stop was observed.
	UserTurnStart time.Time
	// UserTurn is how long releasing the turn took from that moment: the
	// detector's silence window, the transcriber finalizing, and any wait on an
	// end-of-turn analyzer. It is nil when the turn was never released, which is
	// what a pipeline with no turn analyzer looks like.
	UserTurn *time.Duration
	// FunctionCalls is how long each tool call of the cycle took. It is empty
	// when the reply made none.
	FunctionCalls []FunctionCallMetrics
}

LatencyBreakdown accounts for one user-to-bot cycle: what each service in the pipeline contributed to the delay the listener heard.

It is collected between the user falling silent and the bot starting to speak, and only when the pipeline collects metrics at all: the measurements come from the MetricsFrames the services emit, which they only emit when asked to.

func (LatencyBreakdown) ChronologicalEvents added in v0.1.0

func (b LatencyBreakdown) ChronologicalEvents() []string

ChronologicalEvents renders every measurement in the breakdown as a line of text, ordered by when it started. It is what turns the breakdown into a log of where a slow reply spent its time.

type LatencyConfig

type LatencyConfig struct {
	// MaxFrames is how many recent frame ids the observer remembers to
	// recognize one it has already counted; 0 uses 100.
	MaxFrames int
	// OnLatency is called with the time from the user stopping speaking to the
	// bot starting: the user-perceived response latency.
	OnLatency func(d time.Duration)
	// OnBreakdown is called with the per-service account of the same cycle,
	// alongside every latency the observer reports. It is empty of measurements
	// unless the pipeline collects metrics.
	OnBreakdown func(b LatencyBreakdown)
	// OnFirstBotSpeechLatency is called once, with the time from the client
	// connecting to the bot first speaking. It is not called at all when the
	// user speaks first: the figure means the greeting was slow, and there is no
	// greeting to measure once the conversation has started without one.
	OnFirstBotSpeechLatency func(d time.Duration)
}

LatencyConfig configures a UserBotLatency observer.

type MetricsLog added in v0.1.0

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

MetricsLog logs the measurements the pipeline reports: what each service took to answer, how much it was given, and what it billed.

func NewMetricsLog added in v0.1.0

func NewMetricsLog(cfg MetricsLogConfig) *MetricsLog

NewMetricsLog builds a MetricsLog observer.

func (*MetricsLog) OnPushFrame added in v0.1.0

func (o *MetricsLog) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer.

type MetricsLogConfig added in v0.1.0

type MetricsLogConfig struct {
	// Logger is the destination; slog.Default() when nil.
	Logger *slog.Logger
	// Include selects the kinds of measurement to log, given as instances of
	// the frames.MetricsData types wanted. An empty list logs every kind.
	Include []frames.MetricsData
}

MetricsLogConfig configures a MetricsLog observer.

type ProcessorStartupTiming added in v0.1.0

type ProcessorStartupTiming struct {
	// ProcessorName is the name of the processor.
	ProcessorName string
	// StartOffset is how long after the StartFrame entered the pipeline this
	// processor began starting.
	StartOffset time.Duration
	// Duration is what the processor cost to get ready: its setup and its start
	// together. It connects while it is set up, and starting is whatever is left
	// to do once it has.
	Duration time.Duration
	// SetupDuration is how long the processor's setup took, which is the part of
	// Duration spent connecting.
	SetupDuration time.Duration
}

ProcessorStartupTiming is what one processor cost to start.

type StartupTiming

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

StartupTiming measures what starting a pipeline costs, processor by processor.

A processor does its startup work while handling the StartFrame: connecting to a provider, authenticating, loading a model. The observer times the gap between that frame reaching the processor and the processor passing it on, which is exactly that work, and reports the lot once the pipeline is up.

It reports separately on the transport, which is the other half of how long a call takes to become answerable: how long after the pipeline started the bot joined the session, and how long until the first client did.

func NewStartupTiming

func NewStartupTiming(cfg StartupTimingConfig) *StartupTiming

NewStartupTiming builds a StartupTiming observer.

func (*StartupTiming) OnPipelineSetupStarted added in v0.1.0

func (o *StartupTiming) OnPipelineSetupStarted(at time.Time)

OnPipelineSetupStarted implements processor.SetupStartedObserver. Processors connect while they are being set up, so startup begins here rather than at the StartFrame.

func (*StartupTiming) OnPipelineStarted added in v0.1.0

func (o *StartupTiming) OnPipelineStarted()

OnPipelineStarted implements processor.PipelineStartedObserver. The pipeline being up is what says every processor has started, so it is where the report is made.

func (*StartupTiming) OnProcessFrame added in v0.1.0

func (o *StartupTiming) OnProcessFrame(data processor.FrameProcessed)

OnProcessFrame implements processor.ProcessObserver. It records the StartFrame reaching a processor, which is where that processor's startup begins.

func (*StartupTiming) OnProcessorSetup added in v0.1.0

func (o *StartupTiming) OnProcessorSetup(data processor.ProcessorSetUp)

OnProcessorSetup implements processor.SetupObserver. It records what a processor's setup cost, which is the part of getting ready it spends connecting.

func (*StartupTiming) OnPushFrame added in v0.1.0

func (o *StartupTiming) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer. It closes the measurement a processor opened, and watches for the transport's connection milestones.

type StartupTimingConfig added in v0.1.0

type StartupTimingConfig struct {
	// Track selects the processors to measure. When nil every processor is
	// measured except the pipeline plumbing: the pipelines themselves, and the
	// sources they wrap the head of their chains in.
	Track func(processor.Processor) bool
	// OnStartupTimingReport is called once, when the pipeline has started, with
	// what each processor cost. It is not called when nothing was measured.
	OnStartupTimingReport func(r StartupTimingReport)
	// OnTransportTimingReport is called once, when the first client connects,
	// with how long the transport took to get there.
	OnTransportTimingReport func(r TransportTimingReport)
}

StartupTimingConfig configures a StartupTiming observer.

type StartupTimingReport added in v0.1.0

type StartupTimingReport struct {
	// StartTime is the wall-clock time at which the pipeline began setting up.
	StartTime time.Time
	// TotalDuration is the wall-clock time from the pipeline starting to set up
	// until it had started. Processors are set up concurrently, so it is the
	// span rather than the sum of what each of them cost.
	TotalDuration time.Duration
	// ProcessorTimings is what each processor cost, in the order the StartFrame
	// left them.
	ProcessorTimings []ProcessorStartupTiming
}

StartupTimingReport is what every measured processor cost to start.

type TTFBBreakdown added in v0.1.0

type TTFBBreakdown struct {
	// Processor is the name of the processor that reported it.
	Processor string
	// Model is the model it is attributed to, "" when unknown.
	Model string
	// StartTime is the wall-clock time the measurement started at.
	StartTime time.Time
	// Duration is the measured time to first byte.
	Duration time.Duration
}

TTFBBreakdown is one time-to-first-byte measurement, placed on the timeline of the reply it belongs to.

type TextAggregationBreakdown added in v0.1.0

type TextAggregationBreakdown struct {
	// Processor is the name of the processor that reported it.
	Processor string
	// StartTime is the wall-clock time the measurement started at.
	StartTime time.Time
	// Duration is the measured aggregation time.
	Duration time.Duration
}

TextAggregationBreakdown is one text-aggregation measurement, placed on the timeline of the reply it belongs to.

type TranscriptionLog added in v0.1.0

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

TranscriptionLog logs what a speech-to-text service heard, final transcripts and interim ones alike. Only what a transcriber produced is reported, so a transcript arriving from somewhere else is left alone.

func NewTranscriptionLog added in v0.1.0

func NewTranscriptionLog(cfg TranscriptionLogConfig) *TranscriptionLog

NewTranscriptionLog builds a TranscriptionLog observer.

func (*TranscriptionLog) OnPushFrame added in v0.1.0

func (o *TranscriptionLog) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer.

type TranscriptionLogConfig added in v0.1.0

type TranscriptionLogConfig struct {
	// Logger is the destination; slog.Default() when nil.
	Logger *slog.Logger
}

TranscriptionLogConfig configures a TranscriptionLog observer.

type TransportTimingReport added in v0.1.0

type TransportTimingReport struct {
	// StartTime is the wall-clock time at which the pipeline began setting up.
	StartTime time.Time
	// BotConnected is how long after the pipeline began setting up the bot
	// itself joined
	// the session. It is nil on a transport that never reports the bot joining,
	// which is every transport that is not an SFU.
	BotConnected *time.Duration
	// ClientConnected is how long after the pipeline began setting up the first
	// remote participant connected.
	ClientConnected time.Duration
}

TransportTimingReport is how long the transport took to reach the points at which a conversation can actually happen.

type TurnTrace added in v0.1.0

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

TurnTrace traces a conversation and its turns. The conversation span opens with the pipeline and lasts the whole session; each turn opens a span beneath it, and the service spans of that turn nest under it in turn, so one session is one trace shaped like the conversation it recorded.

It observes only the pipeline start; the turns themselves are reported to it by a TurnTracking observer, and the response latency by a UserBotLatency observer, through TurnStarted, TurnEnded and LatencyMeasured.

func NewTurnTrace added in v0.1.0

func NewTurnTrace(cfg TurnTraceConfig) *TurnTrace

NewTurnTrace builds a TurnTrace observer.

func (*TurnTrace) EndConversation added in v0.1.0

func (o *TurnTrace) EndConversation()

EndConversation closes the conversation span, and any turn still open with it. The task calls it when the pipeline has stopped. A nil observer — a task that is not tracing — has nothing to close.

func (*TurnTrace) LatencyMeasured added in v0.1.0

func (o *TurnTrace) LatencyMeasured(d time.Duration)

LatencyMeasured records the user-perceived response latency on the turn it was measured in. Wire it to a UserBotLatency observer's OnLatency.

func (*TurnTrace) OnPushFrame added in v0.1.0

func (o *TurnTrace) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer. The conversation span opens on the StartFrame rather than with the first turn, so whatever a bot does before the user speaks — a greeting, a flow initializing — is part of the conversation.

func (*TurnTrace) StartConversation added in v0.1.0

func (o *TurnTrace) StartConversation(id string)

StartConversation opens the conversation span under id, generating one when id is empty. The task calls it before the pipeline runs, and the StartFrame handler above calls it for an observer wired up by hand. Whichever comes first opens the span; the other finds it open and returns. A nil observer (a task that is not tracing) has nothing to open.

func (*TurnTrace) TurnEnded added in v0.1.0

func (o *TurnTrace) TurnEnded(turn int, d time.Duration, interrupted bool)

TurnEnded closes the span for a turn, recording how long it ran and whether it was cut short. Wire it to a TurnTracking observer's OnTurnEnded.

func (*TurnTrace) TurnStarted added in v0.1.0

func (o *TurnTrace) TurnStarted(turn int)

TurnStarted opens the span for a turn. Wire it to a TurnTracking observer's OnTurnStarted.

type TurnTraceConfig added in v0.1.0

type TurnTraceConfig struct {
	// Tracing is the pipeline's tracing context, which the observer writes as
	// the conversation and its turns begin and end. Required: it is what the
	// services read to parent their spans to the turn being spoken.
	Tracing *tracing.TracingContext
	// ConversationID names the conversation; empty generates one.
	ConversationID string
	// Attributes are set on the conversation span on top of the ones the
	// observer sets itself, and are where the keys a trace backend reads from
	// the root span belong (a session id, a user id, tags).
	Attributes []attribute.KeyValue
}

TurnTraceConfig configures a TurnTrace observer.

type TurnTracking

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

TurnTracking tracks conversational turns. The first turn starts with the pipeline; a turn ends when the bot finishes speaking (after TurnEndTimeout) or is interrupted by the user, at which point the next turn starts.

A turn is timed on the pipeline clock, from the frame that opened it to the frame that closed it. A turn ended by the timeout is therefore measured to the moment the bot fell silent, not to the moment the timer fired: the wait exists to tell a pause apart from an ending, and it is not part of the turn.

func NewTurnTracking

func NewTurnTracking(cfg TurnTrackingConfig) *TurnTracking

NewTurnTracking builds a TurnTracking observer.

func (*TurnTracking) OnPushFrame added in v0.1.0

func (o *TurnTracking) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer.

func (*TurnTracking) OnTurnEnded added in v0.1.0

func (o *TurnTracking) OnTurnEnded(fn func(turn int, duration time.Duration, interrupted bool))

OnTurnEnded adds a listener called when a turn ends, alongside the one the config carries and any added before it.

type TurnTrackingConfig

type TurnTrackingConfig struct {
	// MaxFrames is how many recent frame ids the observer remembers to
	// recognize one it has already counted; 0 uses 100.
	MaxFrames int
	// TurnEndTimeout is how long after the bot stops speaking a turn ends; 0 uses
	// 2.5s. The delay lets a turn survive a brief gap between bot utterances (an
	// HTTP TTS boundary, a function call) without splitting into two turns.
	TurnEndTimeout time.Duration
	// OnTurnStarted is called when a turn begins, with the 1-based turn number.
	OnTurnStarted func(turn int)
	// OnTurnEnded is called when a turn ends, with the turn number, its duration,
	// and whether it was cut short by an interruption.
	OnTurnEnded func(turn int, duration time.Duration, interrupted bool)
}

TurnTrackingConfig configures a TurnTracking observer.

type UserBotLatency

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

UserBotLatency measures the response latency of each turn: the gap between the user stopping speaking and the bot starting. Alongside each measurement it reports a LatencyBreakdown accounting for where that time went, and it reports separately on the first thing the bot says after a client connects.

It watches downstream frames only. A frame broadcast in both directions is therefore counted once, on the way down.

func NewUserBotLatency

func NewUserBotLatency(cfg LatencyConfig) *UserBotLatency

NewUserBotLatency builds a UserBotLatency observer.

func (*UserBotLatency) OnPushFrame added in v0.1.0

func (o *UserBotLatency) OnPushFrame(data processor.FramePushed)

OnPushFrame implements processor.Observer.

Jump to

Keyboard shortcuts

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