datadog-async-handler 0.1.2__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.
@@ -0,0 +1,64 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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
+
18
+ ## [0.1.2] - 2025-08-25
19
+
20
+ ### Added
21
+ - First stable release of datadog-async-handler
22
+ - High-performance async HTTP logging handler for Datadog
23
+ - Asynchronous batching and background processing
24
+ - Automatic retry logic with exponential backoff
25
+ - Configurable batch size and flush intervals
26
+ - Rich metadata support (service, environment, tags)
27
+ - Multi-site Datadog support (US, EU, etc.)
28
+ - Full type hints and mypy compatibility
29
+ - Comprehensive documentation and examples
30
+ - GitHub Actions CI/CD with trusted PyPI publishing
31
+
32
+ ### Features
33
+ - **Performance**: Non-blocking background processing
34
+ - **Reliability**: Comprehensive retry logic and error handling
35
+ - **Efficiency**: Memory-optimized batching reduces API calls
36
+ - **Integration**: Drop-in replacement for standard logging handlers
37
+ - **Framework Support**: Examples for Django, FastAPI, Flask, Celery
38
+ - **Testing**: Comprehensive test suite with >95% coverage
39
+
40
+ ### Documentation
41
+ - Complete API reference with mkdocstrings
42
+ - Framework integration examples
43
+ - Configuration guide with all parameters
44
+ - Troubleshooting documentation
45
+ - Installation instructions
46
+
47
+ ### Infrastructure
48
+ - Modern Python packaging with pyproject.toml
49
+ - Hatch build system with UV package manager
50
+ - Ruff for formatting and linting
51
+ - MyPy for type checking
52
+ - Pytest for testing
53
+ - MkDocs Material theme for documentation
54
+ - GitHub Actions for CI/CD
55
+ - Trusted publishing to PyPI
56
+
57
+ ## [Unreleased]
58
+
59
+ ### Planned
60
+ - Async/await support for truly asynchronous logging
61
+ - Metrics and tracing integration
62
+ - Advanced filtering and transformation options
63
+ - Performance optimizations
64
+ - Additional framework integrations
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: datadog-async-handler
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: High-performance async HTTP logging handler for Datadog with batching and retry logic
5
- Project-URL: Homepage, https://github.com/enlyft/datadog-http-handler
6
- Project-URL: Documentation, https://enlyft.github.io/datadog-http-handler
7
- Project-URL: Repository, https://github.com/enlyft/datadog-http-handler.git
8
- Project-URL: Changelog, https://github.com/enlyft/datadog-http-handler/blob/main/CHANGELOG.md
9
- Project-URL: Bug Tracker, https://github.com/enlyft/datadog-http-handler/issues
5
+ Project-URL: Homepage, https://github.com/enlyft/datadog-async-handler
6
+ Project-URL: Documentation, https://enlyft.github.io/datadog-async-handler
7
+ Project-URL: Repository, https://github.com/enlyft/datadog-async-handler.git
8
+ Project-URL: Changelog, https://github.com/enlyft/datadog-async-handler/blob/main/CHANGELOG.md
9
+ Project-URL: Bug Tracker, https://github.com/enlyft/datadog-async-handler/issues
10
10
  Author-email: Parth <parth@enlyft.com>
11
11
  Maintainer-email: Parth <parth@enlyft.com>
12
12
  License-Expression: MIT
@@ -53,7 +53,7 @@ Description-Content-Type: text/markdown
53
53
 
54
54
  # Datadog Async Handler
55
55
 
