<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anirudh Rajmohan</title>
    <description>The latest articles on DEV Community by Anirudh Rajmohan (@anirudh_rajmohan).</description>
    <link>https://dev.to/anirudh_rajmohan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4040634%2F65b0e576-b337-41c4-b193-2e50de7ddfe0.jpg</url>
      <title>DEV Community: Anirudh Rajmohan</title>
      <link>https://dev.to/anirudh_rajmohan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anirudh_rajmohan"/>
    <language>en</language>
    <item>
      <title>How Old Is My Data? The Missing OpenTelemetry Signal</title>
      <dc:creator>Anirudh Rajmohan</dc:creator>
      <pubDate>Wed, 22 Jul 2026 21:14:54 +0000</pubDate>
      <link>https://dev.to/anirudh_rajmohan/how-old-is-my-data-the-missing-opentelemetry-signal-4dj8</link>
      <guid>https://dev.to/anirudh_rajmohan/how-old-is-my-data-the-missing-opentelemetry-signal-4dj8</guid>
      <description>&lt;p&gt;&lt;em&gt;A proposal — and a working reference implementation — for making data freshness a portable, first-class telemetry signal.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode nobody's dashboard catches
&lt;/h2&gt;

&lt;p&gt;Picture a data pipeline at 3 a.m. Every service is green. Latency is low, error rates are zero, throughput is normal. Every SLO is met.&lt;/p&gt;

&lt;p&gt;And the executive dashboard it feeds is quietly &lt;strong&gt;six hours out of date&lt;/strong&gt;, because one upstream producer stalled and nothing downstream noticed. There was no error to fire on. No latency spike. The data just… stopped being fresh.&lt;/p&gt;

&lt;p&gt;This is one of the most common and most invisible failure modes in modern data systems, and it has a precise name from networked-systems research: the &lt;strong&gt;Age of Information&lt;/strong&gt; (AoI) — how much time has elapsed since the freshest piece of data was generated at its source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;age = now − event_time(freshest record)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a source stops producing, its &lt;code&gt;age&lt;/code&gt; climbs without bound while every other signal stays perfectly healthy. That's exactly the condition freshness monitoring exists to catch — and it's exactly the signal our observability stacks don't have a standard way to express.&lt;/p&gt;

&lt;h2&gt;
  
  
  We standardized everything except freshness
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry has become the de facto standard for telemetry. It gives us portable conventions for latency, errors, throughput, database calls, and messaging. You can move a dashboard or an alert between backends and vendors because the &lt;em&gt;names&lt;/em&gt; are standardized.&lt;/p&gt;

&lt;p&gt;But there is &lt;strong&gt;no&lt;/strong&gt; OpenTelemetry semantic convention for data freshness. &lt;code&gt;db.*&lt;/code&gt; and &lt;code&gt;messaging.*&lt;/code&gt; describe &lt;em&gt;operations&lt;/em&gt; — how long a query took, how many messages were published — not how &lt;em&gt;old the data itself is&lt;/em&gt;. There isn't even a standardized consumer-lag metric.&lt;/p&gt;

&lt;p&gt;So today, every team reinvents freshness in a silo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data-observability vendors (Monte Carlo, Metaplane, Sifflet, …) each compute it internally, incompatibly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dbt source freshness&lt;/code&gt; computes it for dbt.&lt;/li&gt;
&lt;li&gt;Kafka-lag tools (Burrow) compute it for Kafka.&lt;/li&gt;
&lt;li&gt;Everyone else writes a bespoke &lt;code&gt;SELECT MAX(updated_at)&lt;/code&gt; and a Grafana panel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it is comparable across systems or portable across backends. Freshness is a well-understood quantity trapped in a dozen proprietary shapes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proposal: &lt;code&gt;data.staleness.*&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I've been working on a small, vendor-neutral semantic convention for data staleness / freshness, built on OpenTelemetry, plus a complete reference implementation. &lt;strong&gt;To be clear about status: this is an independent, community project — not an official OpenTelemetry project, and not endorsed by or affiliated with the OpenTelemetry project or the CNCF. The convention is a &lt;em&gt;proposal&lt;/em&gt;, at OpenTelemetry "Development" stability, currently under discussion with the Semantic Conventions SIG (&lt;a href="https://github.com/open-telemetry/semantic-conventions/issues/3909" rel="noopener noreferrer"&gt;issue #3909&lt;/a&gt;). It is not (yet) an accepted standard, and names may change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The shape is deliberately compact — a handful of metrics grounded in AoI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data.staleness.age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;now − event_time&lt;/code&gt; of the freshest record (the primary signal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data.staleness.lag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;processing_time − event_time&lt;/code&gt; of the most recent record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data.staleness.last_update.timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unix time of the last successful update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data.staleness.records.behind&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;positional backlog (e.g. Kafka consumer lag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data.staleness.sla.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;configured threshold + breach evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;…with attributes like &lt;code&gt;data.source.system&lt;/code&gt;, &lt;code&gt;data.source.name&lt;/code&gt;, &lt;code&gt;data.staleness.method&lt;/code&gt;, and &lt;code&gt;data.staleness.partition&lt;/code&gt; — orthogonal to, and composable with, the existing &lt;code&gt;db.*&lt;/code&gt; and &lt;code&gt;messaging.*&lt;/code&gt; conventions.&lt;/p&gt;

&lt;p&gt;The key insight is &lt;code&gt;age&lt;/code&gt; vs &lt;code&gt;lag&lt;/code&gt;: if a source stops producing, the last record's &lt;code&gt;lag&lt;/code&gt; stays constant while &lt;code&gt;age&lt;/code&gt; keeps rising. That divergence is the alert you actually want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle: never fabricate a number
&lt;/h2&gt;

&lt;p&gt;One rule runs through the whole implementation: &lt;strong&gt;an unmeasurable freshness value must be &lt;em&gt;visible&lt;/em&gt;, never faked.&lt;/strong&gt; An empty table, a &lt;code&gt;NULL MAX()&lt;/code&gt;, a timeout, an empty Kafka partition — each surfaces as &lt;code&gt;data.staleness.probe.errors&lt;/code&gt; with a specific &lt;code&gt;error.type&lt;/code&gt;, not as a fabricated "0 seconds old." A broken freshness check that silently reports "fresh" is worse than no check at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in two minutes
&lt;/h2&gt;

&lt;p&gt;There are two ways to adopt it — a Python SDK for code you own, and a zero-code Collector for infrastructure you don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python SDK&lt;/strong&gt; — you give it a probe that returns the freshest event time; it emits the standardized metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;data-staleness-otel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;otel_staleness&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StalenessMonitor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conventions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;otel_staleness.probes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SQLFreshnessProbe&lt;/span&gt;

&lt;span class="n"&gt;monitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StalenessMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SQLFreshnessProbe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;fetch_max_epoch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;latest_updated_at&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;# Unix seconds or datetime
&lt;/span&gt;    &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POSTGRESQL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sla_threshold_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Zero-code Collector&lt;/strong&gt; — a custom OpenTelemetry Collector receiver scrapes SQL, Kafka, Kinesis, files, HTTP, schema registries, and DB-migration versions directly from YAML, with no application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;datastaleness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;collection_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
    &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sql&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
        &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql&lt;/span&gt;
        &lt;span class="na"&gt;dsn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://user:pass@db:5432/app?sslmode=disable"&lt;/span&gt;
        &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
        &lt;span class="na"&gt;timestamp_column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;updated_at&lt;/span&gt;
        &lt;span class="na"&gt;sla_threshold_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both emit the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;data.staleness.*&lt;/code&gt; metrics into the same backend, so they interoperate.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's more than a spec
&lt;/h2&gt;

&lt;p&gt;Because a convention is only credible if it's real and checkable, the project ships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A written spec plus a &lt;strong&gt;machine-readable Weaver model&lt;/strong&gt; that passes &lt;code&gt;weaver registry check&lt;/code&gt; — the same tool the OpenTelemetry SIG uses to validate and generate conventions.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;language-agnostic conformance suite&lt;/strong&gt; so the numbers aren't one library's opinion.&lt;/li&gt;
&lt;li&gt;A two-language &lt;strong&gt;reference implementation&lt;/strong&gt; (Python SDK + Go Collector receiver &amp;amp; processor), &lt;strong&gt;validated end-to-end on real backends&lt;/strong&gt; — a suite stands up real Postgres, Kafka, Redis, LocalStack Kinesis, and a schema registry and asserts the emitted metrics are &lt;em&gt;numerically correct&lt;/em&gt; (age equal to a known injected offset, exact consumer lag), and that a stopped source surfaces &lt;code&gt;probe.errors&lt;/code&gt; rather than a fabricated value.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why standardize instead of ship-another-library?
&lt;/h2&gt;

&lt;p&gt;Because the value is portability. If freshness has one vocabulary, then a freshness alert, a Grafana panel, or an SLO travels across Postgres, Kafka, dbt, a vector index, and a replica — and across Prometheus, an OTLP store, or any vendor backend — unchanged. This isn't a new metric; it's the &lt;strong&gt;consolidation&lt;/strong&gt; of a well-studied one (AoI, and what every data-observability tool already computes) into the framework that already standardizes latency and errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get involved
&lt;/h2&gt;

&lt;p&gt;If "how old is my data?" is a question you've had to answer with a bespoke query and a fragile dashboard, I'd love your input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The convention proposal is &lt;a href="https://github.com/open-telemetry/semantic-conventions/issues/3909" rel="noopener noreferrer"&gt;open in the OpenTelemetry Semantic Conventions repo (#3909)&lt;/a&gt;&lt;/strong&gt; — thumbs-up, comments, and use cases genuinely help gauge demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The code, spec, model, and validation live here:&lt;/strong&gt; &lt;a href="https://github.com/anirudhrajreliability/otel-data-staleness" rel="noopener noreferrer"&gt;https://github.com/anirudhrajreliability/otel-data-staleness&lt;/a&gt; (Apache-2.0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try it&lt;/strong&gt; (&lt;code&gt;pip install data-staleness-otel&lt;/code&gt;), tell me where it breaks, and — if this is your problem space — consider co-owning the effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Freshness is a known quantity that, paradoxically, has no standard way to be &lt;em&gt;emitted&lt;/em&gt;. Let's fix that.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>dataengineering</category>
      <category>python</category>
    </item>
  </channel>
</rss>
