data-staleness-otel 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. data_staleness_otel-0.4.0/PKG-INFO +211 -0
  2. data_staleness_otel-0.4.0/README.md +177 -0
  3. data_staleness_otel-0.4.0/pyproject.toml +49 -0
  4. data_staleness_otel-0.4.0/setup.cfg +4 -0
  5. data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/PKG-INFO +211 -0
  6. data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/SOURCES.txt +30 -0
  7. data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/dependency_links.txt +1 -0
  8. data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/requires.txt +20 -0
  9. data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/top_level.txt +1 -0
  10. data_staleness_otel-0.4.0/src/otel_staleness/__init__.py +23 -0
  11. data_staleness_otel-0.4.0/src/otel_staleness/_timeutil.py +44 -0
  12. data_staleness_otel-0.4.0/src/otel_staleness/conventions.py +102 -0
  13. data_staleness_otel-0.4.0/src/otel_staleness/core.py +351 -0
  14. data_staleness_otel-0.4.0/src/otel_staleness/freshness_context.py +129 -0
  15. data_staleness_otel-0.4.0/src/otel_staleness/integrations/__init__.py +3 -0
  16. data_staleness_otel-0.4.0/src/otel_staleness/integrations/dbt.py +106 -0
  17. data_staleness_otel-0.4.0/src/otel_staleness/probes/__init__.py +18 -0
  18. data_staleness_otel-0.4.0/src/otel_staleness/probes/cache.py +76 -0
  19. data_staleness_otel-0.4.0/src/otel_staleness/probes/differential.py +123 -0
  20. data_staleness_otel-0.4.0/src/otel_staleness/probes/kafka.py +66 -0
  21. data_staleness_otel-0.4.0/src/otel_staleness/probes/pipeline.py +44 -0
  22. data_staleness_otel-0.4.0/src/otel_staleness/probes/sql.py +75 -0
  23. data_staleness_otel-0.4.0/src/otel_staleness/probes/version.py +245 -0
  24. data_staleness_otel-0.4.0/src/otel_staleness/py.typed +0 -0
  25. data_staleness_otel-0.4.0/src/otel_staleness/version_extract.py +87 -0
  26. data_staleness_otel-0.4.0/tests/test_core.py +131 -0
  27. data_staleness_otel-0.4.0/tests/test_dbt.py +71 -0
  28. data_staleness_otel-0.4.0/tests/test_extension_metrics.py +140 -0
  29. data_staleness_otel-0.4.0/tests/test_freshness_context.py +77 -0
  30. data_staleness_otel-0.4.0/tests/test_probes.py +136 -0
  31. data_staleness_otel-0.4.0/tests/test_version.py +98 -0
  32. data_staleness_otel-0.4.0/tests/test_version_extract.py +64 -0
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-staleness-otel
3
+ Version: 0.4.0
4
+ Summary: Vendor-neutral data-staleness / data-freshness instrumentation built on OpenTelemetry (community project; not an official OpenTelemetry package)
5
+ Author: Anirudh Rajmohan
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/anirudhrajreliability/otel-data-staleness
8
+ Project-URL: Repository, https://github.com/anirudhrajreliability/otel-data-staleness
9
+ Project-URL: Issues, https://github.com/anirudhrajreliability/otel-data-staleness/issues
10
+ Project-URL: Specification, https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/spec/semantic-conventions.md
11
+ Keywords: opentelemetry,observability,data-freshness,data-staleness,age-of-information
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Monitoring
17
+ Classifier: Framework :: OpenTelemetry
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: opentelemetry-api>=1.20.0
21
+ Requires-Dist: opentelemetry-sdk>=1.20.0
22
+ Provides-Extra: sql
23
+ Requires-Dist: sqlalchemy>=1.4; extra == "sql"
24
+ Provides-Extra: kafka
25
+ Requires-Dist: confluent-kafka>=2.0; extra == "kafka"
26
+ Provides-Extra: cache
27
+ Requires-Dist: redis>=4.0; extra == "cache"
28
+ Provides-Extra: version
29
+ Requires-Dist: packaging>=21.0; extra == "version"
30
+ Provides-Extra: otlp
31
+ Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == "otlp"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+
35
+ > Part of **otel-data-staleness** — see the [root README](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/README.md) for the project overview and the SDK-vs-Collector comparison.
36
+
37
+ # data-staleness-otel (Python SDK)
38
+
39
+ > **Disclaimer:** This is an independent, community project. It is **not** an
40
+ > official OpenTelemetry project and is **not** endorsed by or affiliated with
41
+ > the OpenTelemetry project or the CNCF. It is *built on* OpenTelemetry; the
42
+ > `data.staleness.*` semantic convention is a **proposal** under discussion with
43
+ > the Semantic Conventions SIG ([#3909](https://github.com/open-telemetry/semantic-conventions/issues/3909)),
44
+ > at "Development" stability — names may change and it is not an accepted standard.
45
+
46
+ Vendor-neutral instrumentation for **data staleness / data freshness**, built on
47
+ OpenTelemetry, implementing the conventions in
48
+ [`../spec/semantic-conventions.md`](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/spec/semantic-conventions.md).
49
+
50
+ You give the SDK *probes* that report the event time of the freshest record for
51
+ a logical source; it emits the standardized `data.staleness.*` metrics
52
+ (age, lag, last-update timestamp, records-behind, SLA threshold/breach) via the
53
+ normal OpenTelemetry metrics pipeline (OTLP, Prometheus, console, etc.).
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install data-staleness-otel # core (imported as `otel_staleness`)
59
+ pip install "data-staleness-otel[sql,kafka,cache]" # with optional integrations
60
+ ```
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ from opentelemetry import metrics
66
+ from opentelemetry.sdk.metrics import MeterProvider
67
+ from opentelemetry.sdk.metrics.export import (
68
+ ConsoleMetricExporter, PeriodicExportingMetricReader)
69
+
70
+ from otel_staleness import StalenessMonitor, conventions as sc
71
+ from otel_staleness.probes import SQLFreshnessProbe
72
+
73
+ reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
74
+ metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
75
+
76
+ monitor = StalenessMonitor()
77
+ monitor.add_probe(SQLFreshnessProbe(
78
+ fetch_max_epoch=lambda: get_latest_updated_at(), # returns Unix seconds or datetime
79
+ source_name="orders", system=sc.System.POSTGRESQL,
80
+ namespace="public", sla_threshold_seconds=300))
81
+ monitor.start()
82
+ ```
83
+
84
+ Run the bundled demo (emits all four source types to the console):
85
+
86
+ ```bash
87
+ python examples/quickstart.py
88
+ ```
89
+
90
+ ## Probes
91
+
92
+ | Probe | Source | Method emitted |
93
+ |-------|--------|----------------|
94
+ | `SQLFreshnessProbe` | Postgres/MySQL/Snowflake/Redshift/BigQuery | `max_timestamp` |
95
+ | `KafkaFreshnessProbe` | Kafka (per partition) | `consumer_lag` |
96
+ | `PipelineFreshnessProbe` | dbt/Airflow batch jobs | `run_completion` |
97
+ | `CacheFreshnessProbe` | Redis | `ttl_age` |
98
+ | `ObjectStoreFreshnessProbe` | S3/GCS | `object_mtime` |
99
+ | `IndexFreshnessProbe` | Search/vector indexes (Elasticsearch, OpenSearch, Pinecone, Weaviate, Milvus, Qdrant, pgvector) | `index_lag` |
100
+ | `ReplicationFreshnessProbe` | Replicas / CDC targets / multi-region (Postgres replicas, Debezium, DynamoDB global tables) | `replication_lag` |
101
+ | `VersionFreshnessProbe` | RAG / docs version-currency vs a registry (PyPI, npm, GitHub, Docker Hub) | `version_drift` |
102
+
103
+ The first five emit **absolute** freshness (`age` vs `now`).
104
+ `IndexFreshnessProbe` / `ReplicationFreshnessProbe` emit **differential**
105
+ (source-relative) freshness — how far a derived/replicated store trails its
106
+ upstream — via `data.staleness.lag` plus a `data.staleness.relative_to`
107
+ attribute. `VersionFreshnessProbe` emits **version-currency** (how many releases
108
+ behind). Example of the differential form:
109
+
110
+ ```python
111
+ from otel_staleness.probes import IndexFreshnessProbe
112
+ # how far the vector index trails its source corpus
113
+ monitor.add_probe(IndexFreshnessProbe(
114
+ fetch_source_epoch=lambda: newest_source_write_time(),
115
+ fetch_index_epoch=lambda: newest_source_time_present_in_index(),
116
+ index_name="docs-v1", source="public.documents",
117
+ system=sc.System.PINECONE, sla_threshold_seconds=60))
118
+ ```
119
+
120
+ **Version-currency for RAG / documentation** — is your indexed doc describing
121
+ the *current* release? Compares the documented version against a registry:
122
+
123
+ ```python
124
+ from otel_staleness.probes import VersionFreshnessProbe
125
+ # your RAG corpus documents Kubernetes 1.28; how far behind is it?
126
+ monitor.add_probe(VersionFreshnessProbe.from_github_releases(
127
+ "kubernetes/kubernetes", documented_version="1.28.0",
128
+ source_name="k8s-docs", sla_threshold_seconds=60*60*24*14)) # 14 days
129
+ ```
130
+
131
+ Emits `records.behind` (releases behind), `age` (how long newer releases have
132
+ existed unreflected), and `version.documented` / `version.current` attributes.
133
+ Helpers: `from_pypi`, `from_npm`, `from_github_releases`, `from_dockerhub`
134
+ (needs the `version` extra: `pip install -e ".[version]"`). The comparison logic
135
+ is pure and unit-tested; unparseable versions raise rather than fabricate a `0`.
136
+
137
+ The **documented version** rarely needs hard-coding — extract it from the
138
+ content with `otel_staleness.version_extract` (frontmatter, a semver token, a
139
+ JSON field, an HTML `<meta>`, or a docs URL path), composed with `first_of`:
140
+
141
+ ```python
142
+ from otel_staleness.version_extract import first_of, extract_frontmatter_version, extract_url_path_version
143
+ documented = first_of(
144
+ lambda: extract_frontmatter_version(open("docs/networking.md").read()),
145
+ lambda: extract_url_path_version("https://kubernetes.io/docs/v1.28/"),
146
+ ) # pass as documented_version=...
147
+ ```
148
+
149
+ See `examples/rag_freshness.py` for the combined RAG check: index time-lag
150
+ (`IndexFreshnessProbe`) **and** version currency (`VersionFreshnessProbe`) on the
151
+ same corpus — the two failure modes that make RAG answers wrong.
152
+
153
+ Every probe accepts plain callables (returning Unix seconds or a `datetime`),
154
+ so it is fully unit-testable without a live backend. `SQLFreshnessProbe` also
155
+ ships a `from_sqlalchemy(...)` convenience constructor.
156
+
157
+ A failing probe is isolated — it cannot break collection of the others.
158
+
159
+ ## End-to-end freshness across hops
160
+
161
+ Point probes measure freshness *at one place*. To get **true cross-hop age**,
162
+ stamp the origin event-time into OTEL baggage at ingest; any downstream stage
163
+ reads it back (it rides the standard OTEL context propagators across
164
+ service/messaging boundaries):
165
+
166
+ ```python
167
+ from otel_staleness.freshness_context import stamp_origin, EndToEndFreshness
168
+ from opentelemetry import context as otel_context
169
+
170
+ # at ingest:
171
+ otel_context.attach(stamp_origin(event_epoch))
172
+ # ...work flows downstream carrying baggage...
173
+ # at a later stage:
174
+ e2e = EndToEndFreshness() # emits data.staleness.age, method=end_to_end
175
+ e2e.record("orders-pipeline", stage="serve")
176
+ ```
177
+
178
+ ## Every staleness dimension
179
+
180
+ `StalenessMonitor.start()` also emits the extension metrics that capture the
181
+ dimensions a backend cannot reconstruct: `data.staleness.age.peak` (Peak AoI),
182
+ `data.staleness.update.interval` (cadence histogram), `data.staleness.partition.skew`
183
+ (straggler partitions), and `data.staleness.probe.errors` (a failing probe is now
184
+ **visible**, not silently swallowed — pass `add_probe(p, name="...")` to label it).
185
+ See [`../docs/STALENESS-TAXONOMY.md`](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/docs/STALENESS-TAXONOMY.md) for the full
186
+ map of staleness types to mechanisms.
187
+
188
+ ## Custom probes
189
+
190
+ Subclass `StalenessProbe` and return `FreshnessReading` objects:
191
+
192
+ ```python
193
+ from otel_staleness import StalenessProbe, FreshnessReading, conventions as sc
194
+
195
+ class MyProbe(StalenessProbe):
196
+ def read(self):
197
+ return [FreshnessReading(
198
+ source_system="custom", source_name="widget",
199
+ last_update_epoch=get_event_time(), # or age_seconds=...
200
+ sla_threshold_seconds=60)]
201
+ ```
202
+
203
+ ## Tests
204
+
205
+ ```bash
206
+ python -m pytest -q # 54 tests
207
+ PYTHONPATH=src python ../conformance/runner.py # conformance vectors
208
+ ```
209
+
210
+ Requires **Python 3.8+**. Optional extras: `sql`, `kafka`, `cache`, `otlp`,
211
+ `version`, `dev` (e.g. `pip install -e ".[dev,version]"`).
@@ -0,0 +1,177 @@
1
+ > Part of **otel-data-staleness** — see the [root README](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/README.md) for the project overview and the SDK-vs-Collector comparison.
2
+
3
+ # data-staleness-otel (Python SDK)
4
+
5
+ > **Disclaimer:** This is an independent, community project. It is **not** an
6
+ > official OpenTelemetry project and is **not** endorsed by or affiliated with
7
+ > the OpenTelemetry project or the CNCF. It is *built on* OpenTelemetry; the
8
+ > `data.staleness.*` semantic convention is a **proposal** under discussion with
9
+ > the Semantic Conventions SIG ([#3909](https://github.com/open-telemetry/semantic-conventions/issues/3909)),
10
+ > at "Development" stability — names may change and it is not an accepted standard.
11
+
12
+ Vendor-neutral instrumentation for **data staleness / data freshness**, built on
13
+ OpenTelemetry, implementing the conventions in
14
+ [`../spec/semantic-conventions.md`](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/spec/semantic-conventions.md).
15
+
16
+ You give the SDK *probes* that report the event time of the freshest record for
17
+ a logical source; it emits the standardized `data.staleness.*` metrics
18
+ (age, lag, last-update timestamp, records-behind, SLA threshold/breach) via the
19
+ normal OpenTelemetry metrics pipeline (OTLP, Prometheus, console, etc.).
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install data-staleness-otel # core (imported as `otel_staleness`)
25
+ pip install "data-staleness-otel[sql,kafka,cache]" # with optional integrations
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```python
31
+ from opentelemetry import metrics
32
+ from opentelemetry.sdk.metrics import MeterProvider
33
+ from opentelemetry.sdk.metrics.export import (
34
+ ConsoleMetricExporter, PeriodicExportingMetricReader)
35
+
36
+ from otel_staleness import StalenessMonitor, conventions as sc
37
+ from otel_staleness.probes import SQLFreshnessProbe
38
+
39
+ reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
40
+ metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
41
+
42
+ monitor = StalenessMonitor()
43
+ monitor.add_probe(SQLFreshnessProbe(
44
+ fetch_max_epoch=lambda: get_latest_updated_at(), # returns Unix seconds or datetime
45
+ source_name="orders", system=sc.System.POSTGRESQL,
46
+ namespace="public", sla_threshold_seconds=300))
47
+ monitor.start()
48
+ ```
49
+
50
+ Run the bundled demo (emits all four source types to the console):
51
+
52
+ ```bash
53
+ python examples/quickstart.py
54
+ ```
55
+
56
+ ## Probes
57
+
58
+ | Probe | Source | Method emitted |
59
+ |-------|--------|----------------|
60
+ | `SQLFreshnessProbe` | Postgres/MySQL/Snowflake/Redshift/BigQuery | `max_timestamp` |
61
+ | `KafkaFreshnessProbe` | Kafka (per partition) | `consumer_lag` |
62
+ | `PipelineFreshnessProbe` | dbt/Airflow batch jobs | `run_completion` |
63
+ | `CacheFreshnessProbe` | Redis | `ttl_age` |
64
+ | `ObjectStoreFreshnessProbe` | S3/GCS | `object_mtime` |
65
+ | `IndexFreshnessProbe` | Search/vector indexes (Elasticsearch, OpenSearch, Pinecone, Weaviate, Milvus, Qdrant, pgvector) | `index_lag` |
66
+ | `ReplicationFreshnessProbe` | Replicas / CDC targets / multi-region (Postgres replicas, Debezium, DynamoDB global tables) | `replication_lag` |
67
+ | `VersionFreshnessProbe` | RAG / docs version-currency vs a registry (PyPI, npm, GitHub, Docker Hub) | `version_drift` |
68
+
69
+ The first five emit **absolute** freshness (`age` vs `now`).
70
+ `IndexFreshnessProbe` / `ReplicationFreshnessProbe` emit **differential**
71
+ (source-relative) freshness — how far a derived/replicated store trails its
72
+ upstream — via `data.staleness.lag` plus a `data.staleness.relative_to`
73
+ attribute. `VersionFreshnessProbe` emits **version-currency** (how many releases
74
+ behind). Example of the differential form:
75
+
76
+ ```python
77
+ from otel_staleness.probes import IndexFreshnessProbe
78
+ # how far the vector index trails its source corpus
79
+ monitor.add_probe(IndexFreshnessProbe(
80
+ fetch_source_epoch=lambda: newest_source_write_time(),
81
+ fetch_index_epoch=lambda: newest_source_time_present_in_index(),
82
+ index_name="docs-v1", source="public.documents",
83
+ system=sc.System.PINECONE, sla_threshold_seconds=60))
84
+ ```
85
+
86
+ **Version-currency for RAG / documentation** — is your indexed doc describing
87
+ the *current* release? Compares the documented version against a registry:
88
+
89
+ ```python
90
+ from otel_staleness.probes import VersionFreshnessProbe
91
+ # your RAG corpus documents Kubernetes 1.28; how far behind is it?
92
+ monitor.add_probe(VersionFreshnessProbe.from_github_releases(
93
+ "kubernetes/kubernetes", documented_version="1.28.0",
94
+ source_name="k8s-docs", sla_threshold_seconds=60*60*24*14)) # 14 days
95
+ ```
96
+
97
+ Emits `records.behind` (releases behind), `age` (how long newer releases have
98
+ existed unreflected), and `version.documented` / `version.current` attributes.
99
+ Helpers: `from_pypi`, `from_npm`, `from_github_releases`, `from_dockerhub`
100
+ (needs the `version` extra: `pip install -e ".[version]"`). The comparison logic
101
+ is pure and unit-tested; unparseable versions raise rather than fabricate a `0`.
102
+
103
+ The **documented version** rarely needs hard-coding — extract it from the
104
+ content with `otel_staleness.version_extract` (frontmatter, a semver token, a
105
+ JSON field, an HTML `<meta>`, or a docs URL path), composed with `first_of`:
106
+
107
+ ```python
108
+ from otel_staleness.version_extract import first_of, extract_frontmatter_version, extract_url_path_version
109
+ documented = first_of(
110
+ lambda: extract_frontmatter_version(open("docs/networking.md").read()),
111
+ lambda: extract_url_path_version("https://kubernetes.io/docs/v1.28/"),
112
+ ) # pass as documented_version=...
113
+ ```
114
+
115
+ See `examples/rag_freshness.py` for the combined RAG check: index time-lag
116
+ (`IndexFreshnessProbe`) **and** version currency (`VersionFreshnessProbe`) on the
117
+ same corpus — the two failure modes that make RAG answers wrong.
118
+
119
+ Every probe accepts plain callables (returning Unix seconds or a `datetime`),
120
+ so it is fully unit-testable without a live backend. `SQLFreshnessProbe` also
121
+ ships a `from_sqlalchemy(...)` convenience constructor.
122
+
123
+ A failing probe is isolated — it cannot break collection of the others.
124
+
125
+ ## End-to-end freshness across hops
126
+
127
+ Point probes measure freshness *at one place*. To get **true cross-hop age**,
128
+ stamp the origin event-time into OTEL baggage at ingest; any downstream stage
129
+ reads it back (it rides the standard OTEL context propagators across
130
+ service/messaging boundaries):
131
+
132
+ ```python
133
+ from otel_staleness.freshness_context import stamp_origin, EndToEndFreshness
134
+ from opentelemetry import context as otel_context
135
+
136
+ # at ingest:
137
+ otel_context.attach(stamp_origin(event_epoch))
138
+ # ...work flows downstream carrying baggage...
139
+ # at a later stage:
140
+ e2e = EndToEndFreshness() # emits data.staleness.age, method=end_to_end
141
+ e2e.record("orders-pipeline", stage="serve")
142
+ ```
143
+
144
+ ## Every staleness dimension
145
+
146
+ `StalenessMonitor.start()` also emits the extension metrics that capture the
147
+ dimensions a backend cannot reconstruct: `data.staleness.age.peak` (Peak AoI),
148
+ `data.staleness.update.interval` (cadence histogram), `data.staleness.partition.skew`
149
+ (straggler partitions), and `data.staleness.probe.errors` (a failing probe is now
150
+ **visible**, not silently swallowed — pass `add_probe(p, name="...")` to label it).
151
+ See [`../docs/STALENESS-TAXONOMY.md`](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/docs/STALENESS-TAXONOMY.md) for the full
152
+ map of staleness types to mechanisms.
153
+
154
+ ## Custom probes
155
+
156
+ Subclass `StalenessProbe` and return `FreshnessReading` objects:
157
+
158
+ ```python
159
+ from otel_staleness import StalenessProbe, FreshnessReading, conventions as sc
160
+
161
+ class MyProbe(StalenessProbe):
162
+ def read(self):
163
+ return [FreshnessReading(
164
+ source_system="custom", source_name="widget",
165
+ last_update_epoch=get_event_time(), # or age_seconds=...
166
+ sla_threshold_seconds=60)]
167
+ ```
168
+
169
+ ## Tests
170
+
171
+ ```bash
172
+ python -m pytest -q # 54 tests
173
+ PYTHONPATH=src python ../conformance/runner.py # conformance vectors
174
+ ```
175
+
176
+ Requires **Python 3.8+**. Optional extras: `sql`, `kafka`, `cache`, `otlp`,
177
+ `version`, `dev` (e.g. `pip install -e ".[dev,version]"`).
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "data-staleness-otel"
7
+ version = "0.4.0"
8
+ description = "Vendor-neutral data-staleness / data-freshness instrumentation built on OpenTelemetry (community project; not an official OpenTelemetry package)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Anirudh Rajmohan" }]
13
+ keywords = ["opentelemetry", "observability", "data-freshness", "data-staleness", "age-of-information"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: System :: Monitoring",
20
+ "Framework :: OpenTelemetry",
21
+ ]
22
+ dependencies = [
23
+ "opentelemetry-api>=1.20.0",
24
+ "opentelemetry-sdk>=1.20.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/anirudhrajreliability/otel-data-staleness"
29
+ Repository = "https://github.com/anirudhrajreliability/otel-data-staleness"
30
+ Issues = "https://github.com/anirudhrajreliability/otel-data-staleness/issues"
31
+ Specification = "https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/spec/semantic-conventions.md"
32
+
33
+ [project.optional-dependencies]
34
+ sql = ["sqlalchemy>=1.4"]
35
+ kafka = ["confluent-kafka>=2.0"]
36
+ cache = ["redis>=4.0"]
37
+ version = ["packaging>=21.0"]
38
+ otlp = ["opentelemetry-exporter-otlp>=1.20.0"]
39
+ dev = ["pytest>=7.0"]
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ otel_staleness = ["py.typed"]
46
+
47
+ [tool.pytest.ini_options]
48
+ pythonpath = ["src"]
49
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-staleness-otel
3
+ Version: 0.4.0
4
+ Summary: Vendor-neutral data-staleness / data-freshness instrumentation built on OpenTelemetry (community project; not an official OpenTelemetry package)
5
+ Author: Anirudh Rajmohan
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/anirudhrajreliability/otel-data-staleness
8
+ Project-URL: Repository, https://github.com/anirudhrajreliability/otel-data-staleness
9
+ Project-URL: Issues, https://github.com/anirudhrajreliability/otel-data-staleness/issues
10
+ Project-URL: Specification, https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/spec/semantic-conventions.md
11
+ Keywords: opentelemetry,observability,data-freshness,data-staleness,age-of-information
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Monitoring
17
+ Classifier: Framework :: OpenTelemetry
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: opentelemetry-api>=1.20.0
21
+ Requires-Dist: opentelemetry-sdk>=1.20.0
22
+ Provides-Extra: sql
23
+ Requires-Dist: sqlalchemy>=1.4; extra == "sql"
24
+ Provides-Extra: kafka
25
+ Requires-Dist: confluent-kafka>=2.0; extra == "kafka"
26
+ Provides-Extra: cache
27
+ Requires-Dist: redis>=4.0; extra == "cache"
28
+ Provides-Extra: version
29
+ Requires-Dist: packaging>=21.0; extra == "version"
30
+ Provides-Extra: otlp
31
+ Requires-Dist: opentelemetry-exporter-otlp>=1.20.0; extra == "otlp"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+
35
+ > Part of **otel-data-staleness** — see the [root README](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/README.md) for the project overview and the SDK-vs-Collector comparison.
36
+
37
+ # data-staleness-otel (Python SDK)
38
+
39
+ > **Disclaimer:** This is an independent, community project. It is **not** an
40
+ > official OpenTelemetry project and is **not** endorsed by or affiliated with
41
+ > the OpenTelemetry project or the CNCF. It is *built on* OpenTelemetry; the
42
+ > `data.staleness.*` semantic convention is a **proposal** under discussion with
43
+ > the Semantic Conventions SIG ([#3909](https://github.com/open-telemetry/semantic-conventions/issues/3909)),
44
+ > at "Development" stability — names may change and it is not an accepted standard.
45
+
46
+ Vendor-neutral instrumentation for **data staleness / data freshness**, built on
47
+ OpenTelemetry, implementing the conventions in
48
+ [`../spec/semantic-conventions.md`](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/spec/semantic-conventions.md).
49
+
50
+ You give the SDK *probes* that report the event time of the freshest record for
51
+ a logical source; it emits the standardized `data.staleness.*` metrics
52
+ (age, lag, last-update timestamp, records-behind, SLA threshold/breach) via the
53
+ normal OpenTelemetry metrics pipeline (OTLP, Prometheus, console, etc.).
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install data-staleness-otel # core (imported as `otel_staleness`)
59
+ pip install "data-staleness-otel[sql,kafka,cache]" # with optional integrations
60
+ ```
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ from opentelemetry import metrics
66
+ from opentelemetry.sdk.metrics import MeterProvider
67
+ from opentelemetry.sdk.metrics.export import (
68
+ ConsoleMetricExporter, PeriodicExportingMetricReader)
69
+
70
+ from otel_staleness import StalenessMonitor, conventions as sc
71
+ from otel_staleness.probes import SQLFreshnessProbe
72
+
73
+ reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
74
+ metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
75
+
76
+ monitor = StalenessMonitor()
77
+ monitor.add_probe(SQLFreshnessProbe(
78
+ fetch_max_epoch=lambda: get_latest_updated_at(), # returns Unix seconds or datetime
79
+ source_name="orders", system=sc.System.POSTGRESQL,
80
+ namespace="public", sla_threshold_seconds=300))
81
+ monitor.start()
82
+ ```
83
+
84
+ Run the bundled demo (emits all four source types to the console):
85
+
86
+ ```bash
87
+ python examples/quickstart.py
88
+ ```
89
+
90
+ ## Probes
91
+
92
+ | Probe | Source | Method emitted |
93
+ |-------|--------|----------------|
94
+ | `SQLFreshnessProbe` | Postgres/MySQL/Snowflake/Redshift/BigQuery | `max_timestamp` |
95
+ | `KafkaFreshnessProbe` | Kafka (per partition) | `consumer_lag` |
96
+ | `PipelineFreshnessProbe` | dbt/Airflow batch jobs | `run_completion` |
97
+ | `CacheFreshnessProbe` | Redis | `ttl_age` |
98
+ | `ObjectStoreFreshnessProbe` | S3/GCS | `object_mtime` |
99
+ | `IndexFreshnessProbe` | Search/vector indexes (Elasticsearch, OpenSearch, Pinecone, Weaviate, Milvus, Qdrant, pgvector) | `index_lag` |
100
+ | `ReplicationFreshnessProbe` | Replicas / CDC targets / multi-region (Postgres replicas, Debezium, DynamoDB global tables) | `replication_lag` |
101
+ | `VersionFreshnessProbe` | RAG / docs version-currency vs a registry (PyPI, npm, GitHub, Docker Hub) | `version_drift` |
102
+
103
+ The first five emit **absolute** freshness (`age` vs `now`).
104
+ `IndexFreshnessProbe` / `ReplicationFreshnessProbe` emit **differential**
105
+ (source-relative) freshness — how far a derived/replicated store trails its
106
+ upstream — via `data.staleness.lag` plus a `data.staleness.relative_to`
107
+ attribute. `VersionFreshnessProbe` emits **version-currency** (how many releases
108
+ behind). Example of the differential form:
109
+
110
+ ```python
111
+ from otel_staleness.probes import IndexFreshnessProbe
112
+ # how far the vector index trails its source corpus
113
+ monitor.add_probe(IndexFreshnessProbe(
114
+ fetch_source_epoch=lambda: newest_source_write_time(),
115
+ fetch_index_epoch=lambda: newest_source_time_present_in_index(),
116
+ index_name="docs-v1", source="public.documents",
117
+ system=sc.System.PINECONE, sla_threshold_seconds=60))
118
+ ```
119
+
120
+ **Version-currency for RAG / documentation** — is your indexed doc describing
121
+ the *current* release? Compares the documented version against a registry:
122
+
123
+ ```python
124
+ from otel_staleness.probes import VersionFreshnessProbe
125
+ # your RAG corpus documents Kubernetes 1.28; how far behind is it?
126
+ monitor.add_probe(VersionFreshnessProbe.from_github_releases(
127
+ "kubernetes/kubernetes", documented_version="1.28.0",
128
+ source_name="k8s-docs", sla_threshold_seconds=60*60*24*14)) # 14 days
129
+ ```
130
+
131
+ Emits `records.behind` (releases behind), `age` (how long newer releases have
132
+ existed unreflected), and `version.documented` / `version.current` attributes.
133
+ Helpers: `from_pypi`, `from_npm`, `from_github_releases`, `from_dockerhub`
134
+ (needs the `version` extra: `pip install -e ".[version]"`). The comparison logic
135
+ is pure and unit-tested; unparseable versions raise rather than fabricate a `0`.
136
+
137
+ The **documented version** rarely needs hard-coding — extract it from the
138
+ content with `otel_staleness.version_extract` (frontmatter, a semver token, a
139
+ JSON field, an HTML `<meta>`, or a docs URL path), composed with `first_of`:
140
+
141
+ ```python
142
+ from otel_staleness.version_extract import first_of, extract_frontmatter_version, extract_url_path_version
143
+ documented = first_of(
144
+ lambda: extract_frontmatter_version(open("docs/networking.md").read()),
145
+ lambda: extract_url_path_version("https://kubernetes.io/docs/v1.28/"),
146
+ ) # pass as documented_version=...
147
+ ```
148
+
149
+ See `examples/rag_freshness.py` for the combined RAG check: index time-lag
150
+ (`IndexFreshnessProbe`) **and** version currency (`VersionFreshnessProbe`) on the
151
+ same corpus — the two failure modes that make RAG answers wrong.
152
+
153
+ Every probe accepts plain callables (returning Unix seconds or a `datetime`),
154
+ so it is fully unit-testable without a live backend. `SQLFreshnessProbe` also
155
+ ships a `from_sqlalchemy(...)` convenience constructor.
156
+
157
+ A failing probe is isolated — it cannot break collection of the others.
158
+
159
+ ## End-to-end freshness across hops
160
+
161
+ Point probes measure freshness *at one place*. To get **true cross-hop age**,
162
+ stamp the origin event-time into OTEL baggage at ingest; any downstream stage
163
+ reads it back (it rides the standard OTEL context propagators across
164
+ service/messaging boundaries):
165
+
166
+ ```python
167
+ from otel_staleness.freshness_context import stamp_origin, EndToEndFreshness
168
+ from opentelemetry import context as otel_context
169
+
170
+ # at ingest:
171
+ otel_context.attach(stamp_origin(event_epoch))
172
+ # ...work flows downstream carrying baggage...
173
+ # at a later stage:
174
+ e2e = EndToEndFreshness() # emits data.staleness.age, method=end_to_end
175
+ e2e.record("orders-pipeline", stage="serve")
176
+ ```
177
+
178
+ ## Every staleness dimension
179
+
180
+ `StalenessMonitor.start()` also emits the extension metrics that capture the
181
+ dimensions a backend cannot reconstruct: `data.staleness.age.peak` (Peak AoI),
182
+ `data.staleness.update.interval` (cadence histogram), `data.staleness.partition.skew`
183
+ (straggler partitions), and `data.staleness.probe.errors` (a failing probe is now
184
+ **visible**, not silently swallowed — pass `add_probe(p, name="...")` to label it).
185
+ See [`../docs/STALENESS-TAXONOMY.md`](https://github.com/anirudhrajreliability/otel-data-staleness/blob/main/docs/STALENESS-TAXONOMY.md) for the full
186
+ map of staleness types to mechanisms.
187
+
188
+ ## Custom probes
189
+
190
+ Subclass `StalenessProbe` and return `FreshnessReading` objects:
191
+
192
+ ```python
193
+ from otel_staleness import StalenessProbe, FreshnessReading, conventions as sc
194
+
195
+ class MyProbe(StalenessProbe):
196
+ def read(self):
197
+ return [FreshnessReading(
198
+ source_system="custom", source_name="widget",
199
+ last_update_epoch=get_event_time(), # or age_seconds=...
200
+ sla_threshold_seconds=60)]
201
+ ```
202
+
203
+ ## Tests
204
+
205
+ ```bash
206
+ python -m pytest -q # 54 tests
207
+ PYTHONPATH=src python ../conformance/runner.py # conformance vectors
208
+ ```
209
+
210
+ Requires **Python 3.8+**. Optional extras: `sql`, `kafka`, `cache`, `otlp`,
211
+ `version`, `dev` (e.g. `pip install -e ".[dev,version]"`).