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.
- data_staleness_otel-0.4.0/PKG-INFO +211 -0
- data_staleness_otel-0.4.0/README.md +177 -0
- data_staleness_otel-0.4.0/pyproject.toml +49 -0
- data_staleness_otel-0.4.0/setup.cfg +4 -0
- data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/PKG-INFO +211 -0
- data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/SOURCES.txt +30 -0
- data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/dependency_links.txt +1 -0
- data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/requires.txt +20 -0
- data_staleness_otel-0.4.0/src/data_staleness_otel.egg-info/top_level.txt +1 -0
- data_staleness_otel-0.4.0/src/otel_staleness/__init__.py +23 -0
- data_staleness_otel-0.4.0/src/otel_staleness/_timeutil.py +44 -0
- data_staleness_otel-0.4.0/src/otel_staleness/conventions.py +102 -0
- data_staleness_otel-0.4.0/src/otel_staleness/core.py +351 -0
- data_staleness_otel-0.4.0/src/otel_staleness/freshness_context.py +129 -0
- data_staleness_otel-0.4.0/src/otel_staleness/integrations/__init__.py +3 -0
- data_staleness_otel-0.4.0/src/otel_staleness/integrations/dbt.py +106 -0
- data_staleness_otel-0.4.0/src/otel_staleness/probes/__init__.py +18 -0
- data_staleness_otel-0.4.0/src/otel_staleness/probes/cache.py +76 -0
- data_staleness_otel-0.4.0/src/otel_staleness/probes/differential.py +123 -0
- data_staleness_otel-0.4.0/src/otel_staleness/probes/kafka.py +66 -0
- data_staleness_otel-0.4.0/src/otel_staleness/probes/pipeline.py +44 -0
- data_staleness_otel-0.4.0/src/otel_staleness/probes/sql.py +75 -0
- data_staleness_otel-0.4.0/src/otel_staleness/probes/version.py +245 -0
- data_staleness_otel-0.4.0/src/otel_staleness/py.typed +0 -0
- data_staleness_otel-0.4.0/src/otel_staleness/version_extract.py +87 -0
- data_staleness_otel-0.4.0/tests/test_core.py +131 -0
- data_staleness_otel-0.4.0/tests/test_dbt.py +71 -0
- data_staleness_otel-0.4.0/tests/test_extension_metrics.py +140 -0
- data_staleness_otel-0.4.0/tests/test_freshness_context.py +77 -0
- data_staleness_otel-0.4.0/tests/test_probes.py +136 -0
- data_staleness_otel-0.4.0/tests/test_version.py +98 -0
- 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,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]"`).
|