Metric events
MetricEvent is the input unit of MVX Metrics.
A metric event describes something that happened in production code.
It is not a counter.
It is not a metric snapshot.
It is not a monitoring backend message.
It is a structured domain fact that can later be interpreted by one or more metrics.
Core idea
Production code emits facts.
Metrics decide how those facts become measurements.
For example, production code should not say:
success_total += 1
Instead, it should say:
a document save attempt completed successfully
Then a metric can decide that this fact should increment success_total.
Another metric may ignore the event.
A future metric may use the same event to calculate something else.
This is the main reason metric events exist.
They keep production code focused on what happened, while metrics own the quantitative interpretation.
A metric event is a structured fact
In the first example, DocumentStorage.save_document() emits this event:
DocumentSaveAttemptMetricEvent(
outcome=DocumentSaveAttemptOutcome.SUCCESS,
)
The event type is:
@dataclass(frozen=True, slots=True)
class DocumentSaveAttemptMetricEvent(MetricEvent):
outcome: DocumentSaveAttemptOutcome
@property
def event_type(self) -> str:
return "document_storage.save.attempt"
This event says:
document_storage.save.attempt happened
outcome = SUCCESS
That is all.
It does not say which counters should be updated.
It does not know which metrics are registered.
It does not know how snapshots are built.
It only carries the domain fact.
Event type
Every metric event exposes an event_type.
@property
def event_type(self) -> str:
return "document_storage.save.attempt"
The event type identifies what kind of fact the event represents.
A good event type should be stable and domain-specific.
For example:
document_storage.save.attempt
tcp_stream.read.attempt
tcp_stream.bytes.received
ldap.bind.attempt
The event type is not a metric name.
This distinction matters.
An event type names the fact that happened.
A metric name names the aggregate that measures something based on events.
For the first example:
event type:
document_storage.save.attempt
metric name:
document_storage.save.attempts
The event is the input.
The metric is the aggregate.
Event payload
A metric event should carry useful data for metrics.
The first example keeps the payload small:
outcome: DocumentSaveAttemptOutcome
That is enough to count:
total
success_total
failure_total
But real events often need more information.
For example, a save operation event may carry:
outcome
duration_ms
content_size
A network read event may carry:
outcome
bytes_received
duration_ms
A database query event may carry:
outcome
duration_ms
rows_returned
This data is not the metric state yet.
It is only event payload.
Metrics can later decide how to use it.
Extending the document example
The first DocumentStorage example counts only attempts and outcomes.
Now imagine that later we also want to measure how much data is saved and how long saving takes.
The event can grow from this:
@dataclass(frozen=True, slots=True)
class DocumentSaveAttemptMetricEvent(MetricEvent):
outcome: DocumentSaveAttemptOutcome
@property
def event_type(self) -> str:
return "document_storage.save.attempt"
to this:
@dataclass(frozen=True, slots=True)
class DocumentSaveAttemptMetricEvent(MetricEvent):
outcome: DocumentSaveAttemptOutcome
content_size: int
duration_ms: float
@property
def event_type(self) -> str:
return "document_storage.save.attempt"
The event still represents the same domain fact:
a document save attempt happened
But now it carries more information about that fact.
A simple attempt-counting metric may use only outcome.
A size metric may use content_size.
A duration metric may use duration_ms.
The production method still emits one structured event.
Different metrics may use different parts of its payload.
Payload is not interpretation
This is an important distinction.
The event may carry:
duration_ms = 12.4
content_size = 1840
outcome = SUCCESS
But the event does not decide what those values mean.
It does not decide whether 12.4 ms is fast or slow.
It does not decide whether 1840 bytes should be added to a total, averaged, bucketed, ignored, or exported.
That is the metric’s responsibility.
For example:
DocumentSaveAttemptsMetric
uses outcome
DocumentSavedBytesMetric
uses content_size
DocumentSaveDurationMetric
uses duration_ms
The same event can provide useful data to all of them.
Why production code should emit events, not counters
If production code updates counters directly, it starts to know too much.
It must know:
which counters exist
which counter changes on success
which counter changes on failure
which counter uses bytes
which counter uses duration
which backend receives the values
That creates tight coupling between business logic and observability logic.
With metric events, production code only knows:
what happened
what useful data belongs to that fact
Metrics handle the rest.
This makes it easier to add, remove, or change metrics later without rewriting the business method.
Where metric events should live
Metric events should normally live close to the production component that emits them.
This is because event payload is domain-specific.
The DocumentStorage component knows what a document save attempt means.
The TCP stream engine knows what a read attempt, write attempt, TLS error, or remote disconnect means.
The LDAP adapter knows what bind, search, or schema load attempts mean.
Those event definitions belong near the domain code, not inside generic metrics infrastructure.
The recorder and runtime should not need to understand these event types.
They only move events to metrics.
Event fields should be useful to metrics
Metric event payload should be selected for measurement.
Good event fields are values that metrics may reasonably need.
Examples:
outcome
duration_ms
bytes_sent
bytes_received
rows_returned
retry_count
refusal_reason
error_category
Poor event fields are values that are only useful for detailed diagnostics and not for measurement.
Those usually belong to logging, not metrics.
For example:
full document content
full request body
full exception traceback
large user payload
debug-only object dumps
MVX Logger and MVX Metrics solve different problems.
Logs can carry diagnostic context.
Metric events should carry compact measurement input.
Keep the first event small
A metric event does not have to include every possible value from the beginning.
The first version of the document example uses only:
outcome: DocumentSaveAttemptOutcome
That is enough for the first metric.
Later, if the component needs more measurements, the event can be extended deliberately.
The goal is not to make every event large.
The goal is to include the data that metrics need in order to measure the behavior of the component.
What happens after the event is emitted
After production code emits a metric event, the recorder receives it.
The recorder does not interpret the event.
It does not know the business meaning of the event payload.
It dispatches the event to registered metrics.
Each metric decides whether it can handle the event.
If a metric accepts the event, it updates its own measured state.
Later, snapshots expose that state.
The path is:
production method
-> metric event
-> recorder
-> registered metrics
-> metric state
-> snapshot
Summary
A MetricEvent is a structured domain fact.
It should describe what happened and carry compact data that metrics may need.
It should not update counters.
It should not know which metrics will consume it.
It should not know how results will be exported or displayed.
The production component emits the event.
Metrics interpret it.