Processing pipeline

This page explains how a metric event moves through MVX Metrics at runtime.

It focuses on the pipeline itself:

  • where the event enters the metrics system;

  • where processing is isolated from production code;

  • how the recorder dispatches the event;

  • how metrics accept or ignore the event;

  • where metric state becomes visible.

Overview

At runtime, the normal path is:

production code
   |
   v
create MetricEvent
   |
   v
recorder.register_event(event)
   |
   v
recorder internal buffer
   |
   v
recorder processing side
   |
   v
metric.handle_event(event)
   |
   v
metric state
   |
   v
metric snapshot

The recorder coordinates this pipeline.

The production component creates the event and hands it to the recorder.

The metric decides whether the event changes its state.

The snapshot exposes the current state later.

Step 1. Production code creates an event

The pipeline starts inside production code.

For example, a business method may create an event after a successful operation:

DocumentSaveAttemptMetricEvent(
    outcome=DocumentSaveAttemptOutcome.SUCCESS,
)

or after a failed operation:

DocumentSaveAttemptMetricEvent(
    outcome=DocumentSaveAttemptOutcome.FAILURE,
)

At this point, nothing has been aggregated yet.

The event is only an input object created by production code.

Step 2. Production code hands the event to the recorder

The event enters MVX Metrics through the recorder:

self._metrics_recorder.register_event(event=event)

This call is the production boundary.

From the production method’s point of view, this is a short synchronous handoff:

production method
   |
   v
recorder.register_event(event)

After the handoff, the production method can continue its own work.

The recorder takes responsibility for moving the event into metrics-side processing.

Step 3. The recorder accepts the event

The recorder receives the event and places it into its internal processing buffer.

recorder.register_event(event)
   |
   v
recorder internal buffer

This separates event emission from event processing.

The production method does not call metrics directly.

It does not run the dispatch loop.

It only hands the event to the recorder.

Step 4. Processing moves to the recorder side

After the event is accepted, processing continues on the recorder side.

When the recorder is managed by MetricsRuntime, this side runs in the runtime-owned thread and its asyncio event loop.

recorder internal buffer
   |
   v
runtime-owned thread / asyncio event loop
   |
   v
recorder processing side

This is the main execution separation in the pipeline.

Production code may be synchronous or asynchronous.

Recorder processing still happens in the metrics-processing environment managed by the runtime.

Step 5. The recorder dispatches the event to metrics

The recorder owns registered metric instances.

When it processes an event, it gives that event to the metrics it knows about.

recorder processing side
   |
   v
metric.handle_event(event)

If several metrics are registered, each metric gets a chance to inspect the event.

recorder
   |
   v
metric A.handle_event(event)
   |
   v
metric B.handle_event(event)
   |
   v
metric C.handle_event(event)

The recorder does not decide which dimensions should change.

It only dispatches the event.

Step 6. A metric accepts or ignores the event

A metric decides whether the event is relevant.

The usual pattern is:

def handle_event(self, event: MetricEvent) -> bool:
    if not isinstance(event, DocumentSaveAttemptMetricEvent):
        return False

    ...
    return True

If the metric returns False, the event is ignored by that metric.

metric.handle_event(event)
   |
   v
False
   |
   v
metric state unchanged

If the metric returns True, the metric has accepted the event.

metric.handle_event(event)
   |
   v
True
   |
   v
metric state updated

This return value is important for the recorder.

It tells the recorder whether the metric actually handled the event.

Step 7. Accepted events update metric state

When a metric accepts an event, it updates its own internal state.

For example:

self._total += 1

if event.outcome is DocumentSaveAttemptOutcome.SUCCESS:
    self._success_total += 1

elif event.outcome is DocumentSaveAttemptOutcome.FAILURE:
    self._failure_total += 1

Only the metric mutates its state.

The production component does not mutate metric state.

The recorder does not decide how metric state changes.

Step 8. The recorder may run post-change logic

After a metric accepts an event, the recorder has a post-change point.

At that point, the metric has already updated its state.

Architecturally, this is where recorder-level extensions can react to accepted events.

metric.handle_event(event)
   |
   v
metric state updated
   |
   v
recorder post-change hook

The base in-memory path can keep this simple.

A custom recorder or subclass can use this point for integration work, such as notifying observers or forwarding updated state.

Step 9. Snapshots expose current state

Snapshots are not produced during every production call unless application code asks for them.

They are read later:

snapshots = recorder.get_metric_snapshots()

The recorder asks registered metrics for their current snapshots.

metric state
   |
   v
metric.snapshot()
   |
   v
recorder.get_metric_snapshots()

The snapshot exposes current aggregated state.

It does not replay the pipeline.

It does not contain the event history.

Full pipeline

The complete runtime path is:

production code
   |
   v
create MetricEvent
   |
   v
recorder.register_event(event)
   |
   v
recorder internal buffer
   |
   v
recorder processing side
   |
   v
metric.handle_event(event)
   |
   +--> False
   |       |
   |       v
   |    metric ignored event
   |
   +--> True
           |
           v
        metric state updated
           |
           v
        recorder post-change hook
           |
           v
        later: metric snapshot

The important point is that event creation, event handoff, event processing, state mutation, and state inspection are separate stages.

Pipeline ownership

Each stage has one owner.

production component
   |
   v
creates MetricEvent

recorder
   |
   v
accepts and processes event

metric
   |
   v
accepts or ignores event

metric
   |
   v
updates internal state

recorder
   |
   v
collects snapshots

This ownership is what keeps the system replaceable.

A production component can keep emitting the same events while metrics, recorders, hooks, or snapshot consumers evolve around it.

Pipeline with MetricsRuntime

When recorders are created by MetricsRuntime, the pipeline has an additional execution boundary.

production thread or task
   |
   v
recorder.register_event(event)
   |
   v
recorder internal buffer
   |
   v
runtime-owned thread
   |
   v
runtime-owned asyncio event loop
   |
   v
recorder processing side
   |
   v
metric.handle_event(event)

The recorder remains the production boundary.

MetricsRuntime provides the execution environment behind that boundary.

This allows synchronous and asynchronous production code to use the same recorder-facing contract.

What this pipeline does not include

This pipeline does not include an external monitoring backend.

It ends at metric state and snapshots.

External integration can be added later by using recorder hooks, custom recorders, snapshot consumers, or adapters.

That integration happens after the production boundary.

The production method does not need to change.