Ownership model
MVX Metrics uses explicit ownership boundaries.
Each part of the system owns one kind of responsibility:
application code
|
v
owns MetricsRuntime lifecycle
MetricsRuntime
|
v
owns recorder environment and recorder registry
recorder
|
v
owns registered metric instances for one measured scope
metric
|
v
owns measured state
production component
|
v
owns business behavior and emits metric events
This page explains these ownership boundaries.
It does not describe the full event processing path. That is covered in the processing pipeline page.
Application owns the runtime
Application code owns MetricsRuntime.
It decides:
whether metrics are enabled;
when metrics processing starts;
which runtime namespace is used;
which recorders are created;
when the runtime is shut down.
Typical wiring:
runtime = MetricsRuntime(namespace="example.metrics")
runtime.start()
try:
recorder = runtime.create_recorder("document_storage")
storage = DocumentStorage(metrics_recorder=recorder)
storage.save_document("doc-001", "First document")
finally:
runtime.shutdown()
The production component does not create the runtime.
The production component receives a recorder.
This keeps runtime lifecycle out of business code.
MetricsRuntime owns the recorder environment
MetricsRuntime owns the execution environment used by recorders created inside it.
It owns:
a dedicated thread;
an
asyncioevent loop inside that thread;the registry of runtime-created recorders;
runtime startup and shutdown.
A runtime-created recorder belongs to that runtime.
MetricsRuntime(namespace="example.metrics")
|
v
recorder: document_storage
For larger systems, one runtime can own many recorders:
MetricsRuntime(namespace="networking.metrics")
|
+--> recorder: connection-001
|
+--> recorder: connection-002
|
+--> recorder: connection-003
The usual model is one runtime for an application or large subsystem, with multiple recorders inside it.
Recorder owns metric instances
A recorder owns the metric instances registered inside it.
recorder: document_storage
|
+--> DocumentSaveAttemptsMetric
|
+--> DocumentSaveAverageDurationMetric
If the same metric class is used in several recorders, each recorder still owns its own metric instance.
recorder: connection-001
|
v
TcpStreamBytesReceivedMetric(total=1200)
recorder: connection-002
|
v
TcpStreamBytesReceivedMetric(total=8750)
The metric class is reusable.
The metric state is not shared.
This allows per-entity metrics.
Recorder owns one measured scope
A recorder represents one measured scope.
The scope may be:
one component;
one connection;
one session;
one worker;
one protocol client;
one domain object.
The recorder identity should normally match that scope.
recorder identity: connection-001
|
v
metrics for connection-001
This does not mean the recorder understands the domain.
The recorder does not know what a connection is.
It only owns the metric instances associated with that recorder identity.
Metric owns measured state
A metric owns its internal measured state.
For example:
class DocumentSaveAttemptsMetric(Metric):
def __init__(self) -> None:
self._total = 0
self._success_total = 0
self._failure_total = 0
Only the metric updates this state.
The production component does not update it.
The recorder does not update it directly.
Other metrics do not update it.
The metric changes its state when it accepts an event in handle_event().
MetricEvent
|
v
metric.handle_event(event)
|
v
metric updates its own state
Metric owns interpretation
A metric also owns interpretation.
The recorder can deliver events to the metric, but the metric decides whether the event matters.
def handle_event(self, event: MetricEvent) -> bool:
if not isinstance(event, DocumentSaveAttemptMetricEvent):
return False
...
return True
This keeps business measurement rules inside metric classes.
For example:
DocumentSaveAttemptsMetric
|
v
knows how save-attempt outcomes change attempt counters
DocumentSaveAverageDurationMetric
|
v
knows how save-attempt durations change duration state
The recorder remains generic.
Production component owns business behavior
The production component owns the business operation.
For example:
class DocumentStorage:
def save_document(self, document_id: str, content: str) -> None:
...
The production component decides when a domain operation succeeds or fails.
It emits a metric event to describe that fact.
It does not own metric state.
It does not own recorder processing.
It does not own runtime lifecycle.
In the usual integration pattern, the component receives an optional recorder:
class DocumentStorage:
def __init__(
self,
*,
metrics_recorder: MetricsRecorderProto | None = None,
) -> None:
self._metrics_recorder = metrics_recorder
self._register_metrics()
This keeps metrics integration explicit but optional.
Snapshot ownership
A metric creates its own snapshot.
def snapshot(self) -> Mapping[str, Any]:
return {
"name": self.metric_name,
"dimensions": {
...
},
}
The recorder collects snapshots from metrics registered inside it.
snapshots = recorder.get_metric_snapshots()
So ownership is layered:
metric
|
v
creates metric snapshot
recorder
|
v
collects snapshots for its registered metrics
application code
|
v
reads recorder snapshots
A snapshot is public state, but it is still produced by the metric that owns the underlying measured state.
Ownership summary
The ownership model can be summarized as:
Application
|
v
owns MetricsRuntime lifecycle
MetricsRuntime
|
v
owns recorder registry and processing environment
Recorder
|
v
owns metric instances for one measured scope
Metric
|
v
owns measured state and interpretation
Production component
|
v
owns business behavior and emits events
Each owner has a narrow role.
This is what keeps the system modular.
Why this matters
The ownership model keeps the main parts replaceable.
Production code can keep emitting the same events.
Metrics can change their internal state or snapshot shape.
Recorders can change processing or delivery behavior.
Runtime wiring can change at application level.
External adapters can be added later.
The important part is that these changes do not require one component to take ownership of another component’s job.
production code
|
v
does not own metric state
metric
|
v
does not own runtime lifecycle
recorder
|
v
does not own business meaning
runtime
|
v
does not own domain behavior
That separation is the core of the architecture.