Skip to content

Report what each service spent and consumed as records - #5607

Merged
markbackman merged 2 commits into
mainfrom
metrics-observer
Sep 4, 2026
Merged

Report what each service spent and consumed as records#5607
markbackman merged 2 commits into
mainfrom
metrics-observer

Conversation

@markbackman

@markbackman markbackman commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds ServiceMetricsObserver, which turns each metric a service publishes into a record. Today MetricsLogObserver writes them out for a person reading a terminal; nothing turned them into data.
  • on_service_latencyServiceLatencyRecord: kind (ttfb / ttfa / ttfat), processor, model, timestamp, seconds. A measurement that decomposes keeps its parts — leading_silence_secs behind a time to first audio, thinking_time_secs behind a time to first answer token, and the ttfb_secs both build on.
  • on_service_usageServiceUsageRecord: kind (stt / llm / tts), processor, model, timestamp, plus audio_seconds, characters, and every token count LLMTokenUsage reports including cache reads, reasoning and audio tokens.
  • Nothing is summed. A turn that runs two inferences reports twice, so a consumer groups the records by turn, session or model as it needs — and a session that ends abruptly leaves behind everything up to that point, where a total held in memory goes with the process holding it.
  • What a service made someone wait for is here; what it did with its own time is not. Processing time, text aggregation and smart-turn predictions are all deliberately absent, with a test covering it: aggregation already appears as a span in LatencyBreakdown, and the other two describe how work was done rather than what it cost the person waiting.
observer = ServiceMetricsObserver()

@observer.event_handler("on_service_usage")
async def on_service_usage(observer, record):
    logger.info(record.model_dump_json())

The observer reads a time source a test can supply, and remembers a bounded number of frame IDs so a metric relayed along the pipeline is reported once rather than once per hop.

Testing

  • uv run pytest tests/test_service_metrics_observer.py — 12 tests, 100% line coverage of the new file: every metric type mapped field by field, the optional LLM token counts, several metrics in one frame, a relayed frame reported once, the two omitted metric types passed over, and the frame memory staying bounded.

@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/pipecat/observers/service_metrics_observer.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

leading_silence_secs=metrics.leading_silence,
**common,
)
if isinstance(metrics, TTFATMetricsData):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elif

thinking_time_secs=metrics.thinking_time,
**common,
)
if isinstance(metrics, TTFBMetricsData):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elif

return ServiceLatencyRecord(
kind=ServiceLatencyKind.TTFB, seconds=metrics.value, **common
)
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, keep return None

)
if isinstance(metrics, TTSUsageMetricsData):
return ServiceUsageRecord(kind=ServiceUsageKind.TTS, characters=metrics.value, **common)
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same in this function

@aconchillo

Copy link
Copy Markdown
Contributor

LGTM. Approving, just minor comments.

Services publish metrics as they finish work, and MetricsLogObserver writes
them out for a person reading a terminal. Nothing turned them into data.

ServiceMetricsObserver emits one record per metric: what was measured, which
processor and model reported it, and when. A measurement that decomposes keeps
its parts, so time to first audio carries the leading silence and time to
first answer token carries the thinking that preceded it, and an LLM record
carries every token count a model reports.

Nothing is summed. A turn that runs two inferences reports twice, so a
consumer groups the records by turn, session or model as it needs, and a
session that ends abruptly still leaves behind everything up to that point —
where a total held in memory would go with it.
@markbackman
markbackman merged commit a95aefd into main Sep 4, 2026
6 checks passed
@markbackman
markbackman deleted the metrics-observer branch September 4, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants