Context formatter
This example shows how context_formatter can control the final payload shape built from context_fields.
Use this when a flat list of context fields is not enough and the payload needs a deliberate structure.
Example
from typing import Any
from mvx.common.logger import LogContextProto, log_invocation
def connection_payload(
ctx: LogContextProto,
event_outcome: object,
event: str,
fields: dict[str, Any],
) -> dict[str, Any]:
return {
"connection": {
"event": event,
"outcome": str(event_outcome),
"state": ctx.normalize_value_for_log(fields.get("state")),
"peer": ctx.normalize_value_for_log(fields.get("peer")),
}
}
class Connection:
def __init__(self, log_context: LogContextProto) -> None:
self._log_context = log_context
self.state = "closed"
self.peer = "ldap.example.local"
def get_log_context(self) -> LogContextProto | None:
return self._log_context
@log_invocation(
"open",
context_fields=("state=self.state", "peer=self.peer"),
context_formatter=connection_payload,
)
async def open(self) -> None:
self.state = "opened"
The decorated public API operation is open.
The selected context fields are:
state=self.state
peer=self.peer
The formatter receives those resolved fields and returns the final payload shape.
Emitted records
A successful call:
await connection.open()
emits records conceptually equivalent to:
[
{
"event_name": "open",
"event_outcome": "invoke",
"payload": {
"connection": {
"event": "open",
"outcome": "invoke",
"state": "closed",
"peer": "ldap.example.local",
},
},
},
{
"event_name": "open",
"event_outcome": "success",
"payload": {
"connection": {
"event": "open",
"outcome": "success",
"state": "opened",
"peer": "ldap.example.local",
},
},
},
]
The payload is nested under the connection key because the formatter returned that structure.
Why use a formatter
Without context_formatter, the selected fields would be injected as top-level payload fields:
{
"state": "closed",
"peer": "ldap.example.local",
}
With context_formatter, the example can produce a more intentional shape:
{
"connection": {
"event": "open",
"outcome": "invoke",
"state": "closed",
"peer": "ldap.example.local",
}
}
This is useful when the payload should be grouped, transformed, or enriched before it is emitted.
Raw fields and normalization
The formatter receives raw values resolved from context_fields.
In this example, the formatter explicitly normalizes values before returning them:
ctx.normalize_value_for_log(fields.get("state"))
ctx.normalize_value_for_log(fields.get("peer"))
This makes the formatter responsible for the final custom shape while still reusing the context normalization rules.
Outcome-specific data
The formatter receives both:
event
event_outcome
That allows the payload to include operation identity and lifecycle state.
In this example:
invoke -> connection.outcome = "invoke"
success -> connection.outcome = "success"
The same formatter also sees dynamic context values separately for each outcome:
invoke -> connection.state = "closed"
success -> connection.state = "opened"
What this example demonstrates
This example demonstrates that context_formatter can take selected context fields and build a custom payload structure.
It shows four important points:
context_fields select the raw values
context_formatter receives event and event_outcome
context_formatter returns the final payload shape
dynamic fields are still resolved separately for each emitted outcome
Use this pattern when a public API operation needs a stable, meaningful payload shape that cannot be expressed as a flat list of fields.