Skip to content

Commit 938174a

Browse files
committed
Merge branch 'insights-instrumentation' into event-sampling
2 parents 8f3b499 + 06e1eec commit 938174a

File tree

15 files changed

+280
-62
lines changed

15 files changed

+280
-62
lines changed

honeybadger/contrib/celery.py

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import threading
21
import time
2+
import logging
33

44
from honeybadger import honeybadger
55
from honeybadger.plugins import Plugin, default_plugin_manager
66
from honeybadger.utils import filter_dict, extract_honeybadger_config, get_duration
77

8-
_listener_started = False
8+
logger = logging.getLogger(__name__)
99

1010

1111
class CeleryPlugin(Plugin):
@@ -67,7 +67,13 @@ def init_app(self):
6767
"""
6868
Initialize honeybadger and listen for errors.
6969
"""
70-
from celery.signals import task_failure, task_postrun, task_prerun, worker_ready
70+
from celery.signals import (
71+
task_failure,
72+
task_postrun,
73+
task_prerun,
74+
before_task_publish,
75+
worker_process_init,
76+
)
7177

7278
self._task_starts = {}
7379
self._initialize_honeybadger(self.app.conf)
@@ -79,9 +85,9 @@ def init_app(self):
7985
if honeybadger.config.insights_enabled:
8086
# Enable task events, as we need to listen to
8187
# task-finished events
82-
self.app.conf.worker_send_task_events = True
88+
worker_process_init.connect(self._on_worker_process_init, weak=False)
8389
task_prerun.connect(self._on_task_prerun, weak=False)
84-
worker_ready.connect(self._start_task_event_listener, weak=False)
90+
before_task_publish.connect(self._on_before_task_publish, weak=False)
8591

