luma-observe-sdk 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.
- luma_observe_sdk-0.1.0/LICENSE +21 -0
- luma_observe_sdk-0.1.0/MANIFEST.in +3 -0
- luma_observe_sdk-0.1.0/PKG-INFO +328 -0
- luma_observe_sdk-0.1.0/README.md +305 -0
- luma_observe_sdk-0.1.0/docs/dashboards.md +117 -0
- luma_observe_sdk-0.1.0/docs/integration.zh-CN.md +257 -0
- luma_observe_sdk-0.1.0/docs/releasing.md +121 -0
- luma_observe_sdk-0.1.0/examples/dashboards/custom-metrics.config.json +49 -0
- luma_observe_sdk-0.1.0/examples/dashboards/tasks.config.json +124 -0
- luma_observe_sdk-0.1.0/pyproject.toml +44 -0
- luma_observe_sdk-0.1.0/setup.cfg +4 -0
- luma_observe_sdk-0.1.0/src/luma_observe/__init__.py +35 -0
- luma_observe_sdk-0.1.0/src/luma_observe/config.py +60 -0
- luma_observe_sdk-0.1.0/src/luma_observe/dashboards.py +241 -0
- luma_observe_sdk-0.1.0/src/luma_observe/instrumentation/__init__.py +157 -0
- luma_observe_sdk-0.1.0/src/luma_observe/instrumentation/celery.py +131 -0
- luma_observe_sdk-0.1.0/src/luma_observe/instrumentation/taskiq.py +205 -0
- luma_observe_sdk-0.1.0/src/luma_observe/process.py +122 -0
- luma_observe_sdk-0.1.0/src/luma_observe/py.typed +0 -0
- luma_observe_sdk-0.1.0/src/luma_observe/runtime.py +561 -0
- luma_observe_sdk-0.1.0/src/luma_observe_sdk.egg-info/PKG-INFO +328 -0
- luma_observe_sdk-0.1.0/src/luma_observe_sdk.egg-info/SOURCES.txt +30 -0
- luma_observe_sdk-0.1.0/src/luma_observe_sdk.egg-info/dependency_links.txt +1 -0
- luma_observe_sdk-0.1.0/src/luma_observe_sdk.egg-info/entry_points.txt +2 -0
- luma_observe_sdk-0.1.0/src/luma_observe_sdk.egg-info/requires.txt +15 -0
- luma_observe_sdk-0.1.0/src/luma_observe_sdk.egg-info/top_level.txt +1 -0
- luma_observe_sdk-0.1.0/tests/test_dashboards.py +91 -0
- luma_observe_sdk-0.1.0/tests/test_fork.py +279 -0
- luma_observe_sdk-0.1.0/tests/test_instrumentation.py +691 -0
- luma_observe_sdk-0.1.0/tests/test_propagation.py +259 -0
- luma_observe_sdk-0.1.0/tests/test_runtime.py +208 -0
- luma_observe_sdk-0.1.0/tests/test_task_runtime.py +365 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luma contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: luma-observe-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Optional OpenTelemetry application integration and Grafana configuration for Luma.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/LiuTianjie/luma
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.38
|
|
11
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.38
|
|
12
|
+
Provides-Extra: taskiq
|
|
13
|
+
Requires-Dist: taskiq<0.13,>=0.12.6; extra == "taskiq"
|
|
14
|
+
Provides-Extra: celery
|
|
15
|
+
Requires-Dist: celery<6,>=5.5; extra == "celery"
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest<10,>=8; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-asyncio<2,>=0.24; extra == "test"
|
|
19
|
+
Requires-Dist: build>=1.2; extra == "test"
|
|
20
|
+
Requires-Dist: twine>=6; extra == "test"
|
|
21
|
+
Requires-Dist: ruff>=0.6; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Luma Observe SDK
|
|
25
|
+
|
|
26
|
+
An optional Python integration package for the shared Luma observability stack.
|
|
27
|
+
It wraps official OpenTelemetry providers and OTLP/HTTP exporters, adds generic
|
|
28
|
+
task lifecycle adapters, and generates standard Grafana dashboard JSON from
|
|
29
|
+
application-owned configuration. It is versioned separately from `luma-infra`
|
|
30
|
+
and imports no Control, Django, business model or database code.
|
|
31
|
+
|
|
32
|
+
Python 3.10 or newer is required. See the [Chinese integration guide](docs/integration.zh-CN.md).
|
|
33
|
+
|
|
34
|
+
## Install the distribution
|
|
35
|
+
|
|
36
|
+
The package name is `luma-observe-sdk`, initial source version `0.1.0`.
|
|
37
|
+
`Observe SDK Checks` builds wheel/sdist artifacts independently of Control for
|
|
38
|
+
review and validation; expiring CI artifacts are not a permanent dependency source.
|
|
39
|
+
The independent SDK release workflow publishes checked wheel/sdist files and
|
|
40
|
+
`SHA256SUMS` to a permanent GitHub Release under `observe-sdk-v<version>`.
|
|
41
|
+
After publication, use the exact wheel URL with its SHA-256 from the release
|
|
42
|
+
notes, or pin an accessible, immutable Git commit containing the SDK. See the
|
|
43
|
+
[independent SDK release guide](docs/releasing.md) for dependency formats.
|
|
44
|
+
Uncommitted source has no usable remote SHA; placeholders must be replaced
|
|
45
|
+
with the published asset hash or reviewed, pushed commit.
|
|
46
|
+
|
|
47
|
+
To inspect a downloaded wheel, add only the framework extra used by that process:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
python -m pip install './luma_observe_sdk-0.1.0-py3-none-any.whl[taskiq]'
|
|
51
|
+
# Celery processes use the [celery] extra; processes needing both use [taskiq,celery].
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
PyPI publication is optional and disabled unless the repository variable
|
|
55
|
+
`OBSERVE_SDK_PYPI_ENABLED` is `true` and its Trusted Publisher is configured.
|
|
56
|
+
Once that exact version is published there, consumers can use
|
|
57
|
+
`luma-observe-sdk[taskiq]==0.1.0` with their normal package configuration.
|
|
58
|
+
The existing `v*` Luma release workflow publishes `luma-infra`, not this package.
|
|
59
|
+
|
|
60
|
+
## Configure once per process
|
|
61
|
+
|
|
62
|
+
Luma-managed applications keep the existing Control-injected `OTEL_*` endpoint,
|
|
63
|
+
bearer headers and resource attributes. Existing `luma secret` values stay in use.
|
|
64
|
+
For task metrics, set `OTEL_METRICS_EXPORTER=otlp` in the application deployment;
|
|
65
|
+
Control's automatic default remains `none`.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from luma_observe import configure, span, task
|
|
69
|
+
|
|
70
|
+
configure(
|
|
71
|
+
service_name="document-worker",
|
|
72
|
+
resource_attributes={"deployment.environment.name": "production"},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def convert(document):
|
|
76
|
+
with task("document.convert", framework="manual", queue="documents",
|
|
77
|
+
attributes={"document.format": "pdf"}) as run:
|
|
78
|
+
with span("document.render"):
|
|
79
|
+
result = render_document(document)
|
|
80
|
+
if result.needs_review:
|
|
81
|
+
run.outcome = "failed"
|
|
82
|
+
return result
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`task` records one attempt, elapsed time and the supplied outcome; it preserves
|
|
86
|
+
the original return and exception. An uncaught exception records its type and a
|
|
87
|
+
failed outcome. Use `succeeded`, `failed`, `retrying`, `skipped` or `cancelled`.
|
|
88
|
+
The application defines business completion separately from framework success.
|
|
89
|
+
Custom attributes belong to spans; task metrics use the fixed dimensions below.
|
|
90
|
+
|
|
91
|
+
Inside an adapter-observed task, use `current_task()` to annotate the current
|
|
92
|
+
attempt without creating another task span:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from luma_observe import current_task
|
|
96
|
+
|
|
97
|
+
def mark_review_required():
|
|
98
|
+
run = current_task()
|
|
99
|
+
if run is not None:
|
|
100
|
+
run.business_outcome = "requires_review"
|
|
101
|
+
run.business_status = "pending"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The two business fields accept application-defined strings up to 256 characters;
|
|
105
|
+
they appear as `task.business.outcome` and `task.business.status` in spans and
|
|
106
|
+
optional lifecycle logs, never as built-in metric labels. `set_outcome()` accepts
|
|
107
|
+
only the five standard outcomes above. A framework failure or an escaping
|
|
108
|
+
exception takes precedence over a successful business classification.
|
|
109
|
+
`current_task()` is local to the execution context, restores an outer task after a
|
|
110
|
+
nested task ends, and returns `None` outside a live task or in an inherited child
|
|
111
|
+
process until that child starts its own attempt.
|
|
112
|
+
|
|
113
|
+
`get_tracer()` and `get_meter()` return official OTel instruments from the SDK's
|
|
114
|
+
providers for additional application-defined spans and metrics. Keep custom metric
|
|
115
|
+
dimensions bounded; put execution IDs on spans or structured logs. Direct use of
|
|
116
|
+
official instrument APIs follows their normal semantics.
|
|
117
|
+
|
|
118
|
+
The `counter()` and `histogram()` helpers preserve the same metric API while
|
|
119
|
+
isolating instrumentation errors. Applications choose the metric names and fields:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from luma_observe import counter, histogram
|
|
123
|
+
|
|
124
|
+
completed = counter("document.completed")
|
|
125
|
+
size = histogram("document.pages", unit="1")
|
|
126
|
+
completed.add(1, {"document.format": "pdf", "outcome": "succeeded"})
|
|
127
|
+
size.record(12, {"document.format": "pdf"})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The top-level `counter()` and `histogram()` handles can be declared before
|
|
131
|
+
`configure()` and reused by forked workers; each write uses that process's current
|
|
132
|
+
runtime. Instruments obtained from an explicit `Runtime`, `get_meter()` or
|
|
133
|
+
`get_tracer()` belong to that runtime and process: create them during worker setup.
|
|
134
|
+
|
|
135
|
+
## Framework integration
|
|
136
|
+
|
|
137
|
+
Taskiq:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from luma_observe.instrumentation.taskiq import TaskiqMiddleware
|
|
141
|
+
|
|
142
|
+
broker.with_middlewares(TaskiqMiddleware(), existing_retry_middleware)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Place observation first so completion cleanup runs after the existing middleware
|
|
146
|
+
chain. Execution state stays in the callback context, so later middleware can
|
|
147
|
+
copy or rebuild messages. The carrier contains propagation fields and a private,
|
|
148
|
+
serializable invocation ID; live execution objects are not serialized.
|
|
149
|
+
Existing broker transport, retries, result persistence and ACK ownership
|
|
150
|
+
remain with Taskiq. Celery uses its public process/task signals:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from luma_observe.instrumentation.celery import instrument_celery
|
|
154
|
+
|
|
155
|
+
observation = instrument_celery()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Call this once during Celery app setup; `observation.uninstrument()` disconnects
|
|
159
|
+
the adapter after tasks drain. Providers initialize in the actual process.
|
|
160
|
+
Taskiq's default thread pool carries execution context; application functions
|
|
161
|
+
inside a custom process pool need their own initialization/propagation, while the
|
|
162
|
+
receiver's attempt can still be observed. Both adapters propagate standard W3C
|
|
163
|
+
context and baggage. Every framework execution starts a new trace with a Span Link
|
|
164
|
+
to its producer context, allowing the execution's completion to be sampled
|
|
165
|
+
independently of an earlier producer. Manual `task()` and `span()` retain their
|
|
166
|
+
normal parent semantics; explicit `context` and `links` are also supported.
|
|
167
|
+
The accompanying Collector source separates completed execution roots from early
|
|
168
|
+
child spans for tail sampling. This preserves the completed root summary, not
|
|
169
|
+
every child span in a long trace. Existing Collector deployments need that source
|
|
170
|
+
rebuilt and deployed before this sampling behavior is available; installing the
|
|
171
|
+
SDK alone does not update a running Collector.
|
|
172
|
+
The adapters create short producer preparation spans. Preparation
|
|
173
|
+
spans describe local message preparation, not broker receipt or send success.
|
|
174
|
+
Transport errors and reliable publication state remain the application's concern.
|
|
175
|
+
Use only one task adapter per framework; avoid instrumenting the same task twice
|
|
176
|
+
with another OTel adapter.
|
|
177
|
+
|
|
178
|
+
Adapters observe attempts that reach execution hooks. Revocation before execution
|
|
179
|
+
and hard process termination can leave no completed span. Taskiq's standard
|
|
180
|
+
SmartRetry `NoResultError` conversion identifies a retrying attempt; configurations
|
|
181
|
+
that keep the original error (`no_result_on_retry=False`) expose a failed attempt
|
|
182
|
+
without a definitive retry result. Queue state and business delivery evidence
|
|
183
|
+
remain separate from these attempt metrics.
|
|
184
|
+
|
|
185
|
+
With nested inline Taskiq in-memory execution, a child middleware error caught by
|
|
186
|
+
the parent can leave the child observation context active until the next SDK hook
|
|
187
|
+
restores the parent. Do not use that interim context as proof of parent progress.
|
|
188
|
+
Hard process termination still cannot run completion hooks.
|
|
189
|
+
|
|
190
|
+
## Disable and bound telemetry
|
|
191
|
+
|
|
192
|
+
`configure(enabled=False)`, `LUMA_OBSERVE_ENABLED=false` or
|
|
193
|
+
`OTEL_SDK_DISABLED=true` disables the complete SDK, including optional lifecycle
|
|
194
|
+
logging. With no OTLP endpoint configured, providers and background threads are
|
|
195
|
+
not started. Endpoint/header/protocol/sampler settings use the official `OTEL_*`
|
|
196
|
+
environment contract. This package exports `http/protobuf`.
|
|
197
|
+
Producers without a local endpoint still timestamp publication for worker queue
|
|
198
|
+
wait measurements and preserve carried context; explicit disablement leaves the
|
|
199
|
+
carrier untouched. `telemetry_disabled()` reads those explicit switches without
|
|
200
|
+
initializing a runtime.
|
|
201
|
+
|
|
202
|
+
Inspect `get_runtime().enabled` to check whether the runtime initialized.
|
|
203
|
+
`get_runtime().initialization_error` contains only the initialization exception's
|
|
204
|
+
class name when initialization fails; the runtime stays disabled. It does not
|
|
205
|
+
log configuration or exception contents on the business call path.
|
|
206
|
+
|
|
207
|
+
Defaults are a 512-span queue, batches of 64, a five-second trace export interval,
|
|
208
|
+
a 30-second metric interval, a 1.5-second HTTP export budget and a two-second
|
|
209
|
+
shutdown budget. Lifecycle JSON logging is opt-in with `lifecycle_logs=True` and
|
|
210
|
+
uses a bounded background queue. It writes INFO records to `luma_observe.events`
|
|
211
|
+
using application-configured logging handlers. Route that logger to stdout for
|
|
212
|
+
the existing Nomad shipper; a standalone process can configure it at startup:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
import logging
|
|
216
|
+
import sys
|
|
217
|
+
from luma_observe import configure
|
|
218
|
+
|
|
219
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s")
|
|
220
|
+
configure(lifecycle_logs=True)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Existing applications should use their current logging configuration for this
|
|
224
|
+
logger. The SDK does not create another log store or collector.
|
|
225
|
+
|
|
226
|
+
Call `shutdown()` at the application's final process shutdown; atexit is also
|
|
227
|
+
registered. Celery uses its process shutdown signals. Closing one Taskiq broker
|
|
228
|
+
calls bounded `force_flush()` and does not stop telemetry shared by other brokers.
|
|
229
|
+
Applications may also call `force_flush(timeout=2)` before a known worker exit;
|
|
230
|
+
it returns whether the queued flush completed within its budget and never starts
|
|
231
|
+
a missing runtime. It does not close providers or prove downstream persistence.
|
|
232
|
+
Export failure can discard telemetry;
|
|
233
|
+
it does not make task execution wait for a collector response. Fault and load
|
|
234
|
+
acceptance still belongs to each application's integration rollout.
|
|
235
|
+
|
|
236
|
+
## Metrics and dashboards
|
|
237
|
+
|
|
238
|
+
The shared Collector's Prometheus conversion exposes:
|
|
239
|
+
|
|
240
|
+
| Metric | Meaning |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `task_executions_total` | Completed attempts by `outcome` |
|
|
243
|
+
| `task_duration_seconds_bucket` | Attempt duration histogram |
|
|
244
|
+
| `task_queue_wait_seconds_bucket` | Known enqueue-to-start duration histogram |
|
|
245
|
+
| `task_active` | Active attempts in the process |
|
|
246
|
+
| `worker_heartbeat_seconds` | Exporter process collection time as Unix seconds |
|
|
247
|
+
| `task_oldest_active_age_seconds` | Oldest active attempt age by task name, queue and framework |
|
|
248
|
+
| `task_active_tracking_overflow` | Active attempts beyond the bounded age tracker |
|
|
249
|
+
|
|
250
|
+
Task metric labels are `task_name`, `task_queue` and `task_framework`, plus outcome
|
|
251
|
+
for executions. Standard resource attributes supply `service_name`,
|
|
252
|
+
`deployment_environment_name` and the existing Luma resource dimensions.
|
|
253
|
+
Counters describe collected telemetry, not a financial or exact delivery ledger.
|
|
254
|
+
Keep process liveness and metric staleness in mind when aggregating active tasks.
|
|
255
|
+
Heartbeat and overflow are process-level gauges. Oldest age uses the same bounded
|
|
256
|
+
task dimensions as `task_active`: `task_name`, `task_queue` and `task_framework`.
|
|
257
|
+
It reports the oldest currently active attempt in each group, using a monotonic
|
|
258
|
+
clock. This identifies a long-running task type; it does not measure time since
|
|
259
|
+
the application's last progress event. Choose age thresholds for each task type.
|
|
260
|
+
|
|
261
|
+
Tracking is capped at 8192 active attempts per process and keeps no historical
|
|
262
|
+
task-key cache. Idle groups have no new age sample; overflow also suppresses age
|
|
263
|
+
samples. The example dashboard joins each age to `task_active > 0` with the same
|
|
264
|
+
process identity and all three task labels, then filters fresh heartbeat and zero
|
|
265
|
+
overflow. This excludes old age samples retained in the store's query lookback.
|
|
266
|
+
Only a fresh heartbeat plus an explicitly zero active count establishes local
|
|
267
|
+
idle; a missing, stale or overflowed age sample does not.
|
|
268
|
+
|
|
269
|
+
Heartbeat freshness shows that the process's export path is alive, not that its
|
|
270
|
+
event loop or business work is progressing. Local idle does not establish that a
|
|
271
|
+
broker queue is healthy or empty. Queue backlog and oldest unconsumed message age
|
|
272
|
+
remain application/broker-provided metrics.
|
|
273
|
+
|
|
274
|
+
Process instance identities remain distinct so simultaneous cumulative writers
|
|
275
|
+
do not overwrite one series. Worker recycling therefore creates historical
|
|
276
|
+
series; measure the workload's process churn, task dimensions and storage budget
|
|
277
|
+
before production acceptance. The protocol check records a synthetic multi-process
|
|
278
|
+
series inventory, not a production capacity result. See the
|
|
279
|
+
[dashboard guide](docs/dashboards.md) for freshness settings and query semantics.
|
|
280
|
+
|
|
281
|
+
Copy an example from [`examples/dashboards/`](examples/dashboards/), select your
|
|
282
|
+
service/metrics, and render it offline:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
luma-observe-dashboard application.json --output application-dashboard.json
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The output is ordinary Grafana JSON. Install the reviewed output under the existing
|
|
289
|
+
`/srv/luma/data/luma-observe/extensions/dashboards/` directory using the current
|
|
290
|
+
operator file-delivery workflow. Grafana discovers it in Applications within the
|
|
291
|
+
next 30-second scan. Each application owns its dashboard UID and query expressions.
|
|
292
|
+
Custom fields and panels do not require SDK or Control code changes.
|
|
293
|
+
|
|
294
|
+
Prometheus/vmalert rule YAML still goes under `extensions/rules/`; validate it with
|
|
295
|
+
the existing pinned vmalert binary and use existing Alertmanager routes. See the
|
|
296
|
+
[standard platform integration](../../docs/observe-applications.md) for scopes,
|
|
297
|
+
read APIs, authentication and extension delivery.
|
|
298
|
+
|
|
299
|
+
## Validate and build
|
|
300
|
+
|
|
301
|
+
From the Luma repository root, in an isolated Python environment:
|
|
302
|
+
|
|
303
|
+
```sh
|
|
304
|
+
python -m pip install -e './sdk/python[taskiq,celery,test]'
|
|
305
|
+
bash scripts/check-observe-sdk.sh --output-dir /tmp/luma-observe-sdk-dist
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The script runs SDK checks, builds wheel/sdist in a temporary source copy, validates
|
|
309
|
+
metadata and imports the installed wheel. It removes its temporary build/install
|
|
310
|
+
directories and retains only the explicitly requested distributions. It does not
|
|
311
|
+
contact production, deploy the observability stack or publish a package.
|
|
312
|
+
CI covers Python 3.10 with the minimum supported OTel 1.38.0 and Python 3.12
|
|
313
|
+
with current dependencies.
|
|
314
|
+
|
|
315
|
+
With Docker available, run the separate SDK protocol check:
|
|
316
|
+
|
|
317
|
+
```sh
|
|
318
|
+
python -m pip install -e .
|
|
319
|
+
python scripts/check-observe-sdk-protocols.py --platform linux/amd64
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
This CI job installs the SDK in an isolated container, exports native task/custom
|
|
323
|
+
metrics through the authenticated Collector to VictoriaMetrics, then provisions
|
|
324
|
+
the generated Grafana examples and queries their PromQL expressions against that
|
|
325
|
+
data. It uses synthetic credentials, no host ports, and removes its own containers,
|
|
326
|
+
volumes and temporary image tags. The existing platform protocol checks cover
|
|
327
|
+
the other shared ingestion/query paths. Application throughput, lifecycle coverage
|
|
328
|
+
and production data freshness still require integration acceptance after deployment.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# Luma Observe SDK
|
|
2
|
+
|
|
3
|
+
An optional Python integration package for the shared Luma observability stack.
|
|
4
|
+
It wraps official OpenTelemetry providers and OTLP/HTTP exporters, adds generic
|
|
5
|
+
task lifecycle adapters, and generates standard Grafana dashboard JSON from
|
|
6
|
+
application-owned configuration. It is versioned separately from `luma-infra`
|
|
7
|
+
and imports no Control, Django, business model or database code.
|
|
8
|
+
|
|
9
|
+
Python 3.10 or newer is required. See the [Chinese integration guide](docs/integration.zh-CN.md).
|
|
10
|
+
|
|
11
|
+
## Install the distribution
|
|
12
|
+
|
|
13
|
+
The package name is `luma-observe-sdk`, initial source version `0.1.0`.
|
|
14
|
+
`Observe SDK Checks` builds wheel/sdist artifacts independently of Control for
|
|
15
|
+
review and validation; expiring CI artifacts are not a permanent dependency source.
|
|
16
|
+
The independent SDK release workflow publishes checked wheel/sdist files and
|
|
17
|
+
`SHA256SUMS` to a permanent GitHub Release under `observe-sdk-v<version>`.
|
|
18
|
+
After publication, use the exact wheel URL with its SHA-256 from the release
|
|
19
|
+
notes, or pin an accessible, immutable Git commit containing the SDK. See the
|
|
20
|
+
[independent SDK release guide](docs/releasing.md) for dependency formats.
|
|
21
|
+
Uncommitted source has no usable remote SHA; placeholders must be replaced
|
|
22
|
+
with the published asset hash or reviewed, pushed commit.
|
|
23
|
+
|
|
24
|
+
To inspect a downloaded wheel, add only the framework extra used by that process:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
python -m pip install './luma_observe_sdk-0.1.0-py3-none-any.whl[taskiq]'
|
|
28
|
+
# Celery processes use the [celery] extra; processes needing both use [taskiq,celery].
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
PyPI publication is optional and disabled unless the repository variable
|
|
32
|
+
`OBSERVE_SDK_PYPI_ENABLED` is `true` and its Trusted Publisher is configured.
|
|
33
|
+
Once that exact version is published there, consumers can use
|
|
34
|
+
`luma-observe-sdk[taskiq]==0.1.0` with their normal package configuration.
|
|
35
|
+
The existing `v*` Luma release workflow publishes `luma-infra`, not this package.
|
|
36
|
+
|
|
37
|
+
## Configure once per process
|
|
38
|
+
|
|
39
|
+
Luma-managed applications keep the existing Control-injected `OTEL_*` endpoint,
|
|
40
|
+
bearer headers and resource attributes. Existing `luma secret` values stay in use.
|
|
41
|
+
For task metrics, set `OTEL_METRICS_EXPORTER=otlp` in the application deployment;
|
|
42
|
+
Control's automatic default remains `none`.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from luma_observe import configure, span, task
|
|
46
|
+
|
|
47
|
+
configure(
|
|
48
|
+
service_name="document-worker",
|
|
49
|
+
resource_attributes={"deployment.environment.name": "production"},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def convert(document):
|
|
53
|
+
with task("document.convert", framework="manual", queue="documents",
|
|
54
|
+
attributes={"document.format": "pdf"}) as run:
|
|
55
|
+
with span("document.render"):
|
|
56
|
+
result = render_document(document)
|
|
57
|
+
if result.needs_review:
|
|
58
|
+
run.outcome = "failed"
|
|
59
|
+
return result
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`task` records one attempt, elapsed time and the supplied outcome; it preserves
|
|
63
|
+
the original return and exception. An uncaught exception records its type and a
|
|
64
|
+
failed outcome. Use `succeeded`, `failed`, `retrying`, `skipped` or `cancelled`.
|
|
65
|
+
The application defines business completion separately from framework success.
|
|
66
|
+
Custom attributes belong to spans; task metrics use the fixed dimensions below.
|
|
67
|
+
|
|
68
|
+
Inside an adapter-observed task, use `current_task()` to annotate the current
|
|
69
|
+
attempt without creating another task span:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from luma_observe import current_task
|
|
73
|
+
|
|
74
|
+
def mark_review_required():
|
|
75
|
+
run = current_task()
|
|
76
|
+
if run is not None:
|
|
77
|
+
run.business_outcome = "requires_review"
|
|
78
|
+
run.business_status = "pending"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The two business fields accept application-defined strings up to 256 characters;
|
|
82
|
+
they appear as `task.business.outcome` and `task.business.status` in spans and
|
|
83
|
+
optional lifecycle logs, never as built-in metric labels. `set_outcome()` accepts
|
|
84
|
+
only the five standard outcomes above. A framework failure or an escaping
|
|
85
|
+
exception takes precedence over a successful business classification.
|
|
86
|
+
`current_task()` is local to the execution context, restores an outer task after a
|
|
87
|
+
nested task ends, and returns `None` outside a live task or in an inherited child
|
|
88
|
+
process until that child starts its own attempt.
|
|
89
|
+
|
|
90
|
+
`get_tracer()` and `get_meter()` return official OTel instruments from the SDK's
|
|
91
|
+
providers for additional application-defined spans and metrics. Keep custom metric
|
|
92
|
+
dimensions bounded; put execution IDs on spans or structured logs. Direct use of
|
|
93
|
+
official instrument APIs follows their normal semantics.
|
|
94
|
+
|
|
95
|
+
The `counter()` and `histogram()` helpers preserve the same metric API while
|
|
96
|
+
isolating instrumentation errors. Applications choose the metric names and fields:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from luma_observe import counter, histogram
|
|
100
|
+
|
|
101
|
+
completed = counter("document.completed")
|
|
102
|
+
size = histogram("document.pages", unit="1")
|
|
103
|
+
completed.add(1, {"document.format": "pdf", "outcome": "succeeded"})
|
|
104
|
+
size.record(12, {"document.format": "pdf"})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The top-level `counter()` and `histogram()` handles can be declared before
|
|
108
|
+
`configure()` and reused by forked workers; each write uses that process's current
|
|
109
|
+
runtime. Instruments obtained from an explicit `Runtime`, `get_meter()` or
|
|
110
|
+
`get_tracer()` belong to that runtime and process: create them during worker setup.
|
|
111
|
+
|
|
112
|
+
## Framework integration
|
|
113
|
+
|
|
114
|
+
Taskiq:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from luma_observe.instrumentation.taskiq import TaskiqMiddleware
|
|
118
|
+
|
|
119
|
+
broker.with_middlewares(TaskiqMiddleware(), existing_retry_middleware)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Place observation first so completion cleanup runs after the existing middleware
|
|
123
|
+
chain. Execution state stays in the callback context, so later middleware can
|
|
124
|
+
copy or rebuild messages. The carrier contains propagation fields and a private,
|
|
125
|
+
serializable invocation ID; live execution objects are not serialized.
|
|
126
|
+
Existing broker transport, retries, result persistence and ACK ownership
|
|
127
|
+
remain with Taskiq. Celery uses its public process/task signals:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from luma_observe.instrumentation.celery import instrument_celery
|
|
131
|
+
|
|
132
|
+
observation = instrument_celery()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Call this once during Celery app setup; `observation.uninstrument()` disconnects
|
|
136
|
+
the adapter after tasks drain. Providers initialize in the actual process.
|
|
137
|
+
Taskiq's default thread pool carries execution context; application functions
|
|
138
|
+
inside a custom process pool need their own initialization/propagation, while the
|
|
139
|
+
receiver's attempt can still be observed. Both adapters propagate standard W3C
|
|
140
|
+
context and baggage. Every framework execution starts a new trace with a Span Link
|
|
141
|
+
to its producer context, allowing the execution's completion to be sampled
|
|
142
|
+
independently of an earlier producer. Manual `task()` and `span()` retain their
|
|
143
|
+
normal parent semantics; explicit `context` and `links` are also supported.
|
|
144
|
+
The accompanying Collector source separates completed execution roots from early
|
|
145
|
+
child spans for tail sampling. This preserves the completed root summary, not
|
|
146
|
+
every child span in a long trace. Existing Collector deployments need that source
|
|
147
|
+
rebuilt and deployed before this sampling behavior is available; installing the
|
|
148
|
+
SDK alone does not update a running Collector.
|
|
149
|
+
The adapters create short producer preparation spans. Preparation
|
|
150
|
+
spans describe local message preparation, not broker receipt or send success.
|
|
151
|
+
Transport errors and reliable publication state remain the application's concern.
|
|
152
|
+
Use only one task adapter per framework; avoid instrumenting the same task twice
|
|
153
|
+
with another OTel adapter.
|
|
154
|
+
|
|
155
|
+
Adapters observe attempts that reach execution hooks. Revocation before execution
|
|
156
|
+
and hard process termination can leave no completed span. Taskiq's standard
|
|
157
|
+
SmartRetry `NoResultError` conversion identifies a retrying attempt; configurations
|
|
158
|
+
that keep the original error (`no_result_on_retry=False`) expose a failed attempt
|
|
159
|
+
without a definitive retry result. Queue state and business delivery evidence
|
|
160
|
+
remain separate from these attempt metrics.
|
|
161
|
+
|
|
162
|
+
With nested inline Taskiq in-memory execution, a child middleware error caught by
|
|
163
|
+
the parent can leave the child observation context active until the next SDK hook
|
|
164
|
+
restores the parent. Do not use that interim context as proof of parent progress.
|
|
165
|
+
Hard process termination still cannot run completion hooks.
|
|
166
|
+
|
|
167
|
+
## Disable and bound telemetry
|
|
168
|
+
|
|
169
|
+
`configure(enabled=False)`, `LUMA_OBSERVE_ENABLED=false` or
|
|
170
|
+
`OTEL_SDK_DISABLED=true` disables the complete SDK, including optional lifecycle
|
|
171
|
+
logging. With no OTLP endpoint configured, providers and background threads are
|
|
172
|
+
not started. Endpoint/header/protocol/sampler settings use the official `OTEL_*`
|
|
173
|
+
environment contract. This package exports `http/protobuf`.
|
|
174
|
+
Producers without a local endpoint still timestamp publication for worker queue
|
|
175
|
+
wait measurements and preserve carried context; explicit disablement leaves the
|
|
176
|
+
carrier untouched. `telemetry_disabled()` reads those explicit switches without
|
|
177
|
+
initializing a runtime.
|
|
178
|
+
|
|
179
|
+
Inspect `get_runtime().enabled` to check whether the runtime initialized.
|
|
180
|
+
`get_runtime().initialization_error` contains only the initialization exception's
|
|
181
|
+
class name when initialization fails; the runtime stays disabled. It does not
|
|
182
|
+
log configuration or exception contents on the business call path.
|
|
183
|
+
|
|
184
|
+
Defaults are a 512-span queue, batches of 64, a five-second trace export interval,
|
|
185
|
+
a 30-second metric interval, a 1.5-second HTTP export budget and a two-second
|
|
186
|
+
shutdown budget. Lifecycle JSON logging is opt-in with `lifecycle_logs=True` and
|
|
187
|
+
uses a bounded background queue. It writes INFO records to `luma_observe.events`
|
|
188
|
+
using application-configured logging handlers. Route that logger to stdout for
|
|
189
|
+
the existing Nomad shipper; a standalone process can configure it at startup:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
import logging
|
|
193
|
+
import sys
|
|
194
|
+
from luma_observe import configure
|
|
195
|
+
|
|
196
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s")
|
|
197
|
+
configure(lifecycle_logs=True)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Existing applications should use their current logging configuration for this
|
|
201
|
+
logger. The SDK does not create another log store or collector.
|
|
202
|
+
|
|
203
|
+
Call `shutdown()` at the application's final process shutdown; atexit is also
|
|
204
|
+
registered. Celery uses its process shutdown signals. Closing one Taskiq broker
|
|
205
|
+
calls bounded `force_flush()` and does not stop telemetry shared by other brokers.
|
|
206
|
+
Applications may also call `force_flush(timeout=2)` before a known worker exit;
|
|
207
|
+
it returns whether the queued flush completed within its budget and never starts
|
|
208
|
+
a missing runtime. It does not close providers or prove downstream persistence.
|
|
209
|
+
Export failure can discard telemetry;
|
|
210
|
+
it does not make task execution wait for a collector response. Fault and load
|
|
211
|
+
acceptance still belongs to each application's integration rollout.
|
|
212
|
+
|
|
213
|
+
## Metrics and dashboards
|
|
214
|
+
|
|
215
|
+
The shared Collector's Prometheus conversion exposes:
|
|
216
|
+
|
|
217
|
+
| Metric | Meaning |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `task_executions_total` | Completed attempts by `outcome` |
|
|
220
|
+
| `task_duration_seconds_bucket` | Attempt duration histogram |
|
|
221
|
+
| `task_queue_wait_seconds_bucket` | Known enqueue-to-start duration histogram |
|
|
222
|
+
| `task_active` | Active attempts in the process |
|
|
223
|
+
| `worker_heartbeat_seconds` | Exporter process collection time as Unix seconds |
|
|
224
|
+
| `task_oldest_active_age_seconds` | Oldest active attempt age by task name, queue and framework |
|
|
225
|
+
| `task_active_tracking_overflow` | Active attempts beyond the bounded age tracker |
|
|
226
|
+
|
|
227
|
+
Task metric labels are `task_name`, `task_queue` and `task_framework`, plus outcome
|
|
228
|
+
for executions. Standard resource attributes supply `service_name`,
|
|
229
|
+
`deployment_environment_name` and the existing Luma resource dimensions.
|
|
230
|
+
Counters describe collected telemetry, not a financial or exact delivery ledger.
|
|
231
|
+
Keep process liveness and metric staleness in mind when aggregating active tasks.
|
|
232
|
+
Heartbeat and overflow are process-level gauges. Oldest age uses the same bounded
|
|
233
|
+
task dimensions as `task_active`: `task_name`, `task_queue` and `task_framework`.
|
|
234
|
+
It reports the oldest currently active attempt in each group, using a monotonic
|
|
235
|
+
clock. This identifies a long-running task type; it does not measure time since
|
|
236
|
+
the application's last progress event. Choose age thresholds for each task type.
|
|
237
|
+
|
|
238
|
+
Tracking is capped at 8192 active attempts per process and keeps no historical
|
|
239
|
+
task-key cache. Idle groups have no new age sample; overflow also suppresses age
|
|
240
|
+
samples. The example dashboard joins each age to `task_active > 0` with the same
|
|
241
|
+
process identity and all three task labels, then filters fresh heartbeat and zero
|
|
242
|
+
overflow. This excludes old age samples retained in the store's query lookback.
|
|
243
|
+
Only a fresh heartbeat plus an explicitly zero active count establishes local
|
|
244
|
+
idle; a missing, stale or overflowed age sample does not.
|
|
245
|
+
|
|
246
|
+
Heartbeat freshness shows that the process's export path is alive, not that its
|
|
247
|
+
event loop or business work is progressing. Local idle does not establish that a
|
|
248
|
+
broker queue is healthy or empty. Queue backlog and oldest unconsumed message age
|
|
249
|
+
remain application/broker-provided metrics.
|
|
250
|
+
|
|
251
|
+
Process instance identities remain distinct so simultaneous cumulative writers
|
|
252
|
+
do not overwrite one series. Worker recycling therefore creates historical
|
|
253
|
+
series; measure the workload's process churn, task dimensions and storage budget
|
|
254
|
+
before production acceptance. The protocol check records a synthetic multi-process
|
|
255
|
+
series inventory, not a production capacity result. See the
|
|
256
|
+
[dashboard guide](docs/dashboards.md) for freshness settings and query semantics.
|
|
257
|
+
|
|
258
|
+
Copy an example from [`examples/dashboards/`](examples/dashboards/), select your
|
|
259
|
+
service/metrics, and render it offline:
|
|
260
|
+
|
|
261
|
+
```sh
|
|
262
|
+
luma-observe-dashboard application.json --output application-dashboard.json
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The output is ordinary Grafana JSON. Install the reviewed output under the existing
|
|
266
|
+
`/srv/luma/data/luma-observe/extensions/dashboards/` directory using the current
|
|
267
|
+
operator file-delivery workflow. Grafana discovers it in Applications within the
|
|
268
|
+
next 30-second scan. Each application owns its dashboard UID and query expressions.
|
|
269
|
+
Custom fields and panels do not require SDK or Control code changes.
|
|
270
|
+
|
|
271
|
+
Prometheus/vmalert rule YAML still goes under `extensions/rules/`; validate it with
|
|
272
|
+
the existing pinned vmalert binary and use existing Alertmanager routes. See the
|
|
273
|
+
[standard platform integration](../../docs/observe-applications.md) for scopes,
|
|
274
|
+
read APIs, authentication and extension delivery.
|
|
275
|
+
|
|
276
|
+
## Validate and build
|
|
277
|
+
|
|
278
|
+
From the Luma repository root, in an isolated Python environment:
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
python -m pip install -e './sdk/python[taskiq,celery,test]'
|
|
282
|
+
bash scripts/check-observe-sdk.sh --output-dir /tmp/luma-observe-sdk-dist
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The script runs SDK checks, builds wheel/sdist in a temporary source copy, validates
|
|
286
|
+
metadata and imports the installed wheel. It removes its temporary build/install
|
|
287
|
+
directories and retains only the explicitly requested distributions. It does not
|
|
288
|
+
contact production, deploy the observability stack or publish a package.
|
|
289
|
+
CI covers Python 3.10 with the minimum supported OTel 1.38.0 and Python 3.12
|
|
290
|
+
with current dependencies.
|
|
291
|
+
|
|
292
|
+
With Docker available, run the separate SDK protocol check:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
python -m pip install -e .
|
|
296
|
+
python scripts/check-observe-sdk-protocols.py --platform linux/amd64
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This CI job installs the SDK in an isolated container, exports native task/custom
|
|
300
|
+
metrics through the authenticated Collector to VictoriaMetrics, then provisions
|
|
301
|
+
the generated Grafana examples and queries their PromQL expressions against that
|
|
302
|
+
data. It uses synthetic credentials, no host ports, and removes its own containers,
|
|
303
|
+
volumes and temporary image tags. The existing platform protocol checks cover
|
|
304
|
+
the other shared ingestion/query paths. Application throughput, lifecycle coverage
|
|
305
|
+
and production data freshness still require integration acceptance after deployment.
|