Message Flow Tracing¶
Observability topic table of contents · Spec table of contents · Previous: 02. Runtime Metrics · Next: 04. Request Correlation
Defines the per-message progress record (trace), its attributes, and its recording level. The ownership split among this topic's four documents follows the topic table of contents "2. Documents In This Topic".
1. What Can Be Confirmed¶
This document defines the contract for checking, via trace and structured log, which processing stage one message reached in the framework and where it failed. Node, Channel, Spot — a logical target that receives messages — Actor, and STREAM use the same stage names, processing result, and attribute names. This record doesn't change existing message delivery and completion guarantees.
The application configures the record level, the ratio for sampling a normal flow, and whether to record message byte size. The framework builds a record at each processing boundary and delivers it to the standard tracing and structured logging paths. A standard telemetry provider exports records externally, but must not delay message processing or change the result. The framework public interface doesn't expose an exporter, storage, observer, or event DTO.
The creation/propagation/lifetime of
reply correlation — the
identifying information linking a request and reply to the same work — and
flow_id — indicating several messages continue from the same cause — are
defined by 04. Request Correlation. This
document only defines the condition for including the two identifiers in
a record.
2. Which Processing Stages Are Recorded¶
The framework records a regular processing stage as zlink.message_flow.
If payload interpretation, handler, reply delivery path, or protocol
dispatch fails, it uses zlink.dispatch_error. Both event_id strings
are the same in every language.
2.1 Message Flow Stages¶
phase |
Processing boundary represented by this record |
|---|---|
received |
The message arrived at the framework's receive/dispatch boundary. |
admitted |
The target application queue accepted the message. |
dispatched |
The typed application handler started running. |
completed |
A one-way handler with no reply ended in a terminal state. |
replied |
A request handler built a response or error reply. |
sent |
The source's local transport accepted the outbound submission. |
reply_received |
The outbound request received a terminal reply. |
backpressured |
A backpressure state — insufficient send-path or queue capacity — occurred, or capacity wasn't secured by the time limit. |
dropped |
The message was excluded from delivery per policy. |
%%{init: {'sequence': {'actorFontSize': '18px', 'messageFontSize': '18px', 'noteFontSize': '18px', 'boxMargin': 8, 'width': 140}, 'themeVariables': {'fontSize': '18px'}}}%%
sequenceDiagram
participant Source
participant Transport
participant Queue
participant Handler
Source->>Transport: accepts the send (sent)
Transport->>Queue: receives and the queue accepts it (received, admitted)
Queue->>Handler: starts handler execution (dispatched)
Handler-->>Source: builds and receives the reply (replied, reply_received)
sent doesn't mean the remote handler received the message. admitted
also doesn't mean the handler finished running. A request only reaches
reply_received once the caller received the terminal reply.
Logical Multicast, which delivers a message to several Spots using ChannelName — identifying the logical Channel scope — and topic — selecting a receiving target within it — and Classic fanout, which delivers events over a separate PUB/SUB path, don't confirm a per-subscriber result. So neither method builds a message-flow trace.
2.2 The Public Behavior Recorded¶
The framework applies §2.1's stages at the following boundaries.
- Submit, receive, handler dispatch, and reply for Node direct, which specifies one node by MeshName and target RID, and for RouteMesh/ClientServer Channel — RouteMesh being the scope in which multiple MeshNodes exchange node and Channel messages
- Application queue acceptance and handler completion for Spot direct, which sends a message by global Spot ID
- An Instance Spot's source lookup, creation-message submit, the target securing creation authority, the wait before opening application processing, application queue acceptance, and one-way drop
- Actor queue acceptance, handler completion, and relocation terminal result
- A STREAM session's receive, Actor handler dispatch, reply, and send connected to the current session
- Request timeout, cancellation, runtime termination, and handler dispatch error
A wrapper and a transport don't duplicate the same terminal trace. A request has exactly one terminal record per surface. Actor payload isn't recorded as a Spot's handler dispatch stage.
3. Common Attributes¶
Every record uses the following closed values and inclusion conditions. So records built in different languages can be searched and compared under the same criteria.
3.1 Closed Values Shared by Every Language¶
The value indicating which processing method a message uses — send, request, response, error, or control — is called message kind. The following values must match, including case, in every language.
| Attribute | Allowed values |
|---|---|
surface |
node, channel, spot, instance_spot, actor, stream, actor_relocation, classic_fanout |
message_kind |
send, request, response, error, control |
outcome |
succeeded, failed, backpressured, dropped, cancelled, shutdown |
channel_route_kind |
route_mesh, client_server |
activation_state |
activating, ready, closing |
shutdown means the state where the
runtime is terminating and doesn't accept new operations.
When zlink.message_flow records the cause of a failure, backpressure,
or drop, reason is one of the following values.
backpressure, stale_target, target_closed, shutdown,
location_unavailable, activation_rejected, activation_timeout.
The last three values respectively mean that an Instance Spot's location
couldn't be found, new Spot creation was declined, or creation wasn't
finished within the time limit. Instance Spot close and lease fencing are
recorded as target_closed.
zlink.dispatch_error always uses outcome=failed. reason is one of
the following values.
no_handler, decode_error, handler_exception, invalid_frame,
reply_path_missing, unexpected_reply, backpressure, stale_target,
shutdown.
action |
The result of the framework handling the failure |
|---|---|
reply_error |
Sent an error reply to a request that had a reply delivery path. |
fail_caller |
Ended the local call as a terminal failure. |
drop |
Stopped processing a one-way operation with no reply. |
3.2 Attribute Inclusion Conditions¶
| Attribute | Inclusion condition and meaning |
|---|---|
event_id |
Included in every record. The value is zlink.message_flow or zlink.dispatch_error. |
timestamp |
Included in every record. The time the framework observed that boundary. |
phase |
Included in zlink.message_flow. |
surface, message_kind, outcome |
Included in every message-flow record. |
reason |
Included when there's a failure, backpressure, or drop cause. |
action |
Included in zlink.dispatch_error. |
channel_name |
Included when a logical Channel address exists. |
channel_route_kind |
Included only when surface=channel; not included for classic_fanout. |
mesh_name |
Included when there's a Node direct or RouteMesh scope. |
server_rid |
Included when a ClientServer target was selected. |
source_rid, target_rid |
Included when a routed hop has that identity. |
packet_name |
Included when there's a packet name used to find a typed handler. |
topic, spot_id, actor_id |
Included when that surface uses a logical target. |
instance_spot_type, activation_state |
Included when Instance Spot processing has that value. |
correlation_id |
Included when linking a request and terminal reply. |
flow_id, flow_origin |
Both included together when recording a message flow continuing from the same cause. |
message_size_bytes |
Only included if message size recording is on at Detailed level. |
duration_seconds |
Included when a terminal record for an operation or handler provides elapsed time. |
channel_route_kind, mesh_name, and server_rid aren't inputs for
finding a handler or selecting a target. A trace doesn't include payload,
application metadata values,
native handle, raw frame, or an exception object. When recording an error
description as a string, it's limited to an implementation-defined maximum
length and doesn't include a secret or stack trace.
Structured Log Substitute Notation¶
An implementation providing a structured log as a substitute uses the
zlink flow: prefix and the following keys as-is.
event, phase, surface, kind, mesh, channel, channel_route,
source_rid, target_rid, server_rid, packet, topic, spot,
instance_type, activation_state, actor, corr, flow, origin,
outcome, reason, size.
Normal publish and subscriber delivery for Logical Multicast and Classic
fanout don't build a zlink.message_flow record. If local dispatch at a
Classic fanout subscriber has no handler, the subscriber process records
zlink.dispatch_error with surface=classic_fanout, message_kind=send,
outcome=failed, reason=no_handler, and action=drop through its
logger provider. That record has no channel_route_kind and isn't
returned as a per-publisher delivery result.
4. How the Application Sets the Recording Scope — Level and Sampling¶
The application sets the diagnostics level to one of four values.
| Level | Scope the framework records |
|---|---|
Off |
Doesn't record message flow or dispatch error. |
Errors |
Only records dispatch error, backpressure, and drop. |
Normal |
Records errors and §2.1's main stages. |
Detailed |
Can add message byte size and terminal elapsed time to Normal records. |
The default is Errors. The message size setting only adds byte size,
not payload content. Diagnostics level doesn't turn off metric recording.
Sampling rate is the ratio of normal flows to record, in the range
0.0..1.0. A value outside the range is treated as a startup or public
argument error. The framework selects normal flows using a hash of
flow_id, so every hop of the same flow is either all recorded or all
excluded. zlink.dispatch_error, backpressured, and dropped aren't
sampled. With no flow_id, whether to record is decided by the
generation and local sequence of the source
MeshNode — the runtime
node that participates in RouteMesh to send or receive messages.
The following C# is a non-normative excerpt explaining the common behavior. It doesn't require the same signature in other languages, and the precise .NET declaration is defined by .NET Topology Monitoring.
public interface IZLinkDiagnosticsOptions
{
IZLinkDiagnosticsOptions SetLevel(ZLinkDiagnosticsLevel level);
IZLinkDiagnosticsOptions SetSampleRate(double rate);
IZLinkDiagnosticsOptions IncludeMessageSizes(bool include);
}
options.ConfigureDispatch().Diagnostics
.SetLevel(ZLinkDiagnosticsLevel.Normal) // records errors and the main processing boundaries.
.SetSampleRate(0.1) // selects each normal flow as a whole and records only 10%.
.IncludeMessageSizes(false); // records neither payload content nor byte size.
Public configuration only provides level, sampling rate, and whether to include message size. Exporter, logger provider, and storage backend are owned by standard telemetry configuration.
5. Changing the Record Level at Runtime and the Cost Rule¶
The application can change diagnostics level without restarting the process. The level specified at startup is the initial value, and a runtime change applies to all Node, Channel, Spot, Actor, and STREAM processing in the process. A separate toggle per surface isn't provided. Each language's public interface must provide runtime control to read and change the process's current level.
The change must be an atomic state change that doesn't make message processing wait. Each processing point checks the current level once before building trace data. Processing points that observe the changed level apply the new setting starting from there. A record already in the telemetry queue before the change can be delivered or discarded. Turning the level back on doesn't later build records for a previous processing stage.
At Off, the per-message hot path eliminates even the cost of building
a log message. Beyond reading and branching on the current level, no
trace-only work happens.
- Doesn't build an event object or attribute collection.
- Doesn't assemble a string or collect timestamp and elapsed time.
- Doesn't copy payload and metadata or compute their size.
- Doesn't compute a sampling hash or trace-only
flow_id. - Doesn't build or format a structured log message.
- Doesn't build a telemetry queue item or internal delivery message, or put one on a queue.
- Doesn't call a logger, observer, exporter, or telemetry provider.
So an implementation that only blocks output at the log provider doesn't
satisfy the Off contract. It must exit at the first trace branch on the
message hot path, not right before the provider call.
The per-message tracing hot path is wrapped in if (enabled(outcome)),
so it builds neither an event nor a lambda. A rare transition, such as
failure or abort, only builds an event via a lazy form
(trace(outcome, build) / traceLazy), after the check passes. The lazy
form removes the if at the call site but still allocates one lambda
(C++ inlines it, so there's no allocation), so on the hot path even the
lazy form is wrapped in one more if to block even lambda creation. No
call site assembles a string before the gate.
Language-specific discretion — how the gate is expressed differs by
language. C++ uses a template lambda, .NET uses an interpolated-string
handler and Func<>, Java uses Supplier<>, Node uses a thunk. As long
as the observed result is the same — that when tracing is off, no string,
event, or lambda is built, so the cost is zero — the method is
discretionary. The criterion for this discretion is confirmation in the
call-site code that, after adding a new trace, the path builds no string,
event, or lambda while tracing is off.
6. Completion, Failure, and Lifetime¶
A trace's sent, admitted, handler completion, and reply receipt are
different completion boundaries. Whether the trace record itself
succeeded isn't part of a message operation's completion condition.
Timeout, cancellation, and shutdown decide the result per the original
message operation's contract — tracing doesn't add retry or route
re-selection. Tracing doesn't change routing, handler dispatch, or
lifecycle decisions.
A worker doesn't wait for a slow or failed telemetry provider. If a
bounded telemetry queue fills up, it can drop a normal trace and
increment zlink.observability.events.overflow. Provider failure isn't a
message operation failure.
An implementation logging a provider failure limits how many times the same error is recorded. It doesn't call the same provider again to create a trace for this log. Without a provider, it avoids an allocation made just for the trace.
When the provider failure itself is recorded, the implementation uses the application's fallback logger or process stderr, not the failed provider. Failure of that fallback record also doesn't change the message-operation result, dispatch, or lifecycle.
A trace attribute only includes the identifier needed for diagnosis, and
doesn't reference a caller buffer or runtime object after message
processing ends. The ownership and lifetime of correlation_id,
flow_id, and flow_origin follow
04. Request Correlation, and the three
values aren't used as a metric label.
An Instance Spot's one-way creation failure is recorded exactly once as
surface=instance_spot, phase=dropped, without building a hidden
request or replay.
7. Verification Requirements¶
The following is verified using only the public surface —
event_id, phase, surface/message_kind/outcome/reason/action,
attribute keys, and the diagnostics level/sampling rate configuration
interface. Each item corresponds to one contract test.
Closed values and attributes
- Every language uses the same
event_id, phase, surface, message kind, outcome, reason, action, and attribute keys. - Payload and application metadata values don't appear in trace or structured log.
- Logical Multicast and Classic fanout don't build a message-flow trace.
- A missing subscriber-local Classic fanout handler builds a dispatch
error with
surface=classic_fanout,reason=no_handler, andaction=drop, withoutchannel_route_kind. - Each request surface records the terminal trace exactly once.
- An Instance Spot's one-way creation failure is recorded exactly once as
surface=instance_spot,phase=dropped, without building a hidden request or replay. - RouteMesh and ClientServer paths using the same ChannelName are
distinguished by
channel_route_kind, without requiring an application handler to provide this value.
Level changes and the Off cost
- Changing the level between
Offand a different value at runtime applies the new level starting from processing points after the change. - After turning tracing off at runtime, a new processing point doesn't add existing flow information to the context or outbound envelope, and turning it back on doesn't retroactively apply it to a processing stage that already passed.
- On a path that doesn't record because of the level or sampling, no
payload or metadata appears in a trace or log. The allocation, queue and
call-site cost conditions of the
Offpath are owned as internal check conditions by the rule paragraphs of §5.
Provider and public interface
- Telemetry provider failure doesn't change handler dispatch, reply, or lifecycle results.
- Exporter, storage, observer callback, runtime error sink, and raw event DTO don't appear in the public interface.
Observability topic table of contents · Spec table of contents · Previous: 02. Runtime Metrics · Next: 04. Request Correlation