Runtime model
MetricsRuntime is the synchronous management layer around an internal metrics event loop.
It gives application code a simple API for creating, finding, stopping, and removing recorders, while the actual
recorder work is executed inside a dedicated runtime thread with its own asyncio event loop.
The important split is:
application thread
|
v
MetricsRuntime public API
|
v
runtime thread / asyncio event loop
|
v
AsyncioMetricsRecorder instances
The recorder is still the boundary used by production components.
MetricsRuntime owns the execution environment where runtime-created recorders live.
Public shape
Application code uses MetricsRuntime through synchronous methods:
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 public API hides the runtime thread and event loop.
Application code does not create the recorder loop and does not start recorder tasks manually.
Runtime thread and event loop
When start() is called, the runtime creates a dedicated thread.
Inside that thread, it creates a new asyncio event loop and runs it.
runtime.start()
|
v
create runtime thread
|
v
create asyncio event loop in that thread
|
v
run loop forever
This loop becomes the owning loop for recorders created by the runtime.
Synchronous API over asynchronous internals
The runtime API is synchronous from the caller’s point of view.
Internally, operations that must run in the runtime loop are scheduled into that loop and the public method waits for the result.
caller thread
|
v
runtime public method
|
v
schedule coroutine on runtime loop
|
v
wait for result
This is what lets ordinary application code manage asyncio-based recorders without owning their event loop directly.
Runtime state
MetricsRuntime has its own lifecycle state.
At a high level, the important states are:
VIRGIN
STARTING
RUNNING
STOPPING
CLOSED
FAILURE
The runtime starts in VIRGIN.
During startup it moves through STARTING.
After successful start(), it becomes RUNNING.
During shutdown it moves through STOPPING.
After successful shutdown(), it becomes CLOSED.
If startup or shutdown fails, it moves to FAILURE.
This state protects runtime operations from being called in the wrong order.
Creating a recorder
Application code creates a recorder through the runtime:
recorder = runtime.create_recorder("document_storage")
From the outside this is one synchronous call.
Internally, the runtime:
validates and normalizes the recorder id;
checks that the runtime is running;
schedules recorder creation inside the runtime event loop;
creates
AsyncioMetricsRecorderin that loop;starts the recorder;
stores it in the runtime registry only after successful startup;
returns the ready-to-use recorder to the caller.
The shape is:
create_recorder("document_storage")
|
v
schedule _create_recorder_core(...) on runtime loop
|
v
create AsyncioMetricsRecorder
|
v
start recorder
|
v
store recorder in registry
|
v
return recorder
This matters because a recorder created by the runtime is already started and attached to the runtime-managed event loop.
The production component receives a ready-to-use recorder:
storage = DocumentStorage(metrics_recorder=recorder)
Recorder options
MetricsRuntime can provide defaults for recorders it creates.
Those defaults include:
recorder queue maximum size;
recorder queue overflow policy;
runtime log context.
When create_recorder() is called, application code can override these values for a specific recorder.
If no override is provided, the runtime default is used.
This keeps common recorder settings centralized while still allowing per-recorder customization.
Recorder id and entity id
The recorder id is the key used by the runtime registry.
In simple cases, the same value also identifies the measured entity:
recorder = runtime.create_recorder("document_storage")
For repeated entities, the id can represent a specific instance:
connection_1 = runtime.create_recorder("connection-001")
connection_2 = runtime.create_recorder("connection-002")
Runtime-created recorders can also receive an explicit entity_id.
If no explicit entity id is provided, the recorder id is used as the recorder identity.
recorder id
|
v
runtime registry key
entity id
|
v
measured scope identity
Often they are the same value.
Recorder registry
The runtime owns a registry of recorders created inside it.
MetricsRuntime(namespace="networking.metrics")
|
+--> recorder: connection-001
|
+--> recorder: connection-002
|
+--> recorder: connection-003
The runtime also tracks recorder creation and removal in progress.
Conceptually, it coordinates:
active recorders
recorders being created
recorders being removed
This prevents overlapping create/remove operations from corrupting registry state.
Finding recorders
Once a recorder exists, application code can retrieve it from the runtime.
The runtime provides operations for:
getting a recorder by id;
trying to get a recorder by id;
listing recorder ids.
These operations use the runtime registry and are exposed through the synchronous public API.
Stopping a recorder
A runtime-created recorder can be stopped through the runtime.
At a high level, stopping means:
stop_recorder(recorder_id)
|
v
schedule stop on runtime loop
|
v
stop recorder lifecycle
Stopping a recorder does not have to remove it from the registry.
That distinction lets runtime code separate:
stop processing
from:
remove this recorder from runtime ownership
Stop and remove
A recorder can also be stopped and removed from the runtime registry.
The conceptual flow is:
stop_and_remove_recorder(recorder_id)
|
v
mark recorder as being removed
|
v
stop recorder if needed
|
v
remove recorder from registry
|
v
clear removal marker
This is useful when a measured entity is gone.
For example, when a connection is permanently closed, its recorder can be stopped and removed.
Event processing path
Once a runtime-created recorder is passed to production code, event submission still goes through the recorder.
self._metrics_recorder.register_event(event=event)
The runtime does not receive metric events directly from production components.
The path is:
production code
|
v
recorder.register_event(event)
|
v
recorder internal buffer
|
v
runtime-owned event loop
|
v
recorder dispatcher
|
v
registered metrics
The runtime provides the loop and thread.
The recorder owns event processing.
The metrics own interpretation and state changes.
Logging
MetricsRuntime can use MVX Logger when a log_context is provided.
Runtime logging is based on log_invocation around public runtime operations.
The logged runtime events are:
metrics_runtime.start
metrics_runtime.shutdown
metrics_runtime.create_recorder
metrics_runtime.stop_recorder
metrics_runtime.stop_and_remove_recorder
metrics_runtime.get_recorder
metrics_runtime.try_get_recorder
metrics_runtime.list_recorder_ids
Lifecycle and mutation operations are the normal runtime logging surface:
metrics_runtime.start
metrics_runtime.shutdown
metrics_runtime.create_recorder
metrics_runtime.stop_recorder
metrics_runtime.stop_and_remove_recorder
Inspection operations are separated:
metrics_runtime.get_recorder
metrics_runtime.try_get_recorder
metrics_runtime.list_recorder_ids
This separation matters because inspection calls can be more frequent and less important for ordinary runtime diagnostics.
For convenience, MVX Metrics provides ready-to-use event policies for runtime logging.
The policy mode is controlled by MetricsRuntimeLogPolicyMode:
SILENT
NORMAL
INSPECTION
The helper functions are:
metrics_runtime_event_policy_config(...)
metrics_runtime_event_policy(...)
SILENT disables runtime log events by default.
NORMAL enables the usual lifecycle and recorder-management events:
metrics_runtime.start
metrics_runtime.shutdown
metrics_runtime.create_recorder
metrics_runtime.stop_recorder
metrics_runtime.stop_and_remove_recorder
INSPECTION includes all NORMAL events and also enables inspection operations:
metrics_runtime.get_recorder
metrics_runtime.try_get_recorder
metrics_runtime.list_recorder_ids
This lets applications start with a practical logging profile instead of writing a custom event policy immediately.
The runtime can also pass its log_context to recorders created inside it. A specific recorder can override that
context when it is created.
So the logging model is:
runtime lifecycle and recorder-management calls
|
v
normal runtime log events
runtime inspection calls
|
v
inspection runtime log events
runtime-created recorders
|
v
use runtime log_context unless overridden
Runtime logging is diagnostic infrastructure logging. It does not log every metric event emitted by production code.
Shutdown
shutdown() closes the runtime-managed environment.
At a high level, shutdown does four things:
stops and removes runtime-owned recorders;
stops the runtime event loop;
joins the runtime thread;
clears runtime references.
The shape is:
runtime.shutdown()
|
v
stop/remove all recorders
|
v
stop event loop
|
v
join runtime thread
|
v
clear thread and loop references
|
v
CLOSED
If recorder shutdown fails, the runtime collects those failures and reports shutdown failure through its own error surface.
This keeps shutdown responsibility centralized in the runtime.
One runtime, many recorders
The normal model is:
one application or large subsystem
|
v
one MetricsRuntime
|
v
many recorders
For example:
application
|
v
MetricsRuntime(namespace="application.metrics")
|
+--> recorder: document_storage
|
+--> recorder: connection-001
|
+--> recorder: connection-002
|
+--> recorder: worker-3
Warning
The usual application model is to create and keep one MetricsRuntime running for the application or for a large subsystem, and then create multiple recorders inside that runtime.
Creating a separate runtime for every component or every measured entity is not the intended usage. Each runtime owns its own thread and event loop, so creating too many runtimes can waste threads and eventually exhaust process resources.
Multiple runtimes should be reserved for cases where metrics processing is intentionally separated, for example when one runtime is not enough for a very heavy metrics workload or when subsystems must be isolated deliberately.
Runtime and production code
Production components should normally depend on a recorder, not on MetricsRuntime.
Good:
recorder = runtime.create_recorder("document_storage")
storage = DocumentStorage(metrics_recorder=recorder)
Avoid making the production component create or own the runtime.
Runtime ownership is an application wiring decision.
Metric event emission is a production component responsibility.
Recorder processing belongs to the runtime-managed recorder environment.
Synchronous and asynchronous production code
A runtime-created recorder can be used by synchronous production code:
def save_document(self, document_id: str, content: str) -> None:
...
self._send_metric_event(event)
and by asynchronous production code:
async def save_document(self, document_id: str, content: str) -> None:
...
self._send_metric_event(event)
The production component does not have to run in the runtime event loop.
The runtime owns the loop used for recorder processing.
Runtime is not a recorder
MetricsRuntime is not a recorder.
It does not own metric instances directly.
It does not interpret metric events.
It does not expose metric meaning.
Its job is to create and manage the environment where recorders live.
MetricsRuntime
|
v
owns thread, event loop, recorder registry
Recorder
|
v
owns metrics and event processing
Metric
|
v
owns measurement state
Runtime is not a monitoring backend
MetricsRuntime does not export metrics to Prometheus, OpenTelemetry, StatsD, or any other platform.
It does not store time-series history.
It does not render dashboards.
It only owns local runtime infrastructure for recorders.
External integration belongs to adapters, custom recorders, recorder hooks, or snapshot consumers.
Summary
MetricsRuntime is the synchronous management layer around an internal metrics event loop.
It starts a dedicated thread, creates an asyncio event loop inside it, creates and starts recorders in that loop,
stores them in a registry, and shuts the whole environment down when metrics processing is no longer needed.
It also exposes runtime diagnostic logging through ready-to-use event policies.
The public API stays simple.
The internal runtime model handles the thread, loop, recorder lifecycle, registry, logging, and shutdown coordination.