datadog-async-handler 0.1.3__tar.gz → 0.1.4__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.
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/CHANGELOG.md +10 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/PKG-INFO +3 -3
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/README.md +1 -1
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/handler.py +62 -2
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/.gitignore +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/CONTRIBUTING.md +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/LICENSE +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/mkdocs.yml +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/pyproject.toml +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/__init__.py +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/formatter.py +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/py.typed +0 -0
- {datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/uv.lock +0 -0
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.4] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fork safety: a process that called `fork()` while the worker thread was inside a TLS
|
|
12
|
+
send handed the child OpenSSL state mid-update, and the child deadlocked on its own
|
|
13
|
+
first TLS handshake (observed with Celery prefork workers). `os.register_at_fork`
|
|
14
|
+
hooks now hold the send lock across `fork()` and give the child a fresh queue, lock,
|
|
15
|
+
and worker thread. The lock covers only the network attempt, not retry back-off.
|
|
16
|
+
- The worker thread is named `datadog-http-handler`.
|
|
17
|
+
|
|
8
18
|
## [0.1.2] - 2025-08-25
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: datadog-async-handler
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: High-performance async HTTP logging handler for Datadog with batching and retry logic
|
|
5
5
|
Project-URL: Homepage, https://github.com/enlyft/datadog-async-handler
|
|
6
6
|
Project-URL: Documentation, https://enlyft.github.io/datadog-async-handler
|
|
@@ -53,7 +53,7 @@ Description-Content-Type: text/markdown
|
|
|
53
53
|
|
|
54
54
|
# Datadog Async Handler
|
|
55
55
|
|
|
56
|
-
[](https://pypi.org/project/datadog-async-handler/)
|
|
57
57
|
[](https://pypi.org/project/datadog-async-handler/)
|
|
58
58
|
[](https://opensource.org/licenses/MIT)
|
|
59
59
|
[](https://github.com/astral-sh/ruff)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Datadog Async Handler
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/datadog-async-handler/)
|
|
4
4
|
[](https://pypi.org/project/datadog-async-handler/)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://github.com/astral-sh/ruff)
|
{datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/handler.py
RENAMED
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
import os
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
|
+
import weakref
|
|
12
13
|
from queue import Empty, Queue
|
|
13
14
|
from typing import Optional
|
|
14
15
|
|
|
@@ -18,6 +19,17 @@ from datadog_api_client.v2.model.http_log import HTTPLog
|
|
|
18
19
|
from datadog_api_client.v2.model.http_log_item import HTTPLogItem
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def _at_fork(ref: "weakref.ReferenceType[DatadogHTTPHandler]", method: str) -> None:
|
|
23
|
+
"""Dispatch an ``os.register_at_fork`` callback to a handler that is still alive.
|
|
24
|
+
|
|
25
|
+
``os.register_at_fork`` keeps its callables for the life of the process, so the
|
|
26
|
+
hooks hold only a weak reference and become no-ops once the handler is collected.
|
|
27
|
+
"""
|
|
28
|
+
handler = ref()
|
|
29
|
+
if handler is not None:
|
|
30
|
+
getattr(handler, method)()
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
class DatadogHTTPHandler(logging.Handler):
|
|
22
34
|
"""
|
|
23
35
|
High-performance logging handler that sends logs to Datadog via HTTP API.
|
|
@@ -26,6 +38,12 @@ class DatadogHTTPHandler(logging.Handler):
|
|
|
26
38
|
the main application thread. It includes retry logic with exponential backoff,
|
|
27
39
|
comprehensive error handling, and support for all Datadog sites.
|
|
28
40
|
|
|
41
|
+
The handler is fork-safe. Its worker thread sends over TLS, and a child forked
|
|
42
|
+
while that handshake is in flight inherits OpenSSL state mid-update and deadlocks
|
|
43
|
+
on its own first TLS handshake. ``os.register_at_fork`` hooks hold a send lock
|
|
44
|
+
across ``fork()`` so no child is forked mid-send, and give the child a fresh
|
|
45
|
+
queue, lock, and worker thread (threads do not survive ``fork()``).
|
|
46
|
+
|
|
29
47
|
Args:
|
|
30
48
|
api_key: Datadog API key (or set DD_API_KEY env var)
|
|
31
49
|
site: Datadog site (default: datadoghq.com)
|
|
@@ -60,6 +78,9 @@ class DatadogHTTPHandler(logging.Handler):
|
|
|
60
78
|
>>> logger.addHandler(handler)
|
|
61
79
|
"""
|
|
62
80
|
|
|
81
|
+
# Bounded so a worker wedged inside C can never freeze the parent's fork() forever.
|
|
82
|
+
_FORK_WAIT_SECONDS = 15.0
|
|
83
|
+
|
|
63
84
|
def __init__(
|
|
64
85
|
self,
|
|
65
86
|
api_key: Optional[str] = None,
|
|
@@ -103,7 +124,16 @@ class DatadogHTTPHandler(logging.Handler):
|
|
|
103
124
|
self._log_queue: Queue[HTTPLogItem] = Queue()
|
|
104
125
|
self._stop_event = threading.Event()
|
|
105
126
|
self._worker_thread: Optional[threading.Thread] = None
|
|
127
|
+
self._send_lock = threading.Lock()
|
|
128
|
+
self._fork_lock_held = False
|
|
106
129
|
self._start_worker()
|
|
130
|
+
if hasattr(os, "register_at_fork"): # POSIX only
|
|
131
|
+
ref = weakref.ref(self)
|
|
132
|
+
os.register_at_fork(
|
|
133
|
+
before=lambda: _at_fork(ref, "_before_fork"),
|
|
134
|
+
after_in_parent=lambda: _at_fork(ref, "_after_fork_in_parent"),
|
|
135
|
+
after_in_child=lambda: _at_fork(ref, "_after_fork_in_child"),
|
|
136
|
+
)
|
|
107
137
|
|
|
108
138
|
def _setup_api_client(self) -> None:
|
|
109
139
|
"""Setup the Datadog API client."""
|
|
@@ -117,7 +147,9 @@ class DatadogHTTPHandler(logging.Handler):
|
|
|
117
147
|
def _start_worker(self) -> None:
|
|
118
148
|
"""Start the background worker thread."""
|
|
119
149
|
if self._worker_thread is None or not self._worker_thread.is_alive():
|
|
120
|
-
self._worker_thread = threading.Thread(
|
|
150
|
+
self._worker_thread = threading.Thread(
|
|
151
|
+
target=self._worker, daemon=True, name="datadog-http-handler"
|
|
152
|
+
)
|
|
121
153
|
self._worker_thread.start()
|
|
122
154
|
|
|
123
155
|
def _worker(self) -> None:
|
|
@@ -161,7 +193,8 @@ class DatadogHTTPHandler(logging.Handler):
|
|
|
161
193
|
for attempt in range(self.max_retries + 1):
|
|
162
194
|
try:
|
|
163
195
|
http_log = HTTPLog(batch)
|
|
164
|
-
self.
|
|
196
|
+
with self._send_lock: # fork() waits for this, not for back-off
|
|
197
|
+
self.logs_api.submit_log(body=http_log)
|
|
165
198
|
return # Success
|
|
166
199
|
|
|
167
200
|
except Exception as e:
|
|
@@ -174,6 +207,33 @@ class DatadogHTTPHandler(logging.Handler):
|
|
|
174
207
|
# Wait before retry (exponential backoff)
|
|
175
208
|
time.sleep(2**attempt)
|
|
176
209
|
|
|
210
|
+
def _before_fork(self) -> None:
|
|
211
|
+
"""Hold the send lock across ``fork()`` so no child is forked mid-send."""
|
|
212
|
+
self._fork_lock_held = self._send_lock.acquire(timeout=self._FORK_WAIT_SECONDS)
|
|
213
|
+
if not self._fork_lock_held:
|
|
214
|
+
self._handle_error(
|
|
215
|
+
"forking while a log send is wedged; the child's TLS may deadlock"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _after_fork_in_parent(self) -> None:
|
|
219
|
+
"""Release the lock taken by ``_before_fork``."""
|
|
220
|
+
if self._fork_lock_held:
|
|
221
|
+
self._fork_lock_held = False
|
|
222
|
+
self._send_lock.release()
|
|
223
|
+
|
|
224
|
+
def _after_fork_in_child(self) -> None:
|
|
225
|
+
"""Give the child a fresh lock, queue, and worker thread.
|
|
226
|
+
|
|
227
|
+
The parent's worker thread does not exist in the child, and the inherited
|
|
228
|
+
lock may still be held by it.
|
|
229
|
+
"""
|
|
230
|
+
self._send_lock = threading.Lock()
|
|
231
|
+
self._fork_lock_held = False
|
|
232
|
+
self._log_queue = Queue()
|
|
233
|
+
self._stop_event = threading.Event()
|
|
234
|
+
self._worker_thread = None
|
|
235
|
+
self._start_worker()
|
|
236
|
+
|
|
177
237
|
def _handle_error(self, message: str) -> None:
|
|
178
238
|
"""Handle errors that occur during log submission."""
|
|
179
239
|
# Log to stderr to avoid infinite recursion
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/__init__.py
RENAMED
|
File without changes
|
{datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/formatter.py
RENAMED
|
File without changes
|
{datadog_async_handler-0.1.3 → datadog_async_handler-0.1.4}/src/datadog_http_handler/py.typed
RENAMED
|
File without changes
|
|
File without changes
|