impulse-telemetry 0.1.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.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: impulse-telemetry
3
+ Version: 0.1.0
4
+ Summary: Observability SDK for Impulse microservices
5
+ License: MIT
6
+ Keywords: observability,telemetry,opentelemetry,prometheus,fastapi,ml
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: System :: Monitoring
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: opentelemetry-sdk>=1.24
17
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.24
18
+ Requires-Dist: opentelemetry-propagator-b3>=1.24
19
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.45b0
20
+ Requires-Dist: opentelemetry-instrumentation-requests>=0.45b0
21
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.45b0
22
+ Requires-Dist: prometheus-client>=0.20
23
+ Requires-Dist: structlog>=24.0
24
+ Provides-Extra: extras
25
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.45b0; extra == "extras"
26
+ Requires-Dist: opentelemetry-instrumentation-redis>=0.45b0; extra == "extras"
27
+ Requires-Dist: opentelemetry-instrumentation-celery>=0.45b0; extra == "extras"
28
+ Provides-Extra: ml
29
+ Requires-Dist: pandas>=2.0; extra == "ml"
30
+
31
+ # impulse_telemetry
32
+
33
+ Observability SDK for Impulse microservices. One `init()` call wires up distributed tracing, Prometheus metrics, structured logging, and FastAPI middleware.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install impulse-telemetry
39
+
40
+ # With SQLAlchemy / Redis / Celery auto-instrumentation
41
+ pip install "impulse-telemetry[extras]"
42
+
43
+ # With ML data quality checks (pandas required)
44
+ pip install "impulse-telemetry[ml]"
45
+ ```
46
+
47
+ ## Quickstart
48
+
49
+ ```python
50
+ from fastapi import FastAPI
51
+ from impulse_telemetry import init
52
+
53
+ app = FastAPI()
54
+ init(app, service="my-service", version="1.0.0", env="production")
55
+ ```
56
+
57
+ Every HTTP request now emits RED metrics, a distributed trace, and a structured JSON log line — automatically.
58
+
59
+ ## `init()` parameters
60
+
61
+ | Parameter | Default | Description |
62
+ |---|---|---|
63
+ | `app` | `None` | FastAPI app. Pass to enable HTTP middleware. |
64
+ | `service` | **required** | Service name — appears on all telemetry. |
65
+ | `version` | `"0.0.0"` | Semver string tagged on all telemetry. |
66
+ | `env` | `$ENV` | `"production"` \| `"staging"` \| `"dev"` |
67
+ | `otlp_endpoint` | `$OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP Collector gRPC address. |
68
+ | `prometheus_port` | `None` | Expose `/metrics` on this port (for workers without HTTP). |
69
+ | `log_level` | `"INFO"` | `"DEBUG"` \| `"INFO"` \| `"WARNING"` \| `"ERROR"` |
70
+ | `enable_ml` | `False` | Register ML-specific metric instruments. |
71
+
72
+ ## Logging
73
+
74
+ ```python
75
+ from impulse_telemetry.logging import get_logger
76
+
77
+ log = get_logger(__name__)
78
+ log.info("prediction_served", model_id="rec-v3", latency_ms=43)
79
+ # {"service":"my-service","trace_id":"abc..","event":"prediction_served","model_id":"rec-v3",...}
80
+ ```
81
+
82
+ `trace_id` and `span_id` are injected automatically from the active OTEL span.
83
+
84
+ ### Per-request context
85
+
86
+ Bind fields once per request — they appear on every subsequent log line for that request.
87
+
88
+ ```python
89
+ from impulse_telemetry.logging import bind_request_context, clear_request_context
90
+
91
+ token = bind_request_context(user_id="usr_123", request_id="req_456")
92
+ log.info("doing_work") # user_id + request_id injected automatically
93
+ clear_request_context(token)
94
+ ```
95
+
96
+ ## Metrics
97
+
98
+ Prometheus metrics are pre-registered and labeled with `service` and `env`.
99
+
100
+ ### Automatic (via middleware)
101
+
102
+ | Metric | Type | Description |
103
+ |---|---|---|
104
+ | `http_requests_total` | Counter | Requests by route, method, status |
105
+ | `http_request_errors_total` | Counter | 4xx / 5xx responses |
106
+ | `http_request_duration_seconds` | Histogram | Request latency |
107
+ | `http_active_requests` | Gauge | In-flight requests |
108
+
109
+ ### Manual
110
+
111
+ ```python
112
+ from impulse_telemetry.metrics import get_metrics
113
+
114
+ m = get_metrics()
115
+ m.dependency_latency.labels(**m.labels(dependency="redis")).observe(elapsed)
116
+ m.dependency_errors.labels(**m.labels(dependency="redis")).inc()
117
+ ```
118
+
119
+ Available instruments: `rate_limiter_hits`, `rate_limiter_remaining`, `dependency_latency`, `dependency_errors`, `queue_depth`, `circuit_breaker`.
120
+
121
+ ## Tracing
122
+
123
+ ```python
124
+ from impulse_telemetry.tracing import get_tracer, inject_headers
125
+
126
+ tracer = get_tracer(__name__)
127
+
128
+ with tracer.start_as_current_span("my_operation") as span:
129
+ span.set_attribute("key", "value")
130
+ headers = inject_headers({}) # propagate W3C traceparent to downstream
131
+ requests.get("http://other-service/api", headers=headers)
132
+ ```
133
+
134
+ Auto-instrumented libraries (when installed): `requests`, `httpx`, `SQLAlchemy`, `Redis`, `Celery`.
135
+
136
+ ## ML Monitoring
137
+
138
+ Enable at startup with `enable_ml=True`.
139
+
140
+ ```python
141
+ from impulse_telemetry.ml import MLMonitor
142
+
143
+ monitor = MLMonitor(model_id="rec-v3", user_id=user_id)
144
+ ```
145
+
146
+ ### Inference tracing
147
+
148
+ ```python
149
+ # Context manager
150
+ with monitor.inference(features=df) as span:
151
+ result = model.predict(df)
152
+ span.record_output(result)
153
+
154
+ # Decorator
155
+ @monitor.trace
156
+ def predict(features):
157
+ return model.predict(features)
158
+ ```
159
+
160
+ Both record inference latency, error count, and run data quality checks (missing values, schema violations) automatically.
161
+
162
+ ### Drift & performance metrics
163
+
164
+ ```python
165
+ # Call from batch evaluation jobs
166
+ monitor.record_drift("age", score=0.18, metric="psi")
167
+ monitor.record_performance(rmse=0.042, precision=0.87, recall=0.81)
168
+ ```
169
+
170
+ ### ML metric instruments
171
+
172
+ | Metric | Type | Description |
173
+ |---|---|---|
174
+ | `ml_inference_duration_seconds` | Histogram | Per-model inference latency |
175
+ | `ml_inference_requests_total` | Counter | Inference count |
176
+ | `ml_inference_errors_total` | Counter | Inference errors |
177
+ | `ml_missing_feature_rate` | Gauge | Missing values per column |
178
+ | `ml_schema_violations_total` | Counter | Schema violation count |
179
+ | `ml_feature_drift` | Gauge | Feature drift score |
180
+ | `ml_prediction_drift` | Gauge | Prediction drift score |
181
+ | `ml_rmse` | Gauge | Rolling RMSE |
182
+ | `ml_precision` | Gauge | Rolling precision |
183
+ | `ml_recall` | Gauge | Rolling recall |
184
+
185
+ ## Background workers
186
+
187
+ For services without an HTTP server, expose metrics on a dedicated port:
188
+
189
+ ```python
190
+ init(service="ingest-worker", prometheus_port=9090)
191
+ # Prometheus scrapes localhost:9090/metrics
192
+ ```
193
+
194
+ ## Environment variables
195
+
196
+ | Variable | Description |
197
+ |---|---|
198
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP Collector gRPC address |
199
+ | `ENV` | Deployment environment (`production`, `staging`, `dev`) |
200
+
201
+ ## See also
202
+
203
+ [examples.py](examples.py) — minimal, runnable code for every feature.
@@ -0,0 +1,173 @@
1
+ # impulse_telemetry
2
+
3
+ Observability SDK for Impulse microservices. One `init()` call wires up distributed tracing, Prometheus metrics, structured logging, and FastAPI middleware.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install impulse-telemetry
9
+
10
+ # With SQLAlchemy / Redis / Celery auto-instrumentation
11
+ pip install "impulse-telemetry[extras]"
12
+
13
+ # With ML data quality checks (pandas required)
14
+ pip install "impulse-telemetry[ml]"
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ```python
20
+ from fastapi import FastAPI
21
+ from impulse_telemetry import init
22
+
23
+ app = FastAPI()
24
+ init(app, service="my-service", version="1.0.0", env="production")
25
+ ```
26
+
27
+ Every HTTP request now emits RED metrics, a distributed trace, and a structured JSON log line — automatically.
28
+
29
+ ## `init()` parameters
30
+
31
+ | Parameter | Default | Description |
32
+ |---|---|---|
33
+ | `app` | `None` | FastAPI app. Pass to enable HTTP middleware. |
34
+ | `service` | **required** | Service name — appears on all telemetry. |
35
+ | `version` | `"0.0.0"` | Semver string tagged on all telemetry. |
36
+ | `env` | `$ENV` | `"production"` \| `"staging"` \| `"dev"` |
37
+ | `otlp_endpoint` | `$OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP Collector gRPC address. |
38
+ | `prometheus_port` | `None` | Expose `/metrics` on this port (for workers without HTTP). |
39
+ | `log_level` | `"INFO"` | `"DEBUG"` \| `"INFO"` \| `"WARNING"` \| `"ERROR"` |
40
+ | `enable_ml` | `False` | Register ML-specific metric instruments. |
41
+
42
+ ## Logging
43
+
44
+ ```python
45
+ from impulse_telemetry.logging import get_logger
46
+
47
+ log = get_logger(__name__)
48
+ log.info("prediction_served", model_id="rec-v3", latency_ms=43)
49
+ # {"service":"my-service","trace_id":"abc..","event":"prediction_served","model_id":"rec-v3",...}
50
+ ```
51
+
52
+ `trace_id` and `span_id` are injected automatically from the active OTEL span.
53
+
54
+ ### Per-request context
55
+
56
+ Bind fields once per request — they appear on every subsequent log line for that request.
57
+
58
+ ```python
59
+ from impulse_telemetry.logging import bind_request_context, clear_request_context
60
+
61
+ token = bind_request_context(user_id="usr_123", request_id="req_456")
62
+ log.info("doing_work") # user_id + request_id injected automatically
63
+ clear_request_context(token)
64
+ ```
65
+
66
+ ## Metrics
67
+
68
+ Prometheus metrics are pre-registered and labeled with `service` and `env`.
69
+
70
+ ### Automatic (via middleware)
71
+
72
+ | Metric | Type | Description |
73
+ |---|---|---|
74
+ | `http_requests_total` | Counter | Requests by route, method, status |
75
+ | `http_request_errors_total` | Counter | 4xx / 5xx responses |
76
+ | `http_request_duration_seconds` | Histogram | Request latency |
77
+ | `http_active_requests` | Gauge | In-flight requests |
78
+
79
+ ### Manual
80
+
81
+ ```python
82
+ from impulse_telemetry.metrics import get_metrics
83
+
84
+ m = get_metrics()
85
+ m.dependency_latency.labels(**m.labels(dependency="redis")).observe(elapsed)
86
+ m.dependency_errors.labels(**m.labels(dependency="redis")).inc()
87
+ ```
88
+
89
+ Available instruments: `rate_limiter_hits`, `rate_limiter_remaining`, `dependency_latency`, `dependency_errors`, `queue_depth`, `circuit_breaker`.
90
+
91
+ ## Tracing
92
+
93
+ ```python
94
+ from impulse_telemetry.tracing import get_tracer, inject_headers
95
+
96
+ tracer = get_tracer(__name__)
97
+
98
+ with tracer.start_as_current_span("my_operation") as span:
99
+ span.set_attribute("key", "value")
100
+ headers = inject_headers({}) # propagate W3C traceparent to downstream
101
+ requests.get("http://other-service/api", headers=headers)
102
+ ```
103
+
104
+ Auto-instrumented libraries (when installed): `requests`, `httpx`, `SQLAlchemy`, `Redis`, `Celery`.
105
+
106
+ ## ML Monitoring
107
+
108
+ Enable at startup with `enable_ml=True`.
109
+
110
+ ```python
111
+ from impulse_telemetry.ml import MLMonitor
112
+
113
+ monitor = MLMonitor(model_id="rec-v3", user_id=user_id)
114
+ ```
115
+
116
+ ### Inference tracing
117
+
118
+ ```python
119
+ # Context manager
120
+ with monitor.inference(features=df) as span:
121
+ result = model.predict(df)
122
+ span.record_output(result)
123
+
124
+ # Decorator
125
+ @monitor.trace
126
+ def predict(features):
127
+ return model.predict(features)
128
+ ```
129
+
130
+ Both record inference latency, error count, and run data quality checks (missing values, schema violations) automatically.
131
+
132
+ ### Drift & performance metrics
133
+
134
+ ```python
135
+ # Call from batch evaluation jobs
136
+ monitor.record_drift("age", score=0.18, metric="psi")
137
+ monitor.record_performance(rmse=0.042, precision=0.87, recall=0.81)
138
+ ```
139
+
140
+ ### ML metric instruments
141
+
142
+ | Metric | Type | Description |
143
+ |---|---|---|
144
+ | `ml_inference_duration_seconds` | Histogram | Per-model inference latency |
145
+ | `ml_inference_requests_total` | Counter | Inference count |
146
+ | `ml_inference_errors_total` | Counter | Inference errors |
147
+ | `ml_missing_feature_rate` | Gauge | Missing values per column |
148
+ | `ml_schema_violations_total` | Counter | Schema violation count |
149
+ | `ml_feature_drift` | Gauge | Feature drift score |
150
+ | `ml_prediction_drift` | Gauge | Prediction drift score |
151
+ | `ml_rmse` | Gauge | Rolling RMSE |
152
+ | `ml_precision` | Gauge | Rolling precision |
153
+ | `ml_recall` | Gauge | Rolling recall |
154
+
155
+ ## Background workers
156
+
157
+ For services without an HTTP server, expose metrics on a dedicated port:
158
+
159
+ ```python
160
+ init(service="ingest-worker", prometheus_port=9090)
161
+ # Prometheus scrapes localhost:9090/metrics
162
+ ```
163
+
164
+ ## Environment variables
165
+
166
+ | Variable | Description |
167
+ |---|---|
168
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP Collector gRPC address |
169
+ | `ENV` | Deployment environment (`production`, `staging`, `dev`) |
170
+
171
+ ## See also
172
+
173
+ [examples.py](examples.py) — minimal, runnable code for every feature.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: impulse-telemetry
3
+ Version: 0.1.0
4
+ Summary: Observability SDK for Impulse microservices
5
+ License: MIT
6
+ Keywords: observability,telemetry,opentelemetry,prometheus,fastapi,ml
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: System :: Monitoring
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: opentelemetry-sdk>=1.24
17
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.24
18
+ Requires-Dist: opentelemetry-propagator-b3>=1.24
19
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.45b0
20
+ Requires-Dist: opentelemetry-instrumentation-requests>=0.45b0
21
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.45b0
22
+ Requires-Dist: prometheus-client>=0.20
23
+ Requires-Dist: structlog>=24.0
24
+ Provides-Extra: extras
25
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.45b0; extra == "extras"
26
+ Requires-Dist: opentelemetry-instrumentation-redis>=0.45b0; extra == "extras"
27
+ Requires-Dist: opentelemetry-instrumentation-celery>=0.45b0; extra == "extras"
28
+ Provides-Extra: ml
29
+ Requires-Dist: pandas>=2.0; extra == "ml"
30
+
31
+ # impulse_telemetry
32
+
33
+ Observability SDK for Impulse microservices. One `init()` call wires up distributed tracing, Prometheus metrics, structured logging, and FastAPI middleware.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install impulse-telemetry
39
+
40
+ # With SQLAlchemy / Redis / Celery auto-instrumentation
41
+ pip install "impulse-telemetry[extras]"
42
+
43
+ # With ML data quality checks (pandas required)
44
+ pip install "impulse-telemetry[ml]"
45
+ ```
46
+
47
+ ## Quickstart
48
+
49
+ ```python
50
+ from fastapi import FastAPI
51
+ from impulse_telemetry import init
52
+
53
+ app = FastAPI()
54
+ init(app, service="my-service", version="1.0.0", env="production")
55
+ ```
56
+
57
+ Every HTTP request now emits RED metrics, a distributed trace, and a structured JSON log line — automatically.
58
+
59
+ ## `init()` parameters
60
+
61
+ | Parameter | Default | Description |
62
+ |---|---|---|
63
+ | `app` | `None` | FastAPI app. Pass to enable HTTP middleware. |
64
+ | `service` | **required** | Service name — appears on all telemetry. |
65
+ | `version` | `"0.0.0"` | Semver string tagged on all telemetry. |
66
+ | `env` | `$ENV` | `"production"` \| `"staging"` \| `"dev"` |
67
+ | `otlp_endpoint` | `$OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP Collector gRPC address. |
68
+ | `prometheus_port` | `None` | Expose `/metrics` on this port (for workers without HTTP). |
69
+ | `log_level` | `"INFO"` | `"DEBUG"` \| `"INFO"` \| `"WARNING"` \| `"ERROR"` |
70
+ | `enable_ml` | `False` | Register ML-specific metric instruments. |
71
+
72
+ ## Logging
73
+
74
+ ```python
75
+ from impulse_telemetry.logging import get_logger
76
+
77
+ log = get_logger(__name__)
78
+ log.info("prediction_served", model_id="rec-v3", latency_ms=43)
79
+ # {"service":"my-service","trace_id":"abc..","event":"prediction_served","model_id":"rec-v3",...}
80
+ ```
81
+
82
+ `trace_id` and `span_id` are injected automatically from the active OTEL span.
83
+
84
+ ### Per-request context
85
+
86
+ Bind fields once per request — they appear on every subsequent log line for that request.
87
+
88
+ ```python
89
+ from impulse_telemetry.logging import bind_request_context, clear_request_context
90
+
91
+ token = bind_request_context(user_id="usr_123", request_id="req_456")
92
+ log.info("doing_work") # user_id + request_id injected automatically
93
+ clear_request_context(token)
94
+ ```
95
+
96
+ ## Metrics
97
+
98
+ Prometheus metrics are pre-registered and labeled with `service` and `env`.
99
+
100
+ ### Automatic (via middleware)
101
+
102
+ | Metric | Type | Description |
103
+ |---|---|---|
104
+ | `http_requests_total` | Counter | Requests by route, method, status |
105
+ | `http_request_errors_total` | Counter | 4xx / 5xx responses |
106
+ | `http_request_duration_seconds` | Histogram | Request latency |
107
+ | `http_active_requests` | Gauge | In-flight requests |
108
+
109
+ ### Manual
110
+
111
+ ```python
112
+ from impulse_telemetry.metrics import get_metrics
113
+
114
+ m = get_metrics()
115
+ m.dependency_latency.labels(**m.labels(dependency="redis")).observe(elapsed)
116
+ m.dependency_errors.labels(**m.labels(dependency="redis")).inc()
117
+ ```
118
+
119
+ Available instruments: `rate_limiter_hits`, `rate_limiter_remaining`, `dependency_latency`, `dependency_errors`, `queue_depth`, `circuit_breaker`.
120
+
121
+ ## Tracing
122
+
123
+ ```python
124
+ from impulse_telemetry.tracing import get_tracer, inject_headers
125
+
126
+ tracer = get_tracer(__name__)
127
+
128
+ with tracer.start_as_current_span("my_operation") as span:
129
+ span.set_attribute("key", "value")
130
+ headers = inject_headers({}) # propagate W3C traceparent to downstream
131
+ requests.get("http://other-service/api", headers=headers)
132
+ ```
133
+
134
+ Auto-instrumented libraries (when installed): `requests`, `httpx`, `SQLAlchemy`, `Redis`, `Celery`.
135
+
136
+ ## ML Monitoring
137
+
138
+ Enable at startup with `enable_ml=True`.
139
+
140
+ ```python
141
+ from impulse_telemetry.ml import MLMonitor
142
+
143
+ monitor = MLMonitor(model_id="rec-v3", user_id=user_id)
144
+ ```
145
+
146
+ ### Inference tracing
147
+
148
+ ```python
149
+ # Context manager
150
+ with monitor.inference(features=df) as span:
151
+ result = model.predict(df)
152
+ span.record_output(result)
153
+
154
+ # Decorator
155
+ @monitor.trace
156
+ def predict(features):
157
+ return model.predict(features)
158
+ ```
159
+
160
+ Both record inference latency, error count, and run data quality checks (missing values, schema violations) automatically.
161
+
162
+ ### Drift & performance metrics
163
+
164
+ ```python
165
+ # Call from batch evaluation jobs
166
+ monitor.record_drift("age", score=0.18, metric="psi")
167
+ monitor.record_performance(rmse=0.042, precision=0.87, recall=0.81)
168
+ ```
169
+
170
+ ### ML metric instruments
171
+
172
+ | Metric | Type | Description |
173
+ |---|---|---|
174
+ | `ml_inference_duration_seconds` | Histogram | Per-model inference latency |
175
+ | `ml_inference_requests_total` | Counter | Inference count |
176
+ | `ml_inference_errors_total` | Counter | Inference errors |
177
+ | `ml_missing_feature_rate` | Gauge | Missing values per column |
178
+ | `ml_schema_violations_total` | Counter | Schema violation count |
179
+ | `ml_feature_drift` | Gauge | Feature drift score |
180
+ | `ml_prediction_drift` | Gauge | Prediction drift score |
181
+ | `ml_rmse` | Gauge | Rolling RMSE |
182
+ | `ml_precision` | Gauge | Rolling precision |
183
+ | `ml_recall` | Gauge | Rolling recall |
184
+
185
+ ## Background workers
186
+
187
+ For services without an HTTP server, expose metrics on a dedicated port:
188
+
189
+ ```python
190
+ init(service="ingest-worker", prometheus_port=9090)
191
+ # Prometheus scrapes localhost:9090/metrics
192
+ ```
193
+
194
+ ## Environment variables
195
+
196
+ | Variable | Description |
197
+ |---|---|
198
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP Collector gRPC address |
199
+ | `ENV` | Deployment environment (`production`, `staging`, `dev`) |
200
+
201
+ ## See also
202
+
203
+ [examples.py](examples.py) — minimal, runnable code for every feature.
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ impulse_telemetry.egg-info/PKG-INFO
4
+ impulse_telemetry.egg-info/SOURCES.txt
5
+ impulse_telemetry.egg-info/dependency_links.txt
6
+ impulse_telemetry.egg-info/requires.txt
7
+ impulse_telemetry.egg-info/top_level.txt
8
+ tests/test_logging.py
9
+ tests/test_metrics.py
10
+ tests/test_middleware.py
11
+ tests/test_ml.py
12
+ tests/test_tracing.py
@@ -0,0 +1,16 @@
1
+ opentelemetry-sdk>=1.24
2
+ opentelemetry-exporter-otlp-proto-grpc>=1.24
3
+ opentelemetry-propagator-b3>=1.24
4
+ opentelemetry-instrumentation-fastapi>=0.45b0
5
+ opentelemetry-instrumentation-requests>=0.45b0
6
+ opentelemetry-instrumentation-httpx>=0.45b0
7
+ prometheus-client>=0.20
8
+ structlog>=24.0
9
+
10
+ [extras]
11
+ opentelemetry-instrumentation-sqlalchemy>=0.45b0
12
+ opentelemetry-instrumentation-redis>=0.45b0
13
+ opentelemetry-instrumentation-celery>=0.45b0
14
+
15
+ [ml]
16
+ pandas>=2.0
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "impulse-telemetry"
7
+ version = "0.1.0"
8
+ description = "Observability SDK for Impulse microservices"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["observability", "telemetry", "opentelemetry", "prometheus", "fastapi", "ml"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Topic :: System :: Monitoring",
21
+ ]
22
+
23
+ dependencies = [
24
+ "opentelemetry-sdk>=1.24",
25
+ "opentelemetry-exporter-otlp-proto-grpc>=1.24",
26
+ "opentelemetry-propagator-b3>=1.24",
27
+ "opentelemetry-instrumentation-fastapi>=0.45b0",
28
+ "opentelemetry-instrumentation-requests>=0.45b0",
29
+ "opentelemetry-instrumentation-httpx>=0.45b0",
30
+ "prometheus-client>=0.20",
31
+ "structlog>=24.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ # For services using DBs, caches, queues
36
+ extras = [
37
+ "opentelemetry-instrumentation-sqlalchemy>=0.45b0",
38
+ "opentelemetry-instrumentation-redis>=0.45b0",
39
+ "opentelemetry-instrumentation-celery>=0.45b0",
40
+ ]
41
+ # For ML services using data quality checks
42
+ ml = ["pandas>=2.0"]
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["."]
46
+ include = ["impulse_telemetry*"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ # asyncio_mode = "auto" # uncomment if async tests are added later
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,57 @@
1
+ """
2
+ Tests for impulse_telemetry.logging
3
+
4
+ Covers: get_logger, bind_request_context, clear_request_context, get_request_context.
5
+ """
6
+
7
+ from impulse_telemetry.logging import get_logger, bind_request_context, clear_request_context
8
+ from impulse_telemetry.logging.context import get_request_context
9
+
10
+
11
+ def test_get_logger_returns_bound_logger():
12
+ log = get_logger(__name__)
13
+ assert log is not None
14
+
15
+
16
+ def test_log_does_not_raise():
17
+ log = get_logger(__name__)
18
+ log.info("test_event", key="value")
19
+ log.warning("test_warning", code=42)
20
+ log.error("test_error", reason="none")
21
+
22
+
23
+ def test_bind_injects_fields():
24
+ token = bind_request_context(user_id="u1", request_id="r1")
25
+ ctx = get_request_context()
26
+ assert ctx["user_id"] == "u1"
27
+ assert ctx["request_id"] == "r1"
28
+ clear_request_context(token)
29
+
30
+
31
+ def test_clear_restores_previous_context():
32
+ token = bind_request_context(user_id="u1")
33
+ clear_request_context(token)
34
+ ctx = get_request_context()
35
+ assert "user_id" not in ctx
36
+
37
+
38
+ def test_context_empty_by_default():
39
+ ctx = get_request_context()
40
+ assert isinstance(ctx, dict)
41
+ # No stale fields from previous tests (each test gets its own call stack)
42
+
43
+
44
+ def test_nested_context_bind():
45
+ outer = bind_request_context(user_id="outer")
46
+ inner = bind_request_context(user_id="inner", extra="yes")
47
+
48
+ ctx = get_request_context()
49
+ assert ctx["user_id"] == "inner"
50
+ assert ctx["extra"] == "yes"
51
+
52
+ clear_request_context(inner)
53
+ ctx = get_request_context()
54
+ assert ctx["user_id"] == "outer"
55
+ assert "extra" not in ctx
56
+
57
+ clear_request_context(outer)
@@ -0,0 +1,68 @@
1
+ """
2
+ Tests for impulse_telemetry.metrics
3
+
4
+ Covers: get_metrics singleton, labels(), system instruments, ML instruments.
5
+ """
6
+
7
+ import pytest
8
+ from impulse_telemetry.metrics import get_metrics
9
+ from impulse_telemetry.metrics.registry import Metrics
10
+
11
+
12
+ def test_get_metrics_returns_instance(app):
13
+ m = get_metrics()
14
+ assert isinstance(m, Metrics)
15
+
16
+
17
+ def test_get_metrics_is_singleton(app):
18
+ assert get_metrics() is get_metrics()
19
+
20
+
21
+ def test_labels_merges_defaults(app):
22
+ m = get_metrics()
23
+ labels = m.labels(route="/predict", status="200")
24
+ assert labels["service"] == "test-svc"
25
+ assert labels["env"] == "dev"
26
+ assert labels["route"] == "/predict"
27
+ assert labels["status"] == "200"
28
+
29
+
30
+ def test_labels_defaults_only(app):
31
+ m = get_metrics()
32
+ labels = m.labels()
33
+ assert set(labels.keys()) == {"service", "env"}
34
+
35
+
36
+ def test_system_instruments_present(app):
37
+ sys = get_metrics().system
38
+ assert sys.requests_total is not None
39
+ assert sys.errors_total is not None
40
+ assert sys.request_latency is not None
41
+ assert sys.active_requests is not None
42
+ assert sys.dependency_latency is not None
43
+ assert sys.dependency_errors is not None
44
+
45
+
46
+ def test_ml_instruments_present(app):
47
+ ml = get_metrics().ml
48
+ assert ml is not None
49
+ assert ml.inference_latency is not None
50
+ assert ml.inference_total is not None
51
+ assert ml.inference_errors is not None
52
+ assert ml.feature_drift is not None
53
+ assert ml.missing_feature_rate is not None
54
+ assert ml.rmse is not None
55
+ assert ml.precision is not None
56
+ assert ml.recall is not None
57
+
58
+
59
+ def test_counter_increment_does_not_raise(app):
60
+ m = get_metrics()
61
+ m.system.requests_total.labels(
62
+ **m.labels(route="/test", method="GET", status="200")
63
+ ).inc()
64
+
65
+
66
+ def test_gauge_set_does_not_raise(app):
67
+ m = get_metrics()
68
+ m.system.queue_depth.labels(**m.labels(queue_name="default")).set(5)
@@ -0,0 +1,52 @@
1
+ """
2
+ Tests for impulse_telemetry.middleware
3
+
4
+ Covers: RED metrics recording, X-Trace-Id header injection,
5
+ health-check path skipping, route pattern matching.
6
+ """
7
+
8
+
9
+ def test_200_response(client):
10
+ resp = client.get("/ping")
11
+ assert resp.status_code == 200
12
+
13
+
14
+ def test_400_response(client):
15
+ resp = client.get("/fail")
16
+ assert resp.status_code == 400
17
+
18
+
19
+ def test_trace_id_header_on_success(client):
20
+ resp = client.get("/ping")
21
+ # X-Trace-Id is injected by ImpulseMiddleware when an active span exists.
22
+ # With FastAPIInstrumentor wired up, a span is always created per request.
23
+ assert "x-trace-id" in resp.headers
24
+
25
+
26
+ def test_trace_id_is_32_char_hex(client):
27
+ resp = client.get("/ping")
28
+ trace_id = resp.headers.get("x-trace-id", "")
29
+ assert len(trace_id) == 32
30
+ assert all(c in "0123456789abcdef" for c in trace_id)
31
+
32
+
33
+ def test_request_id_header_forwarded(client):
34
+ resp = client.get("/ping", headers={"x-request-id": "req-abc"})
35
+ assert resp.status_code == 200
36
+
37
+
38
+ def test_health_check_paths_are_skipped(client):
39
+ # These paths exist in _SKIP — middleware calls call_next without recording metrics.
40
+ # They 404 because we didn't register them as routes, which is fine;
41
+ # the important thing is that get_metrics() is not called for them.
42
+ for path in ("/healthz", "/readyz", "/livez"):
43
+ resp = client.get(path)
44
+ assert resp.status_code == 404 # route not found, not a middleware error
45
+
46
+
47
+ def test_route_pattern_used_not_raw_url(client):
48
+ # /users/123 and /users/456 should both match route pattern /users/{user_id}
49
+ resp1 = client.get("/users/123")
50
+ resp2 = client.get("/users/456")
51
+ assert resp1.status_code == 200
52
+ assert resp2.status_code == 200
@@ -0,0 +1,94 @@
1
+ """
2
+ Tests for impulse_telemetry.ml
3
+
4
+ Covers: MLMonitor.inference() context manager, @trace decorator,
5
+ record_drift, record_performance, data quality checks, error handling.
6
+ """
7
+
8
+ import pytest
9
+ from impulse_telemetry.ml import MLMonitor
10
+
11
+
12
+ @pytest.fixture(scope="module")
13
+ def monitor(app): # app ensures init(enable_ml=True) ran first
14
+ return MLMonitor(model_id="test-model", user_id="test-user")
15
+
16
+
17
+ def test_inference_context_manager(monitor):
18
+ with monitor.inference() as span:
19
+ assert span is not None
20
+
21
+
22
+ def test_inference_span_set(monitor):
23
+ with monitor.inference() as span:
24
+ span.set("custom_attr", "hello")
25
+
26
+
27
+ def test_inference_record_output_list(monitor):
28
+ with monitor.inference() as span:
29
+ span.record_output([0.1, 0.9, 0.5])
30
+
31
+
32
+ def test_inference_record_output_no_len(monitor):
33
+ # record_output is best-effort — scalars should not raise
34
+ with monitor.inference() as span:
35
+ span.record_output(0.42)
36
+
37
+
38
+ def test_inference_propagates_exception(monitor):
39
+ with pytest.raises(ValueError, match="boom"):
40
+ with monitor.inference():
41
+ raise ValueError("boom")
42
+
43
+
44
+ def test_trace_decorator(monitor):
45
+ @monitor.trace
46
+ def predict(x):
47
+ return x * 2
48
+
49
+ assert predict(5) == 10
50
+
51
+
52
+ def test_trace_decorator_propagates_exception(monitor):
53
+ @monitor.trace
54
+ def bad():
55
+ raise RuntimeError("fail")
56
+
57
+ with pytest.raises(RuntimeError):
58
+ bad()
59
+
60
+
61
+ def test_record_drift_does_not_raise(monitor):
62
+ monitor.record_drift("age", score=0.18)
63
+ monitor.record_drift("income", score=0.05, metric="kl")
64
+
65
+
66
+ def test_record_performance_does_not_raise(monitor):
67
+ monitor.record_performance(rmse=0.042, precision=0.87, recall=0.81)
68
+
69
+
70
+ def test_record_performance_partial(monitor):
71
+ monitor.record_performance(rmse=0.1)
72
+ monitor.record_performance(precision=0.9)
73
+
74
+
75
+ def test_inference_with_clean_dataframe(monitor):
76
+ pd = pytest.importorskip("pandas")
77
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
78
+ with monitor.inference(features=df) as span:
79
+ span.record_output([0.1, 0.2, 0.3])
80
+
81
+
82
+ def test_inference_with_missing_values_warns(monitor):
83
+ pd = pytest.importorskip("pandas")
84
+ df = pd.DataFrame({"a": [1, None, 3], "b": [None, None, 6]})
85
+ # Missing values are recorded as metrics + logged as warnings — should not raise
86
+ with monitor.inference(features=df):
87
+ pass
88
+
89
+
90
+ def test_inference_with_empty_dataframe(monitor):
91
+ pd = pytest.importorskip("pandas")
92
+ df = pd.DataFrame()
93
+ with monitor.inference(features=df):
94
+ pass
@@ -0,0 +1,66 @@
1
+ """
2
+ Tests for impulse_telemetry.tracing
3
+
4
+ Covers: current_span_context, inject_headers, get_tracer.
5
+ Uses a local TracerProvider + InMemorySpanExporter so tests are
6
+ self-contained and don't require an OTLP collector.
7
+ """
8
+
9
+ from opentelemetry.sdk.trace import TracerProvider
10
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
11
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
12
+
13
+ from impulse_telemetry.tracing import get_tracer, current_span_context, inject_headers
14
+
15
+
16
+ def _local_tracer():
17
+ """Create an isolated tracer backed by an in-memory exporter."""
18
+ exporter = InMemorySpanExporter()
19
+ provider = TracerProvider()
20
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
21
+ return provider.get_tracer("test"), exporter
22
+
23
+
24
+ def test_no_active_span_returns_empty_strings():
25
+ ctx = current_span_context()
26
+ assert ctx == {"trace_id": "", "span_id": ""}
27
+
28
+
29
+ def test_active_span_returns_hex_ids():
30
+ tracer, _ = _local_tracer()
31
+ with tracer.start_as_current_span("op"):
32
+ ctx = current_span_context()
33
+ assert len(ctx["trace_id"]) == 32
34
+ assert len(ctx["span_id"]) == 16
35
+ assert all(c in "0123456789abcdef" for c in ctx["trace_id"])
36
+ assert all(c in "0123456789abcdef" for c in ctx["span_id"])
37
+
38
+
39
+ def test_inject_headers_adds_traceparent():
40
+ tracer, _ = _local_tracer()
41
+ with tracer.start_as_current_span("op"):
42
+ headers = inject_headers({})
43
+ assert "traceparent" in headers
44
+
45
+
46
+ def test_inject_headers_no_span_returns_empty():
47
+ headers = inject_headers({})
48
+ # No active span — propagator injects nothing (or a zero traceparent)
49
+ assert isinstance(headers, dict)
50
+
51
+
52
+ def test_get_tracer_returns_tracer():
53
+ tracer = get_tracer(__name__)
54
+ assert tracer is not None
55
+
56
+
57
+ def test_span_can_set_attributes():
58
+ tracer, exporter = _local_tracer()
59
+ with tracer.start_as_current_span("op") as span:
60
+ span.set_attribute("key", "value")
61
+ span.set_attribute("count", 42)
62
+
63
+ finished = exporter.get_finished_spans()
64
+ assert len(finished) == 1
65
+ assert finished[0].attributes["key"] == "value"
66
+ assert finished[0].attributes["count"] == 42