Skip to content

Commit cdf862a

Browse files
authored
feat: Add OpenTelemetry instrumentation support (#67)
Introduce OpenTelemetry instrumentation for distributed tracing, enabling monitoring of job processing and enqueue operations. Usage is auto-enabled when the Litestar OpenTelemetry Plugin is found.
1 parent 62e09bc commit cdf862a

14 files changed

Lines changed: 1004 additions & 96 deletions

README.md

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,28 @@
66
pip install litestar-saq
77
```
88

9+
For OpenTelemetry support:
10+
11+
```shell
12+
pip install litestar-saq[otel]
13+
```
14+
915
## Usage
1016

1117
Here is a basic application that demonstrates how to use the plugin.
1218

1319
```python
14-
from __future__ import annotations
15-
1620
from litestar import Litestar
17-
1821
from litestar_saq import QueueConfig, SAQConfig, SAQPlugin
1922

20-
saq = SAQPlugin(config=SAQConfig(use_server_lifespan=True, queue_configs=[QueueConfig(name="samples", dsn="redis://localhost:6379/0")]))
23+
saq = SAQPlugin(
24+
config=SAQConfig(
25+
use_server_lifespan=True,
26+
queue_configs=[
27+
QueueConfig(name="samples", dsn="redis://localhost:6379/0")
28+
],
29+
)
30+
)
2131
app = Litestar(plugins=[saq])
2232
```
2333

@@ -47,16 +57,59 @@ If you are starting the process for only specific queues and still want to read
4757
import os
4858
from saq import Queue
4959

50-
5160
def get_queue_directly(queue_name: str, redis_url: str) -> Queue:
5261
return Queue.from_url(redis_url, name=queue_name)
5362

5463
redis_url = os.getenv("REDIS_URL")
5564
queue = get_queue_directly("queue-in-other-process", redis_url)
65+
5666
# Get queue info
5767
info = await queue.info(jobs=True)
68+
5869
# Enqueue new task
59-
queue.enqueue(
60-
....
70+
await queue.enqueue("task_name", arg1="value1")
71+
```
72+
73+
## Monitored Jobs
74+
75+
For long-running tasks, use the `monitored_job` decorator to automatically send heartbeats and prevent SAQ from marking jobs as stuck:
76+
77+
```python
78+
from litestar_saq import monitored_job
79+
80+
@monitored_job() # Auto-calculates interval from job.heartbeat
81+
async def long_running_task(ctx):
82+
await process_large_dataset()
83+
return {"status": "complete"}
84+
85+
@monitored_job(interval=30.0) # Explicit 30-second interval
86+
async def train_model(ctx, model_id: str):
87+
for epoch in range(100):
88+
await train_epoch(model)
89+
return {"model_id": model_id}
90+
```
91+
92+
## OpenTelemetry Integration
93+
94+
litestar-saq supports optional OpenTelemetry instrumentation for distributed tracing.
95+
96+
### Configuration
97+
98+
```python
99+
from litestar_saq import SAQConfig, QueueConfig
100+
101+
config = SAQConfig(
102+
queue_configs=[QueueConfig(dsn="redis://localhost:6379/0")],
103+
enable_otel=None, # Auto-detect (default) - enabled if OTEL installed AND Litestar OpenTelemetryPlugin is active
104+
# enable_otel=True, # Force enable (raises error if not installed)
105+
# enable_otel=False, # Force disable
61106
)
62107
```
108+
109+
When enabled, the plugin creates:
110+
111+
- **CONSUMER spans** for job processing
112+
- **PRODUCER spans** for job enqueue operations
113+
- **Automatic context propagation** across process boundaries
114+
115+
Spans follow [OpenTelemetry messaging semantic conventions](https://opentelemetry.io/docs/specs/semconv/messaging/) with `messaging.system = "saq"`.

examples/basic.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
class SampleController(Controller):
1717
@get(path="/samples")
1818
async def samples_queue_info(self, task_queues: TaskQueues) -> QueueInfo:
19+
"""Get information about the samples queue.
20+
21+
Returns:
22+
Queue information including pending jobs and workers.
23+
"""
1924
queue = task_queues.get("samples")
2025
return await queue.info()
2126

examples/hooks.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ async def example_task(_ctx: Context, *, name: str) -> dict[str, str]:
3737
3838
This task demonstrates a simple async operation with timing hooks
3939
that will automatically log execution duration.
40+
41+
Returns:
42+
Dictionary with greeting message.
4043
"""
4144
await asyncio.sleep(0.5) # Simulate work
4245
return {"message": f"Hello, {name}!"}
@@ -48,13 +51,20 @@ async def slow_task(_ctx: Context, *, duration: float = 2.0) -> dict[str, float]
4851
Args:
4952
_ctx: SAQ context.
5053
duration: How long to sleep (default 2 seconds).
54+
55+
Returns:
56+
Dictionary with the sleep duration.
5157
"""
5258
await asyncio.sleep(duration)
5359
return {"slept_for": duration}
5460

5561

5662
async def scheduled_task(_ctx: Context) -> dict[str, str]:
57-
"""Example scheduled task that runs on cron."""
63+
"""Example scheduled task that runs on cron.
64+
65+
Returns:
66+
Dictionary with execution status.
67+
"""
5868
return {"status": "scheduled task executed"}
5969

6070

@@ -63,7 +73,11 @@ class TaskController(Controller):
6373

6474
@get(path="/queue-info")
6575
async def get_queue_info(self, task_queues: TaskQueues) -> QueueInfo:
66-
"""Get information about the task queue."""
76+
"""Get information about the task queue.
77+
78+
Returns:
79+
Queue information including pending jobs and workers.
80+
"""
6781
queue = task_queues.get("default")
6882
return await queue.info()
6983

examples/postgres.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ async def system_task(_: Context) -> None:
4242
class SampleController(Controller):
4343
@get(path="/samples")
4444
async def samples_queue_info(self, task_queues: TaskQueues) -> QueueInfo:
45+
"""Get information about the samples queue.
46+
47+
Returns:
48+
Queue information including pending jobs and workers.
49+
"""
4550
queue = task_queues.get("samples")
4651
return await queue.info()
4752

litestar_saq/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
timing_before_process,
1313
)
1414
from litestar_saq.plugin import SAQPlugin
15+
from litestar_saq.typing import OPENTELEMETRY_INSTALLED
1516

1617
__all__ = (
18+
# OpenTelemetry
19+
"OPENTELEMETRY_INSTALLED",
1720
"CronJob",
1821
"Job",
1922
"PostgresQueueOptions",

0 commit comments

Comments
 (0)