56
- [![PyPI version](https://badge.fury.io/py/datadog-async-handler.svg)](https://badge.fury.io/py/datadog-async-handler)
56
+ [![PyPI version](https://img.shields.io/pypi/v/datadog-async-handler)](https://pypi.org/project/datadog-async-handler/)
57
57
  [![Python versions](https://img.shields.io/pypi/pyversions/datadog-async-handler.svg)](https://pypi.org/project/datadog-async-handler/)
58
58
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
59
59
  [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -1,6 +1,6 @@
1
1
  # Datadog Async Handler
2
2
 
3
- [![PyPI version](https://badge.fury.io/py/datadog-async-handler.svg)](https://badge.fury.io/py/datadog-async-handler)
3
+ [![PyPI version](https://img.shields.io/pypi/v/datadog-async-handler)](https://pypi.org/project/datadog-async-handler/)
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/datadog-async-handler.svg)](https://pypi.org/project/datadog-async-handler/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
@@ -1,8 +1,8 @@
1
1
  site_name: Datadog Async Handler
2
2
  site_description: High-performance async HTTP logging handler for Datadog with batching and retry logic
3
- site_url: https://enlyft.github.io/datadog-http-handler
4
- repo_url: https://github.com/enlyft/datadog-http-handler
5
- repo_name: enlyft/datadog-http-handler
3
+ site_url: https://enlyft.github.io/datadog-async-handler
4
+ repo_url: https://github.com/enlyft/datadog-async-handler
5
+ repo_name: enlyft/datadog-async-handler
6
6
 
7
7
  theme:
8
8
  name: material
@@ -71,11 +71,11 @@ test = [
71
71
  ]
72
72
 
73
73
  [project.urls]
74
- Homepage = "https://github.com/enlyft/datadog-http-handler"
75
- Documentation = "https://enlyft.github.io/datadog-http-handler"
76
- Repository = "https://github.com/enlyft/datadog-http-handler.git"
77
- Changelog = "https://github.com/enlyft/datadog-http-handler/blob/main/CHANGELOG.md"
78
- "Bug Tracker" = "https://github.com/enlyft/datadog-http-handler/issues"
74
+ Homepage = "https://github.com/enlyft/datadog-async-handler"
75
+ Documentation = "https://enlyft.github.io/datadog-async-handler"
76
+ Repository = "https://github.com/enlyft/datadog-async-handler.git"
77
+ Changelog = "https://github.com/enlyft/datadog-async-handler/blob/main/CHANGELOG.md"
78
+ "Bug Tracker" = "https://github.com/enlyft/datadog-async-handler/issues"
79
79
 
80
80
  [tool.hatch.version]
81
81
  source = "vcs"
@@ -24,4 +24,4 @@ Example:
24
24
  from .handler import DatadogHTTPHandler
25
25
 
26
26
  __version__ = "0.1.0"
27
- __all__ = ["DatadogHTTPHandler"]
27
+ __all__ = ["DatadogHTTPHandler", "DatadogJsonFormatter"]
@@ -0,0 +1,132 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from typing import Optional
5
+ from datetime import datetime, timezone
6
+
7
+
8
+ class DatadogJsonFormatter(logging.Formatter):
9
+ """
10
+ JSON formatter optimized for Datadog log ingestion.
11
+
12
+ This formatter creates structured JSON logs with fields that Datadog
13
+ can automatically parse and index for better searchability and analysis.
14
+ """
15
+
16
+ def __init__(self, service_name: str, version: Optional[str] = None):
17
+ """
18
+ Initialize the formatter.
19
+
20
+ Args:
21
+ service_name: Name of the service (e.g., 'auth-service', 'embedding-service')
22
+ version: Version of the service (optional)
23
+ """
24
+ super().__init__()
25
+ self.service_name = service_name
26
+ self.version = version or os.getenv("SERVICE_VERSION", "unknown")
27
+ self.environment = os.getenv("DD_ENV", "development")
28
+
29
+ def format(self, record: logging.LogRecord) -> str:
30
+ """
31
+ Format a log record as JSON.
32
+
33
+ Args:
34
+ record: The log record to format
35
+
36
+ Returns:
37
+ JSON-formatted log string
38
+ """
39
+ # Base log structure for Datadog
40
+ log_entry = {
41
+ "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
42
+ "level": record.levelname,
43
+ "logger": record.name,
44
+ "message": record.getMessage(),
45
+ "service": self.service_name,
46
+ "version": self.version,
47
+ "env": self.environment,
48
+ "process_id": record.process,
49
+ }
50
+
51
+ # --- Datadog correlation IDs (must be at top level) ---
52
+ # First check nested dd fields
53
+ if hasattr(record, "dd"):
54
+ dd_fields = record.dd
55
+ if "trace_id" in dd_fields:
56
+ log_entry["dd.trace_id"] = dd_fields["trace_id"]
57
+ if "span_id" in dd_fields:
58
+ log_entry["dd.span_id"] = dd_fields["span_id"]
59
+ if "service" in dd_fields:
60
+ log_entry["dd.service"] = dd_fields["service"]
61
+ if "version" in dd_fields:
62
+ log_entry["dd.version"] = dd_fields["version"]
63
+ if "env" in dd_fields:
64
+ log_entry["dd.env"] = dd_fields["env"]
65
+
66
+ # Then check for flat Datadog attributes that ddtrace may inject
67
+ dd_flat_keys = ["dd.trace_id", "dd.span_id", "dd.service", "dd.version", "dd.env"]
68
+ for dd_key in dd_flat_keys:
69
+ # Only use flat key if we don't already have this field from record.dd
70
+ if dd_key not in log_entry and hasattr(record, dd_key):
71
+ log_entry[dd_key] = getattr(record, dd_key)
72
+
73
+ # --- Add source info ---
74
+ if record.pathname:
75
+ log_entry["source"] = {
76
+ "file": record.pathname,
77
+ "line": record.lineno,
78
+ "function": record.funcName,
79
+ }
80
+
81
+ # --- Collect remaining extras, except dd (already flattened) ---
82
+ extra_fields = {
83
+ k: v
84
+ for k, v in record.__dict__.items()
85
+ if k
86
+ not in {
87
+ "name",
88
+ "msg",
89
+ "args",
90
+ "levelname",
91
+ "levelno",
92
+ "pathname",
93
+ "filename",
94
+ "module",
95
+ "lineno",
96
+ "funcName",
97
+ "created",
98
+ "msecs",
99
+ "relativeCreated",
100
+ "thread",
101
+ "threadName",
102
+ "processName",
103
+ "process",
104
+ "getMessage",
105
+ "exc_info",
106
+ "exc_text",
107
+ "stack_info",
108
+ "message",
109
+ "dd", # skip dd because we handled it
110
+ "dd.trace_id", # skip flat dd keys because we handled them
111
+ "dd.span_id",
112
+ "dd.service",
113
+ "dd.version",
114
+ "dd.env",
115
+ }
116
+ }
117
+ if extra_fields:
118
+ log_entry["extra"] = extra_fields
119
+
120
+ # --- Exception handling ---
121
+ if record.exc_info:
122
+ log_entry["exception"] = {
123
+ "class": record.exc_info[0].__name__ if record.exc_info[0] else None,
124
+ "message": str(record.exc_info[1]) if record.exc_info[1] else None,
125
+ "traceback": self.formatException(record.exc_info),
126
+ }
127
+
128
+ if record.stack_info:
129
+ log_entry["stack_trace"] = record.stack_info
130
+
131
+ return json.dumps(log_entry, default=str, ensure_ascii=False)
132
+
@@ -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(target=self._worker, daemon=True)
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.logs_api.submit_log(body=http_log)
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
@@ -1,48 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ### Added
11
- - Initial release of datadog-http-handler
12
- - Asynchronous HTTP logging handler for Datadog
13
- - Batching support with configurable batch size and flush intervals
14
- - Automatic retry logic with exponential backoff
15
- - Comprehensive error handling and graceful degradation
16
- - Type hints and mypy compatibility
17
- - Support for all Datadog sites (US, EU, Government, etc.)
18
- - Environment variable configuration support
19
- - Framework integration examples (Django, FastAPI, Flask)
20
- - Background worker thread for non-blocking log processing
21
- - Automatic tag enrichment (environment, service, version)
22
- - Memory-efficient queue management
23
- - Graceful shutdown handling
24
-
25
- ### Changed
26
- - N/A (initial release)
27
-
28
- ### Deprecated
29
- - N/A (initial release)
30
-
31
- ### Removed
32
- - N/A (initial release)
33
-
34
- ### Fixed
35
- - N/A (initial release)
36
-
37
- ### Security
38
- - N/A (initial release)
39
-
40
- ## [0.1.0] - 2025-08-25
41
-
42
- ### Added
43
- - Initial development version
44
- - Core DatadogHTTPHandler implementation
45
- - Basic test suite
46
- - Documentation and examples
47
- - Modern Python packaging with Hatch
48
- - Comprehensive development tooling (Ruff, mypy, pytest)