datadog-async-handler 0.1.2__py3-none-any.whl
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.2.dist-info/METADATA +294 -0
- datadog_async_handler-0.1.2.dist-info/RECORD +7 -0
- datadog_async_handler-0.1.2.dist-info/WHEEL +4 -0
- datadog_async_handler-0.1.2.dist-info/licenses/LICENSE +21 -0
- datadog_http_handler/__init__.py +27 -0
- datadog_http_handler/handler.py +309 -0
- datadog_http_handler/py.typed +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datadog-async-handler
|
|
3
|
+
Version: 0.1.2
|
|
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
|
|
10
|
+
Author-email: Parth <parth@enlyft.com>
|
|
11
|
+
Maintainer-email: Parth <parth@enlyft.com>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: async,batching,datadog,handler,logging,observability
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Topic :: System :: Logging
|
|
27
|
+
Classifier: Topic :: System :: Monitoring
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Requires-Dist: datadog-api-client<3.0.0,>=2.0.0
|
|
30
|
+
Provides-Extra: async
|
|
31
|
+
Requires-Dist: aiohttp>=3.8.0; extra == 'async'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pre-commit>=3.6.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-benchmark>=4.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.2.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: tox>=4.0.0; extra == 'dev'
|
|
42
|
+
Provides-Extra: docs
|
|
43
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
|
|
44
|
+
Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
|
|
45
|
+
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == 'docs'
|
|
46
|
+
Provides-Extra: test
|
|
47
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
48
|
+
Requires-Dist: pytest-benchmark>=4.0.0; extra == 'test'
|
|
49
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
|
|
50
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == 'test'
|
|
51
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# Datadog Async Handler
|
|
55
|
+
|
|
56
|
+
[](https://badge.fury.io/py/datadog-async-handler)
|
|
57
|
+
[](https://pypi.org/project/datadog-async-handler/)
|
|
58
|
+
[](https://opensource.org/licenses/MIT)
|
|
59
|
+
[](https://github.com/astral-sh/ruff)
|
|
60
|
+
|
|
61
|
+
A modern, high-performance Python logging handler that sends logs directly to Datadog via HTTP API with asynchronous batching, retry logic, and comprehensive error handling.
|
|
62
|
+
|
|
63
|
+
## ✨ Features
|
|
64
|
+
|
|
65
|
+
- **🚀 High Performance**: Asynchronous batching and background processing
|
|
66
|
+
- **🔄 Reliable Delivery**: Automatic retry with exponential backoff
|
|
67
|
+
- **📊 Batching**: Configurable batch size and flush intervals
|
|
68
|
+
- **🏷️ Rich Metadata**: Automatic service, environment, and custom tag support
|
|
69
|
+
- **🔧 Easy Integration**: Drop-in replacement for standard logging handlers
|
|
70
|
+
- **🌐 Multi-Site Support**: Works with all Datadog sites (US, EU, etc.)
|
|
71
|
+
- **📝 Type Safe**: Full type hints and mypy compatibility
|
|
72
|
+
- **⚡ Modern**: Built with Python 3.9+ and latest best practices
|
|
73
|
+
|
|
74
|
+
## 🚀 Quick Start
|
|
75
|
+
|
|
76
|
+
### Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install datadog-async-handler
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Basic Usage
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import logging
|
|
86
|
+
from datadog_http_handler import DatadogHTTPHandler
|
|
87
|
+
|
|
88
|
+
# Configure the handler
|
|
89
|
+
handler = DatadogHTTPHandler(
|
|
90
|
+
api_key="your-datadog-api-key", # or set DD_API_KEY env var
|
|
91
|
+
service="my-application",
|
|
92
|
+
source="python",
|
|
93
|
+
tags="env:production,team:backend"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Set up logging
|
|
97
|
+
logger = logging.getLogger(__name__)
|
|
98
|
+
logger.addHandler(handler)
|
|
99
|
+
logger.setLevel(logging.INFO)
|
|
100
|
+
|
|
101
|
+
# Start logging!
|
|
102
|
+
logger.info("Application started successfully", extra={
|
|
103
|
+
"user_id": "12345",
|
|
104
|
+
"action": "startup"
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Environment Variables
|
|
109
|
+
|
|
110
|
+
The handler automatically picks up standard Datadog environment variables:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export DD_API_KEY="your-api-key"
|
|
114
|
+
export DD_SERVICE="my-application"
|
|
115
|
+
export DD_ENV="production"
|
|
116
|
+
export DD_VERSION="1.2.3"
|
|
117
|
+
export DD_TAGS="team:backend,component:api"
|
|
118
|
+
export DD_SITE="datadoghq.com" # or datadoghq.eu, ddog-gov.com, etc.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 📖 Documentation
|
|
122
|
+
|
|
123
|
+
### Configuration Options
|
|
124
|
+
|
|
125
|
+
| Parameter | Type | Default | Description |
|
|
126
|
+
|-----------|------|---------|-------------|
|
|
127
|
+
| `api_key` | `str` | `None` | Datadog API key (required) |
|
|
128
|
+
| `site` | `str` | `"datadoghq.com"` | Datadog site |
|
|
129
|
+
| `service` | `str` | `None` | Service name |
|
|
130
|
+
| `source` | `str` | `"python"` | Log source |
|
|
131
|
+
| `hostname` | `str` | `None` | Hostname |
|
|
132
|
+
| `tags` | `str` | `None` | Comma-separated tags |
|
|
133
|
+
| `batch_size` | `int` | `10` | Number of logs per batch |
|
|
134
|
+
| `flush_interval_seconds` | `float` | `5.0` | Batch flush interval |
|
|
135
|
+
| `timeout_seconds` | `float` | `10.0` | Request timeout |
|
|
136
|
+
| `max_retries` | `int` | `3` | Maximum retry attempts |
|
|
137
|
+
|
|
138
|
+
### Framework Integration Examples
|
|
139
|
+
|
|
140
|
+
#### Django
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# settings.py
|
|
144
|
+
LOGGING = {
|
|
145
|
+
'version': 1,
|
|
146
|
+
'disable_existing_loggers': False,
|
|
147
|
+
'handlers': {
|
|
148
|
+
'datadog': {
|
|
149
|
+
'class': 'datadog_http_handler.DatadogHTTPHandler',
|
|
150
|
+
'api_key': 'your-api-key',
|
|
151
|
+
'service': 'django-app',
|
|
152
|
+
'source': 'django',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
'root': {
|
|
156
|
+
'handlers': ['datadog'],
|
|
157
|
+
'level': 'INFO',
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### FastAPI
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
import logging
|
|
166
|
+
from fastapi import FastAPI
|
|
167
|
+
from datadog_http_handler import DatadogHTTPHandler
|
|
168
|
+
|
|
169
|
+
app = FastAPI()
|
|
170
|
+
|
|
171
|
+
# Configure logging
|
|
172
|
+
handler = DatadogHTTPHandler(service="fastapi-app", source="fastapi")
|
|
173
|
+
logging.getLogger().addHandler(handler)
|
|
174
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
175
|
+
|
|
176
|
+
@app.get("/")
|
|
177
|
+
async def root():
|
|
178
|
+
logging.info("API endpoint called", extra={"endpoint": "/"})
|
|
179
|
+
return {"message": "Hello World"}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### Flask
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import logging
|
|
186
|
+
from flask import Flask
|
|
187
|
+
from datadog_http_handler import DatadogHTTPHandler
|
|
188
|
+
|
|
189
|
+
app = Flask(__name__)
|
|
190
|
+
|
|
191
|
+
# Configure logging
|
|
192
|
+
handler = DatadogHTTPHandler(service="flask-app", source="flask")
|
|
193
|
+
app.logger.addHandler(handler)
|
|
194
|
+
app.logger.setLevel(logging.INFO)
|
|
195
|
+
|
|
196
|
+
@app.route("/")
|
|
197
|
+
def hello():
|
|
198
|
+
app.logger.info("Flask endpoint called", extra={"endpoint": "/"})
|
|
199
|
+
return "Hello World!"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## 🔧 Development
|
|
203
|
+
|
|
204
|
+
### Setup
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Clone the repository
|
|
208
|
+
git clone https://github.com/enlyft/datadog-http-handler.git
|
|
209
|
+
cd datadog-http-handler
|
|
210
|
+
|
|
211
|
+
# Install UV (modern Python package manager)
|
|
212
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
213
|
+
|
|
214
|
+
# Install dependencies
|
|
215
|
+
uv pip install -e ".[dev]"
|
|
216
|
+
|
|
217
|
+
# Install pre-commit hooks
|
|
218
|
+
pre-commit install
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Running Tests
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Run all tests
|
|
225
|
+
hatch run test
|
|
226
|
+
|
|
227
|
+
# Run with coverage
|
|
228
|
+
hatch run test-cov
|
|
229
|
+
|
|
230
|
+
# Run specific tests
|
|
231
|
+
hatch run test tests/test_handler.py::test_basic_logging
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Code Quality
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Format code
|
|
238
|
+
hatch run format
|
|
239
|
+
|
|
240
|
+
# Lint code
|
|
241
|
+
hatch run lint
|
|
242
|
+
|
|
243
|
+
# Type checking
|
|
244
|
+
hatch run type-check
|
|
245
|
+
|
|
246
|
+
# Run all checks
|
|
247
|
+
hatch run all
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 🤝 Contributing
|
|
251
|
+
|
|
252
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
253
|
+
|
|
254
|
+
1. Fork the repository
|
|
255
|
+
2. Create a feature branch
|
|
256
|
+
3. Make your changes
|
|
257
|
+
4. Add tests
|
|
258
|
+
5. Run the test suite
|
|
259
|
+
6. Submit a pull request
|
|
260
|
+
|
|
261
|
+
## 📋 Requirements
|
|
262
|
+
|
|
263
|
+
- Python 3.9+
|
|
264
|
+
- `datadog-api-client>=2.0.0`
|
|
265
|
+
|
|
266
|
+
## 📄 License
|
|
267
|
+
|
|
268
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
269
|
+
|
|
270
|
+
## 🔗 Links
|
|
271
|
+
|
|
272
|
+
- [Documentation](https://enlyft.github.io/datadog-http-handler)
|
|
273
|
+
- [PyPI Package](https://pypi.org/project/datadog-async-handler/)
|
|
274
|
+
- [GitHub Repository](https://github.com/enlyft/datadog-http-handler)
|
|
275
|
+
- [Issue Tracker](https://github.com/enlyft/datadog-http-handler/issues)
|
|
276
|
+
- [Datadog Logs API Documentation](https://docs.datadoghq.com/api/latest/logs/)
|
|
277
|
+
|
|
278
|
+
## 🆚 Comparison with Other Solutions
|
|
279
|
+
|
|
280
|
+
| Feature | datadog-async-handler | datadog-http-handler | python-datadog | datadog-logger |
|
|
281
|
+
|---------|----------------------|---------------------|----------------|----------------|
|
|
282
|
+
| Async Batching | ✅ | ❌ | ❌ | ❌ |
|
|
283
|
+
| Retry Logic | ✅ | ❌ | ❌ | ❌ |
|
|
284
|
+
| Type Hints | ✅ | ❌ | ❌ | ❌ |
|
|
285
|
+
| Modern Python | ✅ (3.9+) | ❌ (3.6+) | ❌ (2.7+) | ❌ (3.6+) |
|
|
286
|
+
| Official API Client | ✅ | ❌ | ❌ | ❌ |
|
|
287
|
+
| Background Processing | ✅ | ❌ | ❌ | ❌ |
|
|
288
|
+
| Memory Efficient | ✅ | ❌ | ❌ | ❌ |
|
|
289
|
+
| Active Maintenance | ✅ | ❌ (2019) | ✅ | ❌ |
|
|
290
|
+
| Comprehensive Tests | ✅ | ❌ | ✅ | ❌ |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
Made with ❤️ for the Python and Datadog communities.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
datadog_http_handler/__init__.py,sha256=opKEnLnS-qXQ-alt7EPRQQk91E8MEwEznQ4-3k4-92s,744
|
|
2
|
+
datadog_http_handler/handler.py,sha256=_pJAK1iKgVO4mR2dTL9FDlNi4oJVkq_D59R69aH7Sok,10660
|
|
3
|
+
datadog_http_handler/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
datadog_async_handler-0.1.2.dist-info/METADATA,sha256=CQilYl9Lm_A0zDvb6Cqlj5VZQUUVSdBWKNqwLqiOGhc,9017
|
|
5
|
+
datadog_async_handler-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
datadog_async_handler-0.1.2.dist-info/licenses/LICENSE,sha256=E4Va_3PEQq8kxwFMY3NDGGjDbiklFYi-Z6QJI0NEfq8,1061
|
|
7
|
+
datadog_async_handler-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Parth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Datadog HTTP Handler - Modern Python logging handler for Datadog.
|
|
3
|
+
|
|
4
|
+
This package provides a high-performance, asynchronous logging handler that sends
|
|
5
|
+
logs directly to Datadog's HTTP intake API with batching, retry logic, and
|
|
6
|
+
comprehensive error handling.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
Basic usage:
|
|
10
|
+
|
|
11
|
+
>>> import logging
|
|
12
|
+
>>> from datadog_http_handler import DatadogHTTPHandler
|
|
13
|
+
>>>
|
|
14
|
+
>>> handler = DatadogHTTPHandler(
|
|
15
|
+
... api_key="your-api-key",
|
|
16
|
+
... service="my-app",
|
|
17
|
+
... tags="env:production"
|
|
18
|
+
... )
|
|
19
|
+
>>> logger = logging.getLogger(__name__)
|
|
20
|
+
>>> logger.addHandler(handler)
|
|
21
|
+
>>> logger.info("Hello Datadog!")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .handler import DatadogHTTPHandler
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
__all__ = ["DatadogHTTPHandler"]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Datadog HTTP logging handler implementation.
|
|
3
|
+
|
|
4
|
+
This module provides the DatadogHTTPHandler class, a modern Python logging handler
|
|
5
|
+
that sends logs to Datadog via HTTP API with asynchronous batching and retry logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from queue import Empty, Queue
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from datadog_api_client import ApiClient, Configuration
|
|
16
|
+
from datadog_api_client.v2.api.logs_api import LogsApi
|
|
17
|
+
from datadog_api_client.v2.model.http_log import HTTPLog
|
|
18
|
+
from datadog_api_client.v2.model.http_log_item import HTTPLogItem
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatadogHTTPHandler(logging.Handler):
|
|
22
|
+
"""
|
|
23
|
+
High-performance logging handler that sends logs to Datadog via HTTP API.
|
|
24
|
+
|
|
25
|
+
This handler batches logs and sends them asynchronously to avoid blocking
|
|
26
|
+
the main application thread. It includes retry logic with exponential backoff,
|
|
27
|
+
comprehensive error handling, and support for all Datadog sites.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
api_key: Datadog API key (or set DD_API_KEY env var)
|
|
31
|
+
site: Datadog site (default: datadoghq.com)
|
|
32
|
+
service: Service name for logs
|
|
33
|
+
source: Log source (default: python)
|
|
34
|
+
hostname: Hostname for logs
|
|
35
|
+
tags: Tags to add to logs (comma-separated string)
|
|
36
|
+
batch_size: Number of logs to batch before sending
|
|
37
|
+
flush_interval_seconds: How often to flush logs (seconds)
|
|
38
|
+
timeout_seconds: Request timeout
|
|
39
|
+
max_retries: Maximum retry attempts
|
|
40
|
+
level: Logging level
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
Basic usage:
|
|
44
|
+
|
|
45
|
+
>>> import logging
|
|
46
|
+
>>> handler = DatadogHTTPHandler(
|
|
47
|
+
... api_key="your-api-key",
|
|
48
|
+
... service="my-app",
|
|
49
|
+
... tags="env:production,team:backend"
|
|
50
|
+
... )
|
|
51
|
+
>>> logger = logging.getLogger(__name__)
|
|
52
|
+
>>> logger.addHandler(handler)
|
|
53
|
+
>>> logger.info("Application started")
|
|
54
|
+
|
|
55
|
+
With environment variables:
|
|
56
|
+
|
|
57
|
+
>>> # Set DD_API_KEY, DD_SERVICE, DD_ENV, etc.
|
|
58
|
+
>>> handler = DatadogHTTPHandler() # Uses env vars
|
|
59
|
+
>>> logger = logging.getLogger(__name__)
|
|
60
|
+
>>> logger.addHandler(handler)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
api_key: Optional[str] = None,
|
|
66
|
+
site: str = "datadoghq.com",
|
|
67
|
+
service: Optional[str] = None,
|
|
68
|
+
source: str = "python",
|
|
69
|
+
hostname: Optional[str] = None,
|
|
70
|
+
tags: Optional[str] = None,
|
|
71
|
+
batch_size: int = 10,
|
|
72
|
+
flush_interval_seconds: float = 5.0,
|
|
73
|
+
timeout_seconds: float = 10.0,
|
|
74
|
+
max_retries: int = 3,
|
|
75
|
+
level: int = logging.NOTSET,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Initialize the Datadog HTTP handler."""
|
|
78
|
+
super().__init__(level)
|
|
79
|
+
|
|
80
|
+
# API Configuration
|
|
81
|
+
self.api_key = api_key or os.getenv("DD_API_KEY")
|
|
82
|
+
if not self.api_key:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Datadog API key is required. Set DD_API_KEY env var or pass api_key parameter."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.site = site or os.getenv("DD_SITE", "datadoghq.com")
|
|
88
|
+
self.service = service or os.getenv("DD_SERVICE", "unknown")
|
|
89
|
+
self.source = source
|
|
90
|
+
self.hostname = hostname or os.getenv("DD_HOSTNAME")
|
|
91
|
+
self.tags = tags or os.getenv("DD_TAGS", "")
|
|
92
|
+
|
|
93
|
+
# Batch settings
|
|
94
|
+
self.batch_size = max(1, batch_size) # Ensure at least 1
|
|
95
|
+
self.flush_interval = max(0.1, flush_interval_seconds) # Minimum 100ms
|
|
96
|
+
self.timeout = max(1.0, timeout_seconds) # Minimum 1 second
|
|
97
|
+
self.max_retries = max(0, max_retries) # No negative retries
|
|
98
|
+
|
|
99
|
+
# Initialize API client
|
|
100
|
+
self._setup_api_client()
|
|
101
|
+
|
|
102
|
+
# Background processing
|
|
103
|
+
self._log_queue: Queue[HTTPLogItem] = Queue()
|
|
104
|
+
self._stop_event = threading.Event()
|
|
105
|
+
self._worker_thread: Optional[threading.Thread] = None
|
|
106
|
+
self._start_worker()
|
|
107
|
+
|
|
108
|
+
def _setup_api_client(self) -> None:
|
|
109
|
+
"""Setup the Datadog API client."""
|
|
110
|
+
configuration = Configuration()
|
|
111
|
+
configuration.api_key["apiKeyAuth"] = self.api_key
|
|
112
|
+
configuration.server_variables["site"] = self.site
|
|
113
|
+
|
|
114
|
+
self.api_client = ApiClient(configuration)
|
|
115
|
+
self.logs_api = LogsApi(self.api_client)
|
|
116
|
+
|
|
117
|
+
def _start_worker(self) -> None:
|
|
118
|
+
"""Start the background worker thread."""
|
|
119
|
+
if self._worker_thread is None or not self._worker_thread.is_alive():
|
|
120
|
+
self._worker_thread = threading.Thread(target=self._worker, daemon=True)
|
|
121
|
+
self._worker_thread.start()
|
|
122
|
+
|
|
123
|
+
def _worker(self) -> None:
|
|
124
|
+
"""Background worker that processes log batches."""
|
|
125
|
+
batch: list[HTTPLogItem] = []
|
|
126
|
+
last_flush = time.time()
|
|
127
|
+
|
|
128
|
+
while not self._stop_event.is_set():
|
|
129
|
+
try:
|
|
130
|
+
# Get log from queue with timeout
|
|
131
|
+
log_item = self._log_queue.get(timeout=0.1)
|
|
132
|
+
batch.append(log_item)
|
|
133
|
+
|
|
134
|
+
# Check if we should flush
|
|
135
|
+
should_flush = len(batch) >= self.batch_size or (
|
|
136
|
+
batch and time.time() - last_flush >= self.flush_interval
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if should_flush:
|
|
140
|
+
self._send_batch(batch)
|
|
141
|
+
batch.clear()
|
|
142
|
+
last_flush = time.time()
|
|
143
|
+
|
|
144
|
+
except Empty:
|
|
145
|
+
# No logs in queue, check if we should flush existing batch
|
|
146
|
+
if batch and time.time() - last_flush >= self.flush_interval:
|
|
147
|
+
self._send_batch(batch)
|
|
148
|
+
batch.clear()
|
|
149
|
+
last_flush = time.time()
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Flush remaining logs on shutdown
|
|
153
|
+
if batch:
|
|
154
|
+
self._send_batch(batch)
|
|
155
|
+
|
|
156
|
+
def _send_batch(self, batch: list[HTTPLogItem]) -> None:
|
|
157
|
+
"""Send a batch of logs to Datadog."""
|
|
158
|
+
if not batch:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
for attempt in range(self.max_retries + 1):
|
|
162
|
+
try:
|
|
163
|
+
http_log = HTTPLog(batch)
|
|
164
|
+
self.logs_api.submit_log(body=http_log)
|
|
165
|
+
return # Success
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
if attempt == self.max_retries:
|
|
169
|
+
# Last attempt failed, give up
|
|
170
|
+
self._handle_error(
|
|
171
|
+
f"Failed to send logs after {self.max_retries + 1} attempts: {e}"
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
# Wait before retry (exponential backoff)
|
|
175
|
+
time.sleep(2**attempt)
|
|
176
|
+
|
|
177
|
+
def _handle_error(self, message: str) -> None:
|
|
178
|
+
"""Handle errors that occur during log submission."""
|
|
179
|
+
# Log to stderr to avoid infinite recursion
|
|
180
|
+
print(f"DatadogHTTPHandler error: {message}", file=__import__("sys").stderr)
|
|
181
|
+
|
|
182
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
183
|
+
"""Emit a log record."""
|
|
184
|
+
try:
|
|
185
|
+
# Ensure handler is properly initialized
|
|
186
|
+
if not hasattr(self, "_log_queue") or not hasattr(self, "api_key"):
|
|
187
|
+
# Handler not properly initialized, skip logging to avoid errors
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
log_item = self._format_log_item(record)
|
|
191
|
+
self._log_queue.put(log_item, block=False)
|
|
192
|
+
except Exception:
|
|
193
|
+
self.handleError(record)
|
|
194
|
+
|
|
195
|
+
def _format_log_item(self, record: logging.LogRecord) -> HTTPLogItem:
|
|
196
|
+
"""Convert a LogRecord to a Datadog HTTPLogItem."""
|
|
197
|
+
# Format the message
|
|
198
|
+
message = self.format(record)
|
|
199
|
+
|
|
200
|
+
# Build the log item
|
|
201
|
+
log_item = HTTPLogItem(
|
|
202
|
+
message=message,
|
|
203
|
+
ddsource=self.source,
|
|
204
|
+
service=self.service,
|
|
205
|
+
hostname=self.hostname,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Add tags
|
|
209
|
+
tags_list = []
|
|
210
|
+
if self.tags:
|
|
211
|
+
tags_list.extend(self.tags.split(","))
|
|
212
|
+
|
|
213
|
+
# Add log level as tag
|
|
214
|
+
tags_list.append(f"level:{record.levelname.lower()}")
|
|
215
|
+
|
|
216
|
+
# Add logger name as tag
|
|
217
|
+
tags_list.append(f"logger:{record.name}")
|
|
218
|
+
|
|
219
|
+
# Add environment if available
|
|
220
|
+
env = os.getenv("DD_ENV")
|
|
221
|
+
if env:
|
|
222
|
+
tags_list.append(f"env:{env}")
|
|
223
|
+
|
|
224
|
+
# Add version if available
|
|
225
|
+
version = os.getenv("DD_VERSION")
|
|
226
|
+
if version:
|
|
227
|
+
tags_list.append(f"version:{version}")
|
|
228
|
+
|
|
229
|
+
# Add any extra fields from the log record
|
|
230
|
+
if hasattr(record, "__dict__"):
|
|
231
|
+
for key, value in record.__dict__.items():
|
|
232
|
+
if key.startswith("dd_") and isinstance(value, (str, int, float, bool)):
|
|
233
|
+
# Custom Datadog fields
|
|
234
|
+
tags_list.append(f"{key[3:]}:{value}")
|
|
235
|
+
|
|
236
|
+
if tags_list:
|
|
237
|
+
log_item.ddtags = ",".join(tags_list)
|
|
238
|
+
|
|
239
|
+
return log_item
|
|
240
|
+
|
|
241
|
+
def flush(self) -> None:
|
|
242
|
+
"""Flush any pending logs."""
|
|
243
|
+
# Signal worker to process remaining logs
|
|
244
|
+
if (
|
|
245
|
+
hasattr(self, "_worker_thread")
|
|
246
|
+
and self._worker_thread
|
|
247
|
+
and self._worker_thread.is_alive()
|
|
248
|
+
):
|
|
249
|
+
# Wait a bit for the worker to process
|
|
250
|
+
time.sleep(0.1)
|
|
251
|
+
|
|
252
|
+
def close(self) -> None:
|
|
253
|
+
"""Close the handler and cleanup resources."""
|
|
254
|
+
# Stop the worker
|
|
255
|
+
if hasattr(self, "_stop_event"):
|
|
256
|
+
self._stop_event.set()
|
|
257
|
+
|
|
258
|
+
# Wait for worker to finish
|
|
259
|
+
if (
|
|
260
|
+
hasattr(self, "_worker_thread")
|
|
261
|
+
and self._worker_thread
|
|
262
|
+
and self._worker_thread.is_alive()
|
|
263
|
+
):
|
|
264
|
+
self._worker_thread.join(timeout=5.0)
|
|
265
|
+
|
|
266
|
+
# Close API client
|
|
267
|
+
if hasattr(self, "api_client"):
|
|
268
|
+
self.api_client.close()
|
|
269
|
+
|
|
270
|
+
super().close()
|
|
271
|
+
|
|
272
|
+
def health_check(self) -> bool:
|
|
273
|
+
"""
|
|
274
|
+
Perform a health check to verify the handler is working.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
True if the handler is healthy, False otherwise.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
# Check if worker thread is alive
|
|
281
|
+
if not self._worker_thread or not self._worker_thread.is_alive():
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
# Check if we can create a test log item
|
|
285
|
+
test_record = logging.LogRecord(
|
|
286
|
+
name="health_check",
|
|
287
|
+
level=logging.INFO,
|
|
288
|
+
pathname="",
|
|
289
|
+
lineno=0,
|
|
290
|
+
msg="Health check",
|
|
291
|
+
args=(),
|
|
292
|
+
exc_info=None,
|
|
293
|
+
)
|
|
294
|
+
self._format_log_item(test_record)
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
except Exception:
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
def get_queue_size(self) -> int:
|
|
301
|
+
"""Get the current size of the log queue."""
|
|
302
|
+
return self._log_queue.qsize() if hasattr(self, "_log_queue") else 0
|
|
303
|
+
|
|
304
|
+
def __repr__(self) -> str:
|
|
305
|
+
"""Return a string representation of the handler."""
|
|
306
|
+
return (
|
|
307
|
+
f"DatadogHTTPHandler(service={self.service!r}, "
|
|
308
|
+
f"source={self.source!r}, site={self.site!r})"
|
|
309
|
+
)
|
|
File without changes
|