<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Opentelemetry on Corebaseit — Design System Engineering · POS · Payments · Security</title><link>https://corebaseit.com/tags/opentelemetry/</link><description>Recent content in Opentelemetry on Corebaseit — Design System Engineering · POS · Payments · Security</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>contact@corebaseit.com (Vincent Bevia)</managingEditor><webMaster>contact@corebaseit.com (Vincent Bevia)</webMaster><lastBuildDate>Wed, 09 Sep 2026 11:00:00 +0200</lastBuildDate><atom:link href="https://corebaseit.com/tags/opentelemetry/index.xml" rel="self" type="application/rss+xml"/><item><title>Observability in Distributed Systems: From Technical Telemetry to Business Confidence</title><link>https://corebaseit.com/corebaseit_posts/observability-distributed-systems/</link><pubDate>Wed, 09 Sep 2026 11:00:00 +0200</pubDate><author>contact@corebaseit.com (Vincent Bevia)</author><guid>https://corebaseit.com/corebaseit_posts/observability-distributed-systems/</guid><description>&lt;img src="https://corebaseit.com/diagrams/Engineering_True_Observability_Guide.png" alt="Featured image of post Observability in Distributed Systems: From Technical Telemetry to Business Confidence" />&lt;p>A customer request in a distributed system rarely finishes inside one process. It may pass through an API, a database, a message broker, a background worker, and one or more external services before the business operation reaches a terminal state. Splitting work across those boundaries can raise capacity and isolate some faults. It also creates failures a single-process debugger does not see: an HTTP 200 that conceals a later-stranded transaction, or a timeout that is compatible both with “the remote operation failed” and with “it succeeded and the response was lost.”&lt;/p>
&lt;p>This post treats observability as the evidence required to investigate those situations, including questions that were not anticipated when the system was designed. It covers structured logs, metrics, and traces; why those signals are incomplete without durable business identifiers and lifecycle state; how asynchronous boundaries break causality; why a timeout does not prove failure; and how SLOs and alerts should be tied to outcomes rather than to infrastructure health. Collecting logs and deploying dashboards is not the same problem. The system has to emit correlated telemetry, represent uncertainty honestly, and give operators a path from aggregate impact to an authoritative record of what happened.&lt;/p>
&lt;p>Related reading: &lt;a class="link" href="https://corebaseit.com/corebaseit_posts/double-charging/" >double charging and lost confirmation&lt;/a>, &lt;a class="link" href="https://corebaseit.com/corebaseit_posts/tracing-payment-latency-iso8583/" >tracing payment latency in ISO 8583&lt;/a>, &lt;a class="link" href="https://corebaseit.com/corebaseit_posts/monolith-to-microservices-migration/" >from monolith to microservices&lt;/a>.&lt;/p>
&lt;h2 id="claims-to-keep-separate">Claims to keep separate
&lt;/h2>&lt;p>&lt;strong>Confirmed by specifications and primary sources:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>OpenTelemetry treats traces, metrics, and logs as complementary signals, and defines observability as the ability to investigate novel problems (“unknown unknowns”) from telemetry the system already emits (&lt;a class="link" href="https://opentelemetry.io/docs/concepts/observability-primer/" target="_blank" rel="noopener"
>OpenTelemetry Observability Primer&lt;/a>, &lt;a class="link" href="https://opentelemetry.io/docs/concepts/signals/" target="_blank" rel="noopener"
>Signals&lt;/a>).&lt;/li>
&lt;li>W3C Trace Context defines the &lt;code>traceparent&lt;/code> HTTP header so participating systems can continue the same distributed trace. The header carries version, trace-id, parent-id (the caller’s span), and trace-flags (&lt;a class="link" href="https://www.w3.org/TR/trace-context/" target="_blank" rel="noopener"
>W3C Trace Context&lt;/a>).&lt;/li>
&lt;li>Google SRE defines SLIs, SLOs, and SLAs as distinct, and argues that reliability should be measured from the user-facing behaviour you care about, with error budgets as the control loop (&lt;a class="link" href="https://sre.google/sre-book/service-level-objectives/" target="_blank" rel="noopener"
>Service Level Objectives&lt;/a>).&lt;/li>
&lt;li>The same SRE material distinguishes symptoms from causes, names latency, traffic, errors, and saturation as four golden signals for user-facing services, and requires pages to be urgent, actionable, and user-visible (&lt;a class="link" href="https://sre.google/sre-book/monitoring-distributed-systems/" target="_blank" rel="noopener"
>Monitoring Distributed Systems&lt;/a>).&lt;/li>
&lt;li>Unbounded metric labels (user IDs, request IDs, trace IDs) create a new time series per value and can exhaust a metrics backend (&lt;a class="link" href="https://prometheus.io/docs/practices/instrumentation/#do-not-overuse-labels" target="_blank" rel="noopener"
>Prometheus instrumentation&lt;/a>).&lt;/li>
&lt;li>OpenTelemetry uses span links, not parent-child, as the default correlation for many messaging patterns because a span has one parent and batching, fan-out, and ambient consumer context do not fit that tree (&lt;a class="link" href="https://opentelemetry.io/docs/concepts/signals/traces/" target="_blank" rel="noopener"
>Traces&lt;/a>, &lt;a class="link" href="https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/" target="_blank" rel="noopener"
>Messaging spans&lt;/a>).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Interpretation (mine, not a standard):&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>“State” in the operating model below is durable business-lifecycle history. It is not an OpenTelemetry signal. OTel currently documents traces, metrics, logs, and baggage; profiles are still under development.&lt;/li>
&lt;li>The Metrics → Traces → Logs → State sequence is an investigation heuristic, not a protocol. Signals are complementary; the order is how an on-call engineer usually narrows a problem.&lt;/li>
&lt;li>&lt;code>UNKNOWN_EXTERNAL_STATE&lt;/code>, the example SLI, and the seven-phase rollout are a design for asynchronous work with external side effects. They assume you can persist a state machine, the remote API accepts an idempotency key or equivalent, and some later process can read external truth. Those capabilities are not free, and not every vendor API provides them.&lt;/li>
&lt;li>Figures in this post (the 10,000-transaction funnel, the 99.0% / five-minute SLO, the 6.8% alert) are illustrative. They are not measurements from a named system.&lt;/li>
&lt;/ul>
&lt;h2 id="1-monitoring-answers-known-questions-observability-supports-investigation">1. Monitoring answers known questions; observability supports investigation
&lt;/h2>&lt;p>Traditional monitoring is built around questions you already know to ask:&lt;/p>
&lt;ul>
&lt;li>Is CPU utilization too high?&lt;/li>
&lt;li>Is the API returning errors?&lt;/li>
&lt;li>Is the database connection pool exhausted?&lt;/li>
&lt;li>Is the queue growing?&lt;/li>
&lt;/ul>
&lt;p>Those questions matter. They are not sufficient once a request spans several services and an asynchronous worker.&lt;/p>
&lt;p>An engineer then needs to ask questions like:&lt;/p>
&lt;ul>
&lt;li>Why are transactions slower for one customer category?&lt;/li>
&lt;li>Did an external operation succeed before the connection dropped?&lt;/li>
&lt;li>Why was the same message processed twice?&lt;/li>
&lt;li>Which calculation version produced a disputed result?&lt;/li>
&lt;li>Where did a transaction stop progressing?&lt;/li>
&lt;/ul>
&lt;p>OpenTelemetry’s primer frames this gap directly: observability is the ability to ask questions about a system from the outside, including questions you did not anticipate, provided the application already emits enough telemetry. Monitoring still has a job. It watches known conditions. Observability is what you need when the failure is a combination you did not put on a dashboard.&lt;/p>
&lt;p>A practical operating model is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Metrics → Something is wrong
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Traces → Where it is going wrong
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Logs → What happened at that location
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>State → What the system believes about the business operation
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The last line is the part most telemetry stacks do not give you for free. Traces reconstruct one execution. Business state reconstructs what the platform currently asserts, and what it asserted earlier, about a durable operation that may have spanned several traces.&lt;/p>
&lt;h2 id="2-the-core-observability-signals">2. The core observability signals
&lt;/h2>&lt;h3 id="21-logs-what-happened">2.1 Logs: what happened?
&lt;/h3>&lt;p>Logs record individual events. In production they should be structured so software can search, validate, and correlate them.&lt;/p>
&lt;p>A weak message:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Transaction failed
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A structured event:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;2026-09-09T10:42:17.381Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;level&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;error&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;event&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;external_operation.timeout&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;service&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;transaction-worker&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;transactionId&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;TX-18472&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;traceId&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;4bf92f3577b34da6a3ce929d0e0e4736&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;spanId&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;00f067aa0ba902b7&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;messageId&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;MSG-8125&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;operation&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;refund&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;attempt&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">2&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;timeoutMs&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">5000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;durationMs&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">5008&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;outcome&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;unknown_external_state&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The identifiers in that example follow the sample values in the W3C Trace Context specification. The event names the transaction, the execution, the transport message, the attempt, and how the system classified the outcome. That last field is doing real work: &lt;code>unknown_external_state&lt;/code> is not a synonym for &lt;code>failed&lt;/code>.&lt;/p>
&lt;p>A logging standard should define canonical event names, common field names and units, business and execution identifiers, error classifications, state-transition fields, retention, and redaction. Credentials, tokens, personal data, and complete payloads do not belong in telemetry by default. Usefulness during an incident is not a privacy policy.&lt;/p>
&lt;p>Volume is the operational constraint. Structured logs are cheap to emit and expensive to keep. High-cardinality fields that belong on a log line (transaction ID, message ID) must not migrate onto metrics as labels. That split is a cost control, not a style preference.&lt;/p>
&lt;h3 id="22-metrics-how-often-and-how-much">2.2 Metrics: how often and how much?
&lt;/h3>&lt;p>Metrics aggregate behaviour over time: rates, distributions, saturation, and change.&lt;/p>
&lt;p>Infrastructure metrics commonly include CPU and memory, request and error rates, latency percentiles, database query latency, connection-pool saturation, queue depth, age of the oldest queued message, worker restarts, and dead-letter size. Those describe the platform. They do not necessarily describe the customer outcome.&lt;/p>
&lt;p>A system can show normal CPU while a population of transactions sits in a non-terminal state. Business-oriented counters close that gap:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>transactions_requested_total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transactions_completed_total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transactions_failed_total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transactions_unknown_total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transaction_processing_duration_seconds
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>transaction_retries_total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>duplicate_messages_detected_total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>idempotency_conflicts_total
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>reconciliation_mismatches_total
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The useful picture is a funnel, not a single success rate. An illustrative snapshot, not a measured workload:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Requested: 10,000
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── Completed: 9,650
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── Pending: 300
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └── Failed: 50
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A server-health dashboard can look green while 300 operations have not reached a known terminal state. That is the class of problem these metrics exist to catch.&lt;/p>
&lt;p>Keep metric dimensions bounded: operation, region, outcome, dependency category. Unique transaction, message, and trace identifiers belong on logs and spans. Prometheus’s instrumentation guidance is explicit: each label set is another time series, and unbounded values (user IDs, request IDs, and similar) can make the monitoring system expensive or unstable. The diagnostic power you want from those identifiers is real. It belongs in traces and structured logs, where a new value does not create a standing series.&lt;/p>
&lt;h3 id="23-traces-where-did-it-happen">2.3 Traces: where did it happen?
&lt;/h3>&lt;p>A distributed trace is the journey of one logical operation through multiple components. Each unit of work inside that journey is a span.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Trace = the complete journey
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Span = one operation within that journey
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>An asynchronous transaction, timings illustrative:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>POST /transaction 45 ms
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── Database insert 8 ms
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└── Publish message 11 ms
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ⋮
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ⋮ asynchronous queue delay
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ⋮
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Background worker 5.3 s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── Database read 7 ms
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├── External service call 5.1 s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└── Database update 12 ms
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The trace identifies the external call as the dominant contributor to elapsed time. Without tracing, an engineer searches API logs, finds a message, locates a worker, aligns timestamps, then searches integration logs. At any serious request rate those streams are interleaved, retries create extra executions, and clock skew between hosts makes timestamp joins ambiguous. Tracing replaces that reconstruction with a graph of recorded parent-child relationships and links.&lt;/p>
&lt;p>Interoperability across HTTP hops is a standards problem, not a vendor feature. W3C Trace Context’s &lt;code>traceparent&lt;/code> field lets a downstream service continue the same trace. If a hop drops that header, the graph breaks even if every service is “instrumented.”&lt;/p>
&lt;p>Sampling is the cost trade-off. Retaining every span for every request is often unaffordable. Sampling reduces storage and export load; the risk is dropping the traces you later need, especially rare failures whose outcome is not known when the sampling decision is made. How aggressively you sample depends on traffic, budget, and how bad a missing trace is for your incident process. It is not a universal setting.&lt;/p>
&lt;h2 id="3-correlation-requires-more-than-one-identifier">3. Correlation requires more than one identifier
&lt;/h2>&lt;p>Distributed systems usually carry several identifiers with different jobs:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Identifier&lt;/th>
&lt;th>Question answered&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Business ID&lt;/td>
&lt;td>Which customer transaction is this?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Trace ID&lt;/td>
&lt;td>Which distributed execution is this?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Span ID&lt;/td>
&lt;td>Which individual operation is this?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Message ID&lt;/td>
&lt;td>Which transport delivery is this?&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>External request ID&lt;/td>
&lt;td>Which operation does the external system recognize?&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>These are not interchangeable.&lt;/p>
&lt;p>A business transaction can produce several traces over its lifetime: initial submission, automatic retry, manual intervention, later reconciliation. The business identifier joins that history. Each trace describes one execution. The external request ID is what you send when you ask the remote system what it actually did.&lt;/p>
&lt;p>A useful design persists durable business and external identifiers, propagates trace context between services, and writes the relevant IDs together on structured logs so an engineer can move from a customer complaint to a trace to a specific event. In payment backends the same idea shows up as STAN/RRN correlation across hops; the identifiers differ, the joining problem does not.&lt;/p>
&lt;h2 id="4-observability-across-asynchronous-boundaries">4. Observability across asynchronous boundaries
&lt;/h2>&lt;p>Synchronous propagation is a header on the RPC:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Service A ── HTTP trace context ──► Service B
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Asynchronous communication inserts a break in time and in process:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>API ──► Message broker ── waits ──► Background worker
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The original request may have completed before the worker starts. To preserve causality, the producer injects trace context into message metadata and the consumer extracts it.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Producer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── Create producer span
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── Attach trace context to message metadata
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └── Publish message
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Queue delay
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Consumer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── Extract trace context
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├── Create consumer or processing span
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └── Record message and delivery attributes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Measure queue delay separately from processing duration. Otherwise a transaction that waited ten minutes and then ran for one second looks fast.&lt;/p>
&lt;p>Retries, redeliveries, batches, and fan-out often do not fit a single parent. OpenTelemetry’s messaging conventions default to span links for that reason: a span can have only one parent, a batch can originate in many traces, and a consumer may already have an ambient parent (for example an HTTP handler that then pulls a queue). Links record causality without pretending the consumer ran inside the producer. The waterfall becomes less pretty. The graph stays honest.&lt;/p>
&lt;p>If you skip injection, you do not have one distributed trace. You have two unconnected executions and a timestamp coincidence.&lt;/p>
&lt;h2 id="5-business-state-observability">5. Business-state observability
&lt;/h2>&lt;p>Technical telemetry becomes useful for correctness when it is joined to a durable state machine.&lt;/p>
&lt;p>A simplified asynchronous lifecycle:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>REQUESTED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>QUEUED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>PROCESSING
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>EXTERNAL_REQUESTED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├────────► COMPLETED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├────────► FAILED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └────────► UNKNOWN_EXTERNAL_STATE
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Every transition should leave evidence:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;event&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;transaction.state_transition&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;transactionId&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;TX-18472&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;from&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;EXTERNAL_REQUESTED&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;to&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;UNKNOWN_EXTERNAL_STATE&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;reason&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;response_timeout&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;attempt&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;recordVersion&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">7&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;calculationVersion&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;rules-2026-08&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;externalRequestId&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;EXT-94721&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;traceId&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;4bf92f3577b34da6a3ce929d0e0e4736&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That record lets you find transactions stuck in &lt;code>PROCESSING&lt;/code>, illegal or regressive transitions, retry storms, optimistic-locking conflicts, a growing unknown population, results produced by an unexpected calculation version, and disagreements between local and external state.&lt;/p>
&lt;p>&lt;code>recordVersion&lt;/code> and &lt;code>calculationVersion&lt;/code> are not telemetry decoration. Concurrent workers racing on the same business ID need a version to detect lost updates. Disputed outcomes need to know which rules produced the number. Those fields are cheap to store on the transition event and expensive to reconstruct later from logs.&lt;/p>
&lt;p>This is the distinction the rest of the post depends on: component availability is not the same as “business operations are reaching a known, correct terminal state.”&lt;/p>
&lt;h2 id="6-ambiguous-outcomes-a-timeout-is-not-a-failure">6. Ambiguous outcomes: a timeout is not a failure
&lt;/h2>&lt;p>One of the most expensive assumptions in distributed processing is that a timeout means the operation failed.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Local worker External system
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │ │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ├──── Perform operation ──────►│
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │ │ Operation succeeds
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │◄──── Success response ───────X Connection interrupted
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └──── Observes timeout
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The local system did not receive confirmation. The external effect may already have occurred. Recording the transaction as failed and retrying it blindly can duplicate that effect. In card payments this is the double-charge path: a lost authorization response followed by a retry with a new identity. The same shape appears in refunds, payouts, provisioning calls, and any other remote mutation.&lt;/p>
&lt;p>A more accurate local state is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>UNKNOWN_EXTERNAL_STATE
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That state does not mean the system is broken. It means the evidence is insufficient to assert success or failure.&lt;/p>
&lt;p>Managing it safely takes four capabilities that do different jobs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Observability
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Idempotency
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Explicit state
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Reconciliation
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Observability records what was attempted and why the result is ambiguous. Idempotency makes a repeated request collapse onto the original effect, provided the remote system honours the same key. The state machine refuses to pretend it knows. Reconciliation later compares local state with external truth.&lt;/p>
&lt;p>None of this makes the distributed operation atomic. Two-phase commit is a different design, with its own availability cost, and most external APIs will not join your transaction. The combination above is an eventual-consistency recovery path. It needs a queryable remote status, or an equivalent (webhooks plus idempotent application, statements, clearing files). If the vendor cannot tell you what happened, you cannot reconcile; you can only guess more slowly.&lt;/p>
&lt;p>The payment-specific version of this argument is in &lt;a class="link" href="https://corebaseit.com/corebaseit_posts/double-charging/" >double charging&lt;/a>. The architectural point is general: an error is often a loss of visibility, not a result.&lt;/p>
&lt;h2 id="7-reliability-should-be-expressed-as-an-outcome">7. Reliability should be expressed as an outcome
&lt;/h2>&lt;p>Reliability is easier to argue about when it is stated in terms a user or a business owner can recognize.&lt;/p>
&lt;p>Google’s SRE book keeps three terms distinct:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Service Level Indicator (SLI):&lt;/strong> a quantitative measure of the service level provided.&lt;/li>
&lt;li>&lt;strong>Service Level Objective (SLO):&lt;/strong> an internal target for that measure.&lt;/li>
&lt;li>&lt;strong>Service Level Agreement (SLA):&lt;/strong> a contractual commitment, usually with consequences.&lt;/li>
&lt;/ul>
&lt;p>An outcome-oriented SLI for asynchronous work might be:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Transactions reaching a known terminal state within five minutes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>─────────────────────────────────────────────────────────────────
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Eligible transaction requests
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>An associated SLO, as an example rather than a recommendation:&lt;/p>
&lt;blockquote>
&lt;p>At least 99.0% of eligible transactions will reach a known terminal state within five minutes over a rolling 28-day period.&lt;/p>&lt;/blockquote>
&lt;p>CPU below a threshold is not that statement. Infrastructure metrics help explain an SLO miss. They are not the customer outcome. The SRE SLO chapter and the OpenTelemetry primer both push in this direction: measure whether the service is doing what users expect, not whether a box is up.&lt;/p>
&lt;p>A complete SLO definition has to specify the eligible population, the event that starts the clock, what counts as a good outcome, the allowed duration, the evaluation window, exclusions, and how ambiguous outcomes are classified.&lt;/p>
&lt;p>That last item is a real trade-off, not paperwork. If &lt;code>UNKNOWN_EXTERNAL_STATE&lt;/code> counts as success, the SLO hides the population that can duplicate effects. If it counts as failure, a slow but correct reconciliation burns error budget even when money or state eventually line up. A third option is to measure two SLIs: time to a &lt;em>known&lt;/em> terminal state, and the residual unknown rate. The numbers you choose depend on how costly a duplicate is versus how costly a delayed confirmation is. There is no default that fits every product.&lt;/p>
&lt;p>Error budgets follow from the SLO. They are a decision tool for how much change risk you can take, not a badge. If you do not have an SLO, you still have an implicit reliability target. It just is not written down, so arguments about “is this too risky to ship?” have no shared unit.&lt;/p>
&lt;h2 id="8-alert-on-symptoms-that-require-action">8. Alert on symptoms that require action
&lt;/h2>&lt;p>An alert should mean a human needs to act. It should not mean that something unusual occurred.&lt;/p>
&lt;p>Google’s monitoring chapter is blunt: paging is expensive; pages should be urgent, actionable, and tied to user-visible (or imminently user-visible) harm; chasing causes instead of symptoms produces noise. Latency, traffic, errors, and saturation remain useful diagnostic signals. They are not automatically paging signals.&lt;/p>
&lt;p>Symptoms that often deserve a page in this design:&lt;/p>
&lt;ul>
&lt;li>Rapid consumption of the SLO error budget&lt;/li>
&lt;li>Transactions remaining non-terminal beyond the expected duration&lt;/li>
&lt;li>A sustained rise in unknown external outcomes&lt;/li>
&lt;li>Material growth in the age of queued work&lt;/li>
&lt;li>Reconciliation failing to close discrepancies&lt;/li>
&lt;li>Evidence that idempotency is no longer collapsing duplicates&lt;/li>
&lt;/ul>
&lt;p>A brief CPU spike can be useful on a dashboard. It should not wake someone if customers are unaffected. Saturation is the usual exception: it is a leading indicator, and paging on “the queue’s oldest message is now older than the SLO window” is paging on a symptom of stalled work, not on a kernel metric.&lt;/p>
&lt;p>A useful alert carries enough context to start work. Illustrative, not a recorded incident:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Impact: 6.8% of transactions missed the five-minute objective
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Scope: one operation category in one region
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Related evidence: queue age rising; external timeouts elevated
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Links: dashboard, representative traces, runbook, recent changes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Action: verify processing progress and unknown-state accumulation
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Threshold-only alerts (“error rate &amp;gt; 2%”) skip impact, scope, and the next action. They also tend to fire on traffic mix changes that are not failures.&lt;/p>
&lt;h2 id="9-the-operational-investigation-path">9. The operational investigation path
&lt;/h2>&lt;p>Incident work should move from aggregate impact toward an authoritative record:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Metrics
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │ Define scope and customer impact
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Traces
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │ Identify the slow or failing stage
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Structured logs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │ Explain decisions, errors and retries
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Business-state history
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> │ Establish what the platform believes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ▼
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>External evidence and reconciliation
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Establish the actual outcome
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A typical sequence:&lt;/p>
&lt;ol>
&lt;li>Confirm the customer-facing symptom and the affected scope.&lt;/li>
&lt;li>Look at queue progress, processing latency, and external dependencies.&lt;/li>
&lt;li>Select representative healthy and unhealthy traces.&lt;/li>
&lt;li>Identify the stage responsible for delay or failure.&lt;/li>
&lt;li>Read correlated logs for those spans.&lt;/li>
&lt;li>Review the durable state-transition history.&lt;/li>
&lt;li>Resolve ambiguous outcomes against external evidence.&lt;/li>
&lt;li>Confirm that the SLI recovers and the backlog drains.&lt;/li>
&lt;li>Record instrumentation gaps.&lt;/li>
&lt;/ol>
&lt;p>Timestamp alignment across hosts is not causality. Traces, deployment markers, state history, and (when needed) a controlled reproduction should be combined before you name a root cause. A deploy that coincides with a latency spike is a hypothesis. The trace that shows the new code path, or the state history that shows illegal transitions starting at that deploy, is closer to evidence.&lt;/p>
&lt;h2 id="10-a-practical-implementation-sequence">10. A practical implementation sequence
&lt;/h2>&lt;p>This is a suggested order for a team that does not yet have the above, not a maturity model and not a requirement to finish one phase before emitting any telemetry. Waiting for a perfect schema delays evidence; shipping unstructured logs first creates a cleanup bill. Pick a bias and write it down.&lt;/p>
&lt;p>&lt;strong>Phase 1: Semantic consistency.&lt;/strong> Canonical event names, field names, outcome classifications, identifiers, and units. The backend (vendor A versus vendor B) matters less than whether two services call the same failure two different things.&lt;/p>
&lt;p>&lt;strong>Phase 2: One critical journey.&lt;/strong> Trace one important business operation from entry to terminal state, including database access, publish, queue delay, worker processing, and external calls. Prove propagation across the async boundary before you instrument everything.&lt;/p>
&lt;p>&lt;strong>Phase 3: Business-state telemetry.&lt;/strong> State populations, state age, transition rates, retries, duplicate suppression, unknown outcomes.&lt;/p>
&lt;p>&lt;strong>Phase 4: Reliability objectives.&lt;/strong> SLIs and SLOs from customer outcomes. Write down eligibility, success, clock start, window, and the unknown-state rule.&lt;/p>
&lt;p>&lt;strong>Phase 5: Investigation workflow.&lt;/strong> Dashboard panels to traces, traces to logs, telemetry to durable state records. If the join requires a hero query, it will not happen at 3 a.m.&lt;/p>
&lt;p>&lt;strong>Phase 6: Failure and recovery drills.&lt;/strong> Duplicate delivery, worker crash mid-flight, concurrent processors, external timeout, lost response. Verify both the correctness mechanism and the evidence it leaves. If idempotency works but leaves no trace, the next incident will look like a mystery.&lt;/p>
&lt;p>&lt;strong>Phase 7: Cost and privacy.&lt;/strong> Budgets for metric cardinality, log volume, trace sampling, and retention. Test redaction. Watch the telemetry pipeline itself for dropped spans, broken propagation, and silent discard. An unobservable observability stack is an easy way to lose the only copy of the truth.&lt;/p>
&lt;p>How far you take each phase depends on scale, budget, and how expensive an unexplainable transaction is. A low-rate internal tool and a payment mutation path should not get the same spend.&lt;/p>
&lt;h2 id="closing">Closing
&lt;/h2>&lt;p>Logs, metrics, and traces are different views of technical behaviour. Their value for correctness appears when they are correlated with business identities, lifecycle state, and a recovery path that can tolerate not knowing.&lt;/p>
&lt;p>For systems that mutate state outside their own process, the design rule is intellectual honesty: distinguish what the system knows from what it is assuming. A timeout does not prove failure. A successful API response does not prove the business operation completed. A healthy server does not prove a healthy transaction flow.&lt;/p>
&lt;p>The objective is therefore broader than operational visibility:&lt;/p>
&lt;blockquote>
&lt;p>Every important transaction should be explainable, its correctness should be verifiable, and an ambiguous outcome should be recoverable.&lt;/p>&lt;/blockquote>
&lt;p>That is the gap between collecting telemetry and engineering a system you can actually investigate.&lt;/p>
&lt;h2 id="references">References
&lt;/h2>&lt;ol>
&lt;li>OpenTelemetry, &lt;a class="link" href="https://opentelemetry.io/docs/concepts/observability-primer/" target="_blank" rel="noopener"
>“Observability Primer”&lt;/a>.&lt;/li>
&lt;li>OpenTelemetry, &lt;a class="link" href="https://opentelemetry.io/docs/concepts/signals/" target="_blank" rel="noopener"
>“Signals”&lt;/a>.&lt;/li>
&lt;li>OpenTelemetry, &lt;a class="link" href="https://opentelemetry.io/docs/concepts/signals/traces/" target="_blank" rel="noopener"
>“Traces”&lt;/a> (span links; producer and consumer kinds).&lt;/li>
&lt;li>OpenTelemetry Semantic Conventions, &lt;a class="link" href="https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/" target="_blank" rel="noopener"
>“Semantic Conventions for Messaging”&lt;/a>.&lt;/li>
&lt;li>World Wide Web Consortium, &lt;a class="link" href="https://www.w3.org/TR/trace-context/" target="_blank" rel="noopener"
>“Trace Context”&lt;/a>.&lt;/li>
&lt;li>Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy (eds.), &lt;em>Site Reliability Engineering&lt;/em> (O’Reilly, 2016), &lt;a class="link" href="https://sre.google/sre-book/service-level-objectives/" target="_blank" rel="noopener"
>“Service Level Objectives”&lt;/a> and &lt;a class="link" href="https://sre.google/sre-book/monitoring-distributed-systems/" target="_blank" rel="noopener"
>“Monitoring Distributed Systems”&lt;/a>.&lt;/li>
&lt;li>Prometheus, &lt;a class="link" href="https://prometheus.io/docs/practices/instrumentation/#do-not-overuse-labels" target="_blank" rel="noopener"
>“Instrumentation”&lt;/a> (label cardinality).&lt;/li>
&lt;/ol></description></item></channel></rss>