8692
def _initialize_honeybadger(self, config):
8793
"""
@@ -96,35 +102,34 @@ def _initialize_honeybadger(self, config):
96102
honeybadger.configure(**config_kwargs)
97103
honeybadger.config.set_12factor_config() # environment should override celery settings
98104

99-
def _start_task_event_listener(self, *args, **kwargs):
100-
# only start the listener once
101-
global _listener_started
102-
if _listener_started:
103-
return
104-
_listener_started = True
105-
106-
from celery.events import EventReceiver # type: ignore[import]
107-
108-
def run():
109-
with self.app.connection() as conn:
110-
recv = EventReceiver(
111-
conn, handlers={"task-finished": self._on_task_finished}
112-
)
113-
recv.capture(limit=None, timeout=None, wakeup=False)
114-
115-
self._listen_thread = threading.Thread(target=run, daemon=True)
116-
self._listen_thread.start()
117-
118-
def _on_task_finished(self, payload, **kwargs):
119-
honeybadger.event("celery.task_finished", payload["payload"])
105+
def _on_worker_process_init(self, *args, **kwargs):
106+
# Restart the events worker to ensure it is running in the new worker
107+
# process.
108+
try:
109+
honeybadger.events_worker.restart()
110+
except Exception as e:
111+
logger.warning(f"Warning: Failed to restart Honeybadger events worker: {e}")
112+
113+
def _on_before_task_publish(self, sender=None, body=None, headers=None, **kwargs):
114+
# Inject Honeybadger event context into task headers
115+
if headers is not None:
116+
current_context = honeybadger._get_event_context()
117+
if current_context:
118+
headers["honeybadger_event_context"] = current_context
120119

121120
def _on_task_prerun(self, task_id=None, task=None, *args, **kwargs):
122121
self._task_starts[task_id] = time.time()
123122

123+
if task:
124+
context = getattr(task.request, "honeybadger_event_context", None)
125+
if context:
126+
honeybadger.set_event_context(context)
127+
124128
def _on_task_postrun(self, task_id=None, task=None, *args, **kwargs):
125129
"""
126130
Callback executed after a task is finished.
127131
"""
132+
128133
insights_config = honeybadger.config.insights_config
129134

130135
exclude = insights_config.celery.exclude_tasks
@@ -159,7 +164,7 @@ def _on_task_postrun(self, task_id=None, task=None, *args, **kwargs):
159164
remove_keys=True,
160165
)
161166

162-
task.send_event("task-finished", payload=payload)
167+
honeybadger.event("celery.task_finished", payload=payload)
163168

164169
honeybadger.reset_context()
165170

@@ -180,13 +185,15 @@ def tearDown(self):
180185
task_failure.disconnect(self._on_task_failure)
181186

182187
if honeybadger.config.insights_enabled:
183-
from celery.signals import worker_ready, task_prerun
188+
from celery.signals import (
189+
task_prerun,
190+
worker_process_init,
191+
before_task_publish,
192+
)
184193

185194
task_prerun.disconnect(self._on_task_prerun)
186-
worker_ready.disconnect(self._start_task_event_listener)
187-
188-
if hasattr(self, "_listen_thread"):
189-
self._listen_thread.join(timeout=1)
195+
worker_process_init.disconnect(self._on_worker_process_init, weak=False)
196+
before_task_publish.disconnect(self._on_before_task_publish, weak=False)
190197

191198
# Keep the misspelled method for backward compatibility
192199
def tearDowm(self):

honeybadger/contrib/django.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from __future__ import absolute_import
22
import re
33
import time
4+
import uuid
45

56
from six import iteritems
67

78
from honeybadger import honeybadger
89
from honeybadger.plugins import Plugin, default_plugin_manager
9-
from honeybadger.utils import filter_dict, filter_env_vars, get_duration
10+
from honeybadger.utils import (
11+
filter_dict,
12+
filter_env_vars,
13+
get_duration,
14+
sanitize_request_id,
15+
)
1016
from honeybadger.contrib.db import DBHoneybadger
1117

1218
try:
@@ -138,6 +144,7 @@ def __call__(self, request):
138144
set_request(request)
139145
start_time = time.time()
140146
honeybadger.begin_request(request)
147+
self._set_request_id(request)
141148
response = self.get_response(request)
142149

143150
if (
@@ -151,6 +158,19 @@ def __call__(self, request):
151158

152159
return response
153160

161+
def _set_request_id(self, request):
162+
# Attempt to get request ID from various sources
163+
request_id = (
164+
getattr(request, "id", None)
165+
or getattr(request, "request_id", None)
166+
or request.headers.get("X-Request-ID", None)
167+
)
168+
request_id = sanitize_request_id(request_id)
169+
if not request_id:
170+
request_id = str(uuid.uuid4())
171+
172+
honeybadger.set_event_context(request_id=request_id)
173+
154174
def _patch_cursor(self):
155175
from django.db.backends.utils import CursorWrapper
156176

honeybadger/contrib/flask.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import logging
55
import time
6+
import uuid
67

78
from honeybadger import honeybadger
89
from honeybadger.plugins import Plugin, default_plugin_manager
@@ -11,6 +12,7 @@
1112
filter_env_vars,
1213
get_duration,
1314
extract_honeybadger_config,
15+
sanitize_request_id,
1416
)
1517
from honeybadger.contrib.db import DBHoneybadger
1618
from six import iteritems
@@ -201,6 +203,12 @@ def _initialize_honeybadger(self, config):
201203
def _handle_request_started(self, sender, *args, **kwargs):
202204
from flask import request
203205

206+
request_id = sanitize_request_id(request.headers.get("X-Request-ID"))
207+
if not request_id:
208+
request_id = str(uuid.uuid4())
209+
210+
honeybadger.set_event_context(request_id=request_id)
211+
204212
_request_info.set(
205213
{
206214
"start_time": time.time(),

honeybadger/core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ def notify(
8383
merged_ctx = {**base, **(context or {})}
8484
merged_tags = list({*tag_ctx, *(tags or [])})
8585

86+
request_id = self._get_event_context().get("request_id", None)
87+
8688
notice = Notice(
8789
exception=exception,
8890
error_class=error_class,
@@ -91,6 +93,7 @@ def notify(
9193
fingerprint=fingerprint,
9294
tags=merged_tags,
9395
config=self.config,
96+
request_id=request_id,
9497
)
9598
return self._send_notice(notice)
9699

honeybadger/events_worker.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import time
23
import threading
34
import logging
@@ -41,12 +42,29 @@ def __init__(
4142

4243
self._thread = threading.Thread(
4344
target=self._run,
44-
name="honeybadger-events-worker",
45+
name=f"honeybadger-events-worker-{os.getpid()}",
4546
daemon=True,
4647
)
4748
self._thread.start()
4849
self.log.debug("Events worker started")
4950

51+
def restart(self):
52+
"""Restart the batch worker thread (useful after process forking)"""
53+
if hasattr(self, "_thread") and self._thread and self._thread.is_alive():
54+
self.shutdown()
55+
56+
# Reset state
57+
self._stop = False
58+
59+
self._thread = threading.Thread(
60+
target=self._run,
61+
name=f"honeybadger-events-worker-{os.getpid()}",
62+
daemon=True,
63+
)
64+
self._thread.start()
65+
66+
return self._thread.is_alive()
67+
5068
def push(self, event: Event) -> bool:
5169
with self._cond:
5270
if self._all_events_queued_len() >= self.config.events_max_queue_size:

honeybadger/notice.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def __init__(self, *args, **kwargs):
1111
self.fingerprint = kwargs.get("fingerprint", None)
1212
self.config = kwargs.get("config", None)
1313
self.context = kwargs.get("context", {})
14+
self.request_id = kwargs.get("request_id", None)
1415
self.tags = self._construct_tags(kwargs.get("tags", []))
1516

1617
self._process_exception()
@@ -34,6 +35,7 @@ def payload(self):
3435
context=self.context,
3536
tags=self.tags,
3637
config=self.config,
38+
correlation_context=self._correlation_context(),
3739
)
3840

3941
def excluded_exception(self):
@@ -49,6 +51,11 @@ def excluded_exception(self):
4951
return True
5052
return False
5153

54+
def _correlation_context(self):
55+
if self.request_id:
56+
return {"request_id": self.request_id}
57+
return None
58+
5259
def _construct_tags(self, tags):
5360
"""
5461
Accepts either:

honeybadger/payload.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ def create_payload(
158158
config=None,
159159
context=None,
160160
fingerprint=None,
161+
correlation_context=None,
161162
tags=None,
162163
):
163164
# if using local_variables get them
@@ -188,4 +189,7 @@ def create_payload(
188189
"request": {"context": context, "local_variables": local_variables},
189190
}
190191

192+
if correlation_context:
193+
payload["correlation_context"] = correlation_context
194+
191195
return default_plugin_manager.generate_payload(payload, config, context)

0 commit comments

Comments
 (0)