watchdock-errors 0.3.0__tar.gz → 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.
- {watchdock_errors-0.3.0/src/watchdock_errors.egg-info → watchdock_errors-0.4.0}/PKG-INFO +25 -1
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/README.md +22 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/pyproject.toml +1 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/__init__.py +10 -1
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/config.py +10 -6
- watchdock_errors-0.4.0/src/watchdock_errors/integrations/celery.py +280 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0/src/watchdock_errors.egg-info}/PKG-INFO +25 -1
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/SOURCES.txt +1 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/requires.txt +3 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/scm_file_list.json +9 -8
- watchdock_errors-0.4.0/src/watchdock_errors.egg-info/scm_version.json +8 -0
- watchdock_errors-0.3.0/src/watchdock_errors.egg-info/scm_version.json +0 -8
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/.github/workflows/ci.yml +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/.github/workflows/release.yml +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/.gitignore +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/setup.cfg +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/client.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/event.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/__init__.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/django.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/fastapi.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/utils.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/dependency_links.txt +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/top_level.txt +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/tests/test_client.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/tests/test_event.py +0 -0
- {watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: watchdock-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Application error tracking SDK for the Watchdock platform
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: error-tracking,observability,watchdock
|
|
@@ -12,6 +12,8 @@ Requires-Dist: django>=3.2; extra == "django"
|
|
|
12
12
|
Provides-Extra: fastapi
|
|
13
13
|
Requires-Dist: fastapi>=0.95; extra == "fastapi"
|
|
14
14
|
Requires-Dist: starlette>=0.27; extra == "fastapi"
|
|
15
|
+
Provides-Extra: celery
|
|
16
|
+
Requires-Dist: celery>=5.0; extra == "celery"
|
|
15
17
|
Provides-Extra: dev
|
|
16
18
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
19
|
Requires-Dist: pytest-mock>=3.0; extra == "dev"
|
|
@@ -33,6 +35,7 @@ With framework extras:
|
|
|
33
35
|
```bash
|
|
34
36
|
pip install "watchdock-errors[django]"
|
|
35
37
|
pip install "watchdock-errors[fastapi]"
|
|
38
|
+
pip install "watchdock-errors[celery]"
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
## Quickstart
|
|
@@ -74,6 +77,27 @@ from watchdock_errors.integrations.fastapi import setup_watchdock
|
|
|
74
77
|
setup_watchdock(app)
|
|
75
78
|
```
|
|
76
79
|
|
|
80
|
+
### Celery
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from celery import Celery
|
|
84
|
+
from watchdock_errors.integrations.celery import register
|
|
85
|
+
|
|
86
|
+
app = Celery("myproject")
|
|
87
|
+
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
88
|
+
app.autodiscover_tasks()
|
|
89
|
+
|
|
90
|
+
register(app)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Call `register(app)` once, after `autodiscover_tasks()`. It reports, automatically:
|
|
94
|
+
|
|
95
|
+
- **Task registry** — every registered task (and, for periodic tasks, their beat schedule), once per worker on startup.
|
|
96
|
+
- **Failures, retries, and revocations** — via `task_failure`, `task_retry`, and `task_revoked`. Successful runs are not reported.
|
|
97
|
+
- **Heartbeats for periodic tasks** — a lightweight ping on every run of a beat-scheduled task, so Watchdock can flag one that's gone silent. Non-periodic tasks are never pinged.
|
|
98
|
+
|
|
99
|
+
`watchdock_errors.init()` must be called before these signals fire — if you're on Django, a `worker_process_init` handler that calls `init()` covers every forked worker process.
|
|
100
|
+
|
|
77
101
|
## Manual capture
|
|
78
102
|
|
|
79
103
|
```python
|
|
@@ -13,6 +13,7 @@ With framework extras:
|
|
|
13
13
|
```bash
|
|
14
14
|
pip install "watchdock-errors[django]"
|
|
15
15
|
pip install "watchdock-errors[fastapi]"
|
|
16
|
+
pip install "watchdock-errors[celery]"
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
## Quickstart
|
|
@@ -54,6 +55,27 @@ from watchdock_errors.integrations.fastapi import setup_watchdock
|
|
|
54
55
|
setup_watchdock(app)
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
### Celery
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from celery import Celery
|
|
62
|
+
from watchdock_errors.integrations.celery import register
|
|
63
|
+
|
|
64
|
+
app = Celery("myproject")
|
|
65
|
+
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
66
|
+
app.autodiscover_tasks()
|
|
67
|
+
|
|
68
|
+
register(app)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Call `register(app)` once, after `autodiscover_tasks()`. It reports, automatically:
|
|
72
|
+
|
|
73
|
+
- **Task registry** — every registered task (and, for periodic tasks, their beat schedule), once per worker on startup.
|
|
74
|
+
- **Failures, retries, and revocations** — via `task_failure`, `task_retry`, and `task_revoked`. Successful runs are not reported.
|
|
75
|
+
- **Heartbeats for periodic tasks** — a lightweight ping on every run of a beat-scheduled task, so Watchdock can flag one that's gone silent. Non-periodic tasks are never pinged.
|
|
76
|
+
|
|
77
|
+
`watchdock_errors.init()` must be called before these signals fire — if you're on Django, a `worker_process_init` handler that calls `init()` covers every forked worker process.
|
|
78
|
+
|
|
57
79
|
## Manual capture
|
|
58
80
|
|
|
59
81
|
```python
|
|
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|
|
29
29
|
from .config import SDKConfig
|
|
30
30
|
from .client import WatchdockClient
|
|
31
31
|
|
|
32
|
-
__all__ = ["init", "capture_exception", "capture_message", "flush", "close", "__version__"]
|
|
32
|
+
__all__ = ["init", "capture_exception", "capture_message", "flush", "close", "get_config", "__version__"]
|
|
33
33
|
|
|
34
34
|
_client: "WatchdockClient | None" = None
|
|
35
35
|
_config: "SDKConfig | None" = None
|
|
@@ -139,6 +139,15 @@ def capture_message(
|
|
|
139
139
|
_client.capture(event)
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def get_config() -> "SDKConfig | None":
|
|
143
|
+
"""Return the active SDKConfig, or None if init() has not been called.
|
|
144
|
+
|
|
145
|
+
Intended for integrations (e.g. celery.py) that need direct access to
|
|
146
|
+
api_key/endpoint/etc. rather than going through capture_exception/message.
|
|
147
|
+
"""
|
|
148
|
+
return _config
|
|
149
|
+
|
|
150
|
+
|
|
142
151
|
def flush(timeout: float = 2.0) -> None:
|
|
143
152
|
"""Block until all queued events have been sent or timeout expires."""
|
|
144
153
|
if _client is not None:
|
|
@@ -3,14 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Callable
|
|
5
5
|
|
|
6
|
-
try:
|
|
7
|
-
from ._version import version as __version__
|
|
8
|
-
except ImportError:
|
|
9
|
-
__version__ = "0.0.0"
|
|
10
|
-
|
|
11
6
|
|
|
12
7
|
def _get_version() -> str:
|
|
13
|
-
|
|
8
|
+
try:
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
return version("watchdock-errors")
|
|
11
|
+
except Exception:
|
|
12
|
+
pass
|
|
13
|
+
try:
|
|
14
|
+
from ._version import version as _v
|
|
15
|
+
return _v
|
|
16
|
+
except ImportError:
|
|
17
|
+
return "0.0.0"
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
@dataclass
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Celery integration for watchdock-errors — task registry + failure/retry reporting.
|
|
3
|
+
|
|
4
|
+
Two things get reported automatically once wired up:
|
|
5
|
+
|
|
6
|
+
1. Task registry: the customer's registered Celery tasks (and, for periodic
|
|
7
|
+
tasks, their beat schedule), reported once per worker right after Celery
|
|
8
|
+
finishes autodiscovering task modules. Tells Watchdock what tasks are
|
|
9
|
+
expected to exist — the prerequisite for beat-schedule health checks.
|
|
10
|
+
|
|
11
|
+
2. Failures, retries, and revocations: every `task_failure`, `task_retry`,
|
|
12
|
+
and `task_revoked` is reported as it happens, with the exception,
|
|
13
|
+
traceback, task id/queue/worker hostname, and retry count vs.
|
|
14
|
+
max_retries. Successful runs are not reported — only what a dev
|
|
15
|
+
actually needs to see.
|
|
16
|
+
|
|
17
|
+
3. Heartbeats for periodic (beat-scheduled) tasks only: a lightweight ping
|
|
18
|
+
on every `task_prerun` for a task name present in app.conf.beat_schedule,
|
|
19
|
+
so Watchdock can tell when a periodic task has gone silent. Non-periodic
|
|
20
|
+
tasks are never pinged — this stays cheap regardless of task volume.
|
|
21
|
+
|
|
22
|
+
Usage — call once from your Celery app module, after autodiscover_tasks():
|
|
23
|
+
|
|
24
|
+
from celery import Celery
|
|
25
|
+
from watchdock_errors.integrations.celery import register
|
|
26
|
+
|
|
27
|
+
app = Celery("myproject")
|
|
28
|
+
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
29
|
+
app.autodiscover_tasks()
|
|
30
|
+
|
|
31
|
+
register(app)
|
|
32
|
+
|
|
33
|
+
watchdock_errors.init() must have been called before these signals fire
|
|
34
|
+
(e.g. in a `worker_process_init` handler, or at import time) — if not,
|
|
35
|
+
the report is skipped.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
import threading
|
|
42
|
+
from typing import TYPE_CHECKING
|
|
43
|
+
|
|
44
|
+
import requests
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from celery import Celery
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger("watchdock_errors")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def register(app: "Celery") -> None:
|
|
53
|
+
"""Wire up the Watchdock Celery integration on the given Celery app."""
|
|
54
|
+
try:
|
|
55
|
+
from celery.signals import task_failure, task_prerun, task_retry, task_revoked, worker_ready
|
|
56
|
+
except ImportError:
|
|
57
|
+
logger.warning("watchdock_errors: celery is not installed — Celery integration disabled")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
periodic_task_names = set(_periodic_schedules_by_task_name(app).keys())
|
|
61
|
+
|
|
62
|
+
@worker_ready.connect(weak=False)
|
|
63
|
+
def _on_worker_ready(sender=None, **kwargs) -> None:
|
|
64
|
+
_report_task_registry(app)
|
|
65
|
+
|
|
66
|
+
@task_prerun.connect(weak=False)
|
|
67
|
+
def _on_task_prerun(sender=None, task=None, **kwargs) -> None:
|
|
68
|
+
name = getattr(task, "name", None) or getattr(sender, "name", None)
|
|
69
|
+
if name in periodic_task_names:
|
|
70
|
+
_report_heartbeat(name)
|
|
71
|
+
|
|
72
|
+
@task_failure.connect(weak=False)
|
|
73
|
+
def _on_task_failure(sender=None, task_id=None, exception=None, einfo=None, **kwargs) -> None:
|
|
74
|
+
request = getattr(sender, "request", None)
|
|
75
|
+
_report_execution(
|
|
76
|
+
status="failure",
|
|
77
|
+
task_id=task_id,
|
|
78
|
+
task_name=getattr(sender, "name", None),
|
|
79
|
+
queue=getattr(sender, "queue", None),
|
|
80
|
+
worker_hostname=getattr(request, "hostname", None),
|
|
81
|
+
retries=getattr(request, "retries", None),
|
|
82
|
+
max_retries=getattr(sender, "max_retries", None),
|
|
83
|
+
exception_type=type(exception).__name__ if exception is not None else "",
|
|
84
|
+
exception_message=str(exception) if exception is not None else "",
|
|
85
|
+
traceback=str(einfo) if einfo is not None else "",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@task_retry.connect(weak=False)
|
|
89
|
+
def _on_task_retry(sender=None, request=None, reason=None, einfo=None, **kwargs) -> None:
|
|
90
|
+
_report_execution(
|
|
91
|
+
status="retry",
|
|
92
|
+
task_id=getattr(request, "id", None),
|
|
93
|
+
task_name=getattr(sender, "name", None),
|
|
94
|
+
queue=getattr(sender, "queue", None),
|
|
95
|
+
worker_hostname=getattr(request, "hostname", None),
|
|
96
|
+
retries=getattr(request, "retries", None),
|
|
97
|
+
max_retries=getattr(sender, "max_retries", None),
|
|
98
|
+
exception_type=type(reason).__name__ if reason is not None else "",
|
|
99
|
+
exception_message=str(reason) if reason is not None else "",
|
|
100
|
+
traceback=str(einfo) if einfo is not None else "",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@task_revoked.connect(weak=False)
|
|
104
|
+
def _on_task_revoked(sender=None, request=None, terminated=None, signum=None, expired=None, **kwargs) -> None:
|
|
105
|
+
if expired:
|
|
106
|
+
message = "Task expired before it could run"
|
|
107
|
+
elif terminated:
|
|
108
|
+
message = f"Task terminated mid-execution (signal {signum})"
|
|
109
|
+
else:
|
|
110
|
+
message = "Task revoked before it started running"
|
|
111
|
+
|
|
112
|
+
_report_execution(
|
|
113
|
+
status="revoked",
|
|
114
|
+
task_id=getattr(request, "id", None),
|
|
115
|
+
task_name=getattr(sender, "name", None),
|
|
116
|
+
queue=getattr(sender, "queue", None),
|
|
117
|
+
worker_hostname=getattr(request, "hostname", None),
|
|
118
|
+
retries=getattr(request, "retries", None),
|
|
119
|
+
max_retries=getattr(sender, "max_retries", None),
|
|
120
|
+
exception_type="TaskRevoked",
|
|
121
|
+
exception_message=message,
|
|
122
|
+
traceback="",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _report_task_registry(app: "Celery") -> None:
|
|
127
|
+
config = _get_config_or_warn()
|
|
128
|
+
if config is None:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
tasks = _collect_tasks(app)
|
|
132
|
+
threading.Thread(
|
|
133
|
+
target=_send_registry, args=(config, tasks), daemon=True, name="watchdock-celery-registry"
|
|
134
|
+
).start()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _report_execution(
|
|
138
|
+
status: str,
|
|
139
|
+
task_id: str | None,
|
|
140
|
+
task_name: str | None,
|
|
141
|
+
queue: str | None,
|
|
142
|
+
worker_hostname: str | None,
|
|
143
|
+
retries: int | None,
|
|
144
|
+
max_retries: int | None,
|
|
145
|
+
exception_type: str,
|
|
146
|
+
exception_message: str,
|
|
147
|
+
traceback: str,
|
|
148
|
+
) -> None:
|
|
149
|
+
config = _get_config_or_warn()
|
|
150
|
+
if config is None:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
event = {
|
|
154
|
+
"task_id": task_id or "",
|
|
155
|
+
"task_name": task_name or "unknown",
|
|
156
|
+
"status": status,
|
|
157
|
+
"queue": queue or "",
|
|
158
|
+
"worker_hostname": worker_hostname or "",
|
|
159
|
+
"retries": retries,
|
|
160
|
+
"max_retries": max_retries,
|
|
161
|
+
"exception_type": exception_type,
|
|
162
|
+
"exception_message": exception_message,
|
|
163
|
+
"traceback": traceback,
|
|
164
|
+
}
|
|
165
|
+
threading.Thread(
|
|
166
|
+
target=_send_execution, args=(config, event), daemon=True, name="watchdock-celery-execution"
|
|
167
|
+
).start()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _report_heartbeat(task_name: str) -> None:
|
|
171
|
+
config = _get_config_or_warn()
|
|
172
|
+
if config is None:
|
|
173
|
+
return
|
|
174
|
+
threading.Thread(
|
|
175
|
+
target=_send_heartbeat, args=(config, task_name), daemon=True, name="watchdock-celery-heartbeat"
|
|
176
|
+
).start()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _get_config_or_warn():
|
|
180
|
+
import watchdock_errors
|
|
181
|
+
|
|
182
|
+
config = watchdock_errors.get_config()
|
|
183
|
+
if config is None:
|
|
184
|
+
logger.warning("watchdock_errors: celery integration fired before init() — skipping report")
|
|
185
|
+
return config
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _periodic_schedules_by_task_name(app: "Celery") -> dict:
|
|
189
|
+
beat_schedule = app.conf.beat_schedule or {}
|
|
190
|
+
schedules_by_task_name = {}
|
|
191
|
+
for entry in beat_schedule.values():
|
|
192
|
+
task_name = entry.get("task") if isinstance(entry, dict) else getattr(entry, "task", None)
|
|
193
|
+
if task_name:
|
|
194
|
+
schedules_by_task_name[task_name] = _serialize_schedule(entry)
|
|
195
|
+
return schedules_by_task_name
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _collect_tasks(app: "Celery") -> list[dict]:
|
|
199
|
+
schedules_by_task_name = _periodic_schedules_by_task_name(app)
|
|
200
|
+
|
|
201
|
+
tasks = []
|
|
202
|
+
for name, task in app.tasks.items():
|
|
203
|
+
if name.startswith("celery."):
|
|
204
|
+
continue
|
|
205
|
+
tasks.append(
|
|
206
|
+
{
|
|
207
|
+
"name": name,
|
|
208
|
+
"module": getattr(task, "__module__", "") or "",
|
|
209
|
+
"queue": getattr(task, "queue", "") or "",
|
|
210
|
+
"max_retries": getattr(task, "max_retries", None),
|
|
211
|
+
"is_periodic": name in schedules_by_task_name,
|
|
212
|
+
"schedule": schedules_by_task_name.get(name),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
return tasks
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _serialize_schedule(entry) -> dict | None:
|
|
219
|
+
schedule = entry.get("schedule") if isinstance(entry, dict) else getattr(entry, "schedule", None)
|
|
220
|
+
if schedule is None:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
if hasattr(schedule, "_orig_minute"): # celery.schedules.crontab
|
|
224
|
+
return {
|
|
225
|
+
"type": "crontab",
|
|
226
|
+
"minute": schedule._orig_minute,
|
|
227
|
+
"hour": schedule._orig_hour,
|
|
228
|
+
"day_of_week": schedule._orig_day_of_week,
|
|
229
|
+
"day_of_month": schedule._orig_day_of_month,
|
|
230
|
+
"month_of_year": schedule._orig_month_of_year,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
seconds = getattr(schedule, "seconds", schedule) # celery.schedules.schedule or plain number
|
|
234
|
+
try:
|
|
235
|
+
return {"type": "interval", "every_seconds": float(seconds)}
|
|
236
|
+
except (TypeError, ValueError):
|
|
237
|
+
return {"type": "unknown"}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _send_registry(config, tasks: list[dict]) -> None:
|
|
241
|
+
if not tasks:
|
|
242
|
+
return
|
|
243
|
+
_post(config, "/api/v1/celery/registry/", {"environment": config.environment, "tasks": tasks}, "task", len(tasks))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _send_execution(config, event: dict) -> None:
|
|
247
|
+
payload = {**event, "environment": config.environment}
|
|
248
|
+
_post(config, "/api/v1/celery/executions/", payload, "execution", 1)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _send_heartbeat(config, task_name: str) -> None:
|
|
252
|
+
payload = {"task_name": task_name, "environment": config.environment}
|
|
253
|
+
_post(config, "/api/v1/celery/heartbeat/", payload, "heartbeat", 1)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _post(config, path: str, payload: dict, noun: str, count: int) -> None:
|
|
257
|
+
url = f"{config.endpoint.rstrip('/')}{path}"
|
|
258
|
+
try:
|
|
259
|
+
response = requests.post(
|
|
260
|
+
url,
|
|
261
|
+
json=payload,
|
|
262
|
+
headers={
|
|
263
|
+
"Authorization": f"Bearer {config.api_key}",
|
|
264
|
+
"Content-Type": "application/json",
|
|
265
|
+
"User-Agent": f"watchdock-errors/{config.sdk_version}",
|
|
266
|
+
},
|
|
267
|
+
timeout=config.timeout * 3,
|
|
268
|
+
)
|
|
269
|
+
if response.status_code in (200, 201, 202):
|
|
270
|
+
logger.info("watchdock_errors: reported %d celery %s(s) to %s", count, noun, path)
|
|
271
|
+
else:
|
|
272
|
+
logger.warning(
|
|
273
|
+
"watchdock_errors: celery %s rejected — status=%s body=%s", noun, response.status_code, response.text[:500]
|
|
274
|
+
)
|
|
275
|
+
except requests.exceptions.ConnectionError as e:
|
|
276
|
+
logger.error("watchdock_errors: connection failed reporting celery %s — %s", noun, e)
|
|
277
|
+
except requests.exceptions.Timeout:
|
|
278
|
+
logger.error("watchdock_errors: timed out reporting celery %s after %ss", noun, config.timeout * 3)
|
|
279
|
+
except Exception:
|
|
280
|
+
logger.error("watchdock_errors: unexpected error reporting celery %s", noun, exc_info=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: watchdock-errors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Application error tracking SDK for the Watchdock platform
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: error-tracking,observability,watchdock
|
|
@@ -12,6 +12,8 @@ Requires-Dist: django>=3.2; extra == "django"
|
|
|
12
12
|
Provides-Extra: fastapi
|
|
13
13
|
Requires-Dist: fastapi>=0.95; extra == "fastapi"
|
|
14
14
|
Requires-Dist: starlette>=0.27; extra == "fastapi"
|
|
15
|
+
Provides-Extra: celery
|
|
16
|
+
Requires-Dist: celery>=5.0; extra == "celery"
|
|
15
17
|
Provides-Extra: dev
|
|
16
18
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
19
|
Requires-Dist: pytest-mock>=3.0; extra == "dev"
|
|
@@ -33,6 +35,7 @@ With framework extras:
|
|
|
33
35
|
```bash
|
|
34
36
|
pip install "watchdock-errors[django]"
|
|
35
37
|
pip install "watchdock-errors[fastapi]"
|
|
38
|
+
pip install "watchdock-errors[celery]"
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
## Quickstart
|
|
@@ -74,6 +77,27 @@ from watchdock_errors.integrations.fastapi import setup_watchdock
|
|
|
74
77
|
setup_watchdock(app)
|
|
75
78
|
```
|
|
76
79
|
|
|
80
|
+
### Celery
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from celery import Celery
|
|
84
|
+
from watchdock_errors.integrations.celery import register
|
|
85
|
+
|
|
86
|
+
app = Celery("myproject")
|
|
87
|
+
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
88
|
+
app.autodiscover_tasks()
|
|
89
|
+
|
|
90
|
+
register(app)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Call `register(app)` once, after `autodiscover_tasks()`. It reports, automatically:
|
|
94
|
+
|
|
95
|
+
- **Task registry** — every registered task (and, for periodic tasks, their beat schedule), once per worker on startup.
|
|
96
|
+
- **Failures, retries, and revocations** — via `task_failure`, `task_retry`, and `task_revoked`. Successful runs are not reported.
|
|
97
|
+
- **Heartbeats for periodic tasks** — a lightweight ping on every run of a beat-scheduled task, so Watchdock can flag one that's gone silent. Non-periodic tasks are never pinged.
|
|
98
|
+
|
|
99
|
+
`watchdock_errors.init()` must be called before these signals fire — if you're on Django, a `worker_process_init` handler that calls `init()` covers every forked worker process.
|
|
100
|
+
|
|
77
101
|
## Manual capture
|
|
78
102
|
|
|
79
103
|
```python
|
|
@@ -16,6 +16,7 @@ src/watchdock_errors.egg-info/scm_file_list.json
|
|
|
16
16
|
src/watchdock_errors.egg-info/scm_version.json
|
|
17
17
|
src/watchdock_errors.egg-info/top_level.txt
|
|
18
18
|
src/watchdock_errors/integrations/__init__.py
|
|
19
|
+
src/watchdock_errors/integrations/celery.py
|
|
19
20
|
src/watchdock_errors/integrations/django.py
|
|
20
21
|
src/watchdock_errors/integrations/fastapi.py
|
|
21
22
|
tests/test_client.py
|
{watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/scm_file_list.json
RENAMED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
|
+
".github/workflows/ci.yml",
|
|
4
|
+
".github/workflows/release.yml",
|
|
3
5
|
".gitignore",
|
|
4
6
|
"README.md",
|
|
5
7
|
"pyproject.toml",
|
|
6
|
-
"src/watchdock_errors/
|
|
8
|
+
"src/watchdock_errors/__init__.py",
|
|
7
9
|
"src/watchdock_errors/client.py",
|
|
8
10
|
"src/watchdock_errors/config.py",
|
|
9
|
-
"src/watchdock_errors/
|
|
10
|
-
"src/watchdock_errors/__init__.py",
|
|
11
|
-
"src/watchdock_errors/integrations/fastapi.py",
|
|
12
|
-
"src/watchdock_errors/integrations/django.py",
|
|
11
|
+
"src/watchdock_errors/event.py",
|
|
13
12
|
"src/watchdock_errors/integrations/__init__.py",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
13
|
+
"src/watchdock_errors/integrations/celery.py",
|
|
14
|
+
"src/watchdock_errors/integrations/django.py",
|
|
15
|
+
"src/watchdock_errors/integrations/fastapi.py",
|
|
16
|
+
"src/watchdock_errors/utils.py",
|
|
17
17
|
"tests/test_client.py",
|
|
18
|
+
"tests/test_event.py",
|
|
18
19
|
"tests/test_utils.py"
|
|
19
20
|
]
|
|
20
21
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/__init__.py
RENAMED
|
File without changes
|
{watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/django.py
RENAMED
|
File without changes
|
{watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{watchdock_errors-0.3.0 → watchdock_errors-0.4.0}/src/watchdock_errors.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|