goldcast-cl-logger 0.2.8__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.
- goldcast_cl_logger-0.2.8/LICENSE +21 -0
- goldcast_cl_logger-0.2.8/PKG-INFO +336 -0
- goldcast_cl_logger-0.2.8/README.md +310 -0
- goldcast_cl_logger-0.2.8/cl_logger/__init__.py +49 -0
- goldcast_cl_logger-0.2.8/cl_logger/http_utils.py +121 -0
- goldcast_cl_logger-0.2.8/cl_logger/logger.py +206 -0
- goldcast_cl_logger-0.2.8/cl_logger/sqs_utils.py +178 -0
- goldcast_cl_logger-0.2.8/cl_logger/trace_context.py +71 -0
- goldcast_cl_logger-0.2.8/pyproject.toml +38 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Content Lab
|
|
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,336 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: goldcast-cl-logger
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: A lightweight, flexible logging library for Content Lab projects with distributed tracing support
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Goldcast
|
|
7
|
+
Author-email: engineering@goldcast.io
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: System :: Logging
|
|
19
|
+
Provides-Extra: http
|
|
20
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0) ; extra == "http"
|
|
21
|
+
Project-URL: Bug Tracker, https://github.com/goldcast/content-lab-backend/issues
|
|
22
|
+
Project-URL: Documentation, https://github.com/goldcast/content-lab-backend/tree/main/libraries/cl-logger
|
|
23
|
+
Project-URL: Repository, https://github.com/goldcast/content-lab-backend
|
|
24
|
+
Project-URL: Source Code, https://github.com/goldcast/content-lab-backend/tree/main/libraries/cl-logger
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# goldcast-cl-logger
|
|
28
|
+
|
|
29
|
+
A lightweight, flexible logging library for Content Lab projects with distributed tracing support.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Install from GitHub Packages (recommended)
|
|
35
|
+
pip install goldcast-cl-logger
|
|
36
|
+
|
|
37
|
+
# Install with HTTP extras for traced requests
|
|
38
|
+
pip install goldcast-cl-logger[http]
|
|
39
|
+
|
|
40
|
+
# Add to requirements.txt
|
|
41
|
+
goldcast-cl-logger==0.2.8
|
|
42
|
+
|
|
43
|
+
# Add to pyproject.toml (Poetry)
|
|
44
|
+
[[tool.poetry.source]]
|
|
45
|
+
name = "github"
|
|
46
|
+
url = "https://ghcr.io/goldcast"
|
|
47
|
+
|
|
48
|
+
[tool.poetry.dependencies]
|
|
49
|
+
goldcast-cl-logger = { version = "^0.2.8", source = "github" }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Simple API**: Easy to use with minimal configuration
|
|
55
|
+
- **JSON Logging by Default**: Structured JSON logging for better observability
|
|
56
|
+
- **Distributed Tracing**: Built-in support for trace ID propagation
|
|
57
|
+
- **Sentry Integration**: Compatible with Sentry's distributed tracing
|
|
58
|
+
- **Environment Configuration**: Control logging format via environment variables
|
|
59
|
+
- **Extra Context**: Add custom fields to log entries
|
|
60
|
+
- **File Logging**: Optional file output support
|
|
61
|
+
- **Zero Dependencies**: Uses only Python standard library (requests is optional)
|
|
62
|
+
- **HTTP & SQS Propagation**: Automatic trace ID propagation for external calls
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from cl_logger import get_logger
|
|
68
|
+
|
|
69
|
+
# Get a logger instance
|
|
70
|
+
logger = get_logger(__name__)
|
|
71
|
+
|
|
72
|
+
# Basic logging (JSON by default)
|
|
73
|
+
logger.info("Application started")
|
|
74
|
+
logger.error("Something went wrong")
|
|
75
|
+
|
|
76
|
+
# With extra context
|
|
77
|
+
logger.info("User action", extra={"user_id": 123, "action": "login"})
|
|
78
|
+
|
|
79
|
+
# Exception logging with traceback
|
|
80
|
+
try:
|
|
81
|
+
risky_operation()
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.exception("Operation failed", extra={"operation": "risky_operation"})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Distributed Tracing
|
|
87
|
+
|
|
88
|
+
### Automatic Trace ID Propagation
|
|
89
|
+
|
|
90
|
+
The logger automatically includes trace IDs in all log entries when used with the provided middleware:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Django middleware automatically creates/extracts trace IDs
|
|
94
|
+
# In settings.py:
|
|
95
|
+
MIDDLEWARE = [
|
|
96
|
+
# ...
|
|
97
|
+
'content_lab_backend.middleware.logging_middleware.RequestLoggingMiddleware',
|
|
98
|
+
# ...
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Manual Trace Context
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from cl_logger import TraceContext, get_trace_id
|
|
106
|
+
|
|
107
|
+
# Create a new trace context
|
|
108
|
+
with TraceContext() as ctx:
|
|
109
|
+
logger.info("Starting operation")
|
|
110
|
+
# trace_id is automatically included in logs
|
|
111
|
+
current_trace_id = get_trace_id()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### HTTP Request Propagation
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from cl_logger import http_utils
|
|
118
|
+
|
|
119
|
+
# Use traced HTTP client for automatic trace propagation
|
|
120
|
+
response = http_utils.get("https://api.example.com/data")
|
|
121
|
+
response = http_utils.post("https://api.example.com/users", json={"name": "John"})
|
|
122
|
+
|
|
123
|
+
# Or use a traced session
|
|
124
|
+
session = http_utils.TracedSession()
|
|
125
|
+
response = session.get("https://api.example.com/data")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### SQS Message Propagation
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import boto3
|
|
132
|
+
from cl_logger import sqs_utils
|
|
133
|
+
|
|
134
|
+
# Wrap SQS client for automatic trace propagation
|
|
135
|
+
sqs = boto3.client('sqs')
|
|
136
|
+
traced_sqs = sqs_utils.TracedSQSClient(sqs)
|
|
137
|
+
|
|
138
|
+
# Send message with trace ID
|
|
139
|
+
traced_sqs.send_message(
|
|
140
|
+
QueueUrl='https://sqs.region.amazonaws.com/account/queue',
|
|
141
|
+
MessageBody='Hello World'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Process incoming SQS message with trace context
|
|
145
|
+
def handle_message(message):
|
|
146
|
+
logger.info("Processing message")
|
|
147
|
+
# trace_id from message is automatically set
|
|
148
|
+
|
|
149
|
+
sqs_utils.process_sqs_message(sqs_message, handle_message)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Configuration
|
|
153
|
+
|
|
154
|
+
### Environment Variables
|
|
155
|
+
|
|
156
|
+
- `CL_JSON_LOGGING`: Set to `"false"` to disable JSON logging (default: `"true"`)
|
|
157
|
+
|
|
158
|
+
### Programmatic Configuration
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from cl_logger import CLLogger
|
|
162
|
+
|
|
163
|
+
# Create logger with specific settings
|
|
164
|
+
logger = CLLogger(
|
|
165
|
+
name="my_app",
|
|
166
|
+
level="DEBUG",
|
|
167
|
+
json_logging=True, # Default
|
|
168
|
+
log_to_file="app.log"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Toggle to normal logging at runtime
|
|
172
|
+
logger.set_json_logging(False)
|
|
173
|
+
|
|
174
|
+
# Change log level
|
|
175
|
+
logger.set_level("WARNING")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Output Examples
|
|
179
|
+
|
|
180
|
+
### JSON Logging (Default)
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"timestamp": "2024-01-15T10:30:45.123Z",
|
|
184
|
+
"level": "INFO",
|
|
185
|
+
"logger": "my_app",
|
|
186
|
+
"message": "User action",
|
|
187
|
+
"module": "views",
|
|
188
|
+
"function": "login",
|
|
189
|
+
"line": 42,
|
|
190
|
+
"trace_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
191
|
+
"user_id": 123,
|
|
192
|
+
"action": "login"
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Normal Logging
|
|
197
|
+
```
|
|
198
|
+
2024-01-15 10:30:45,123 - my_app - INFO - [550e8400-e29b-41d4-a716-446655440000] - User action | user_id=123 | action=login
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Django Integration
|
|
202
|
+
|
|
203
|
+
### Settings Configuration
|
|
204
|
+
|
|
205
|
+
In your Django settings:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# settings.py
|
|
209
|
+
import os
|
|
210
|
+
from cl_logger import get_logger
|
|
211
|
+
|
|
212
|
+
# Logger for settings module
|
|
213
|
+
logger = get_logger(__name__)
|
|
214
|
+
|
|
215
|
+
# JSON logging is enabled by default
|
|
216
|
+
# To disable: export CL_JSON_LOGGING=false
|
|
217
|
+
|
|
218
|
+
# Middleware configuration
|
|
219
|
+
MIDDLEWARE = [
|
|
220
|
+
# ...
|
|
221
|
+
'content_lab_backend.middleware.logging_middleware.RequestLoggingMiddleware',
|
|
222
|
+
# ...
|
|
223
|
+
]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Usage in Views
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
# views.py
|
|
230
|
+
from cl_logger import get_logger, get_trace_id
|
|
231
|
+
|
|
232
|
+
logger = get_logger(__name__)
|
|
233
|
+
|
|
234
|
+
def my_view(request):
|
|
235
|
+
# Trace ID is automatically available from middleware
|
|
236
|
+
logger.info("View accessed", extra={
|
|
237
|
+
"user_id": request.user.id,
|
|
238
|
+
"method": request.method,
|
|
239
|
+
"path": request.path
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
# Access trace ID if needed
|
|
243
|
+
trace_id = get_trace_id()
|
|
244
|
+
# or
|
|
245
|
+
trace_id = request.trace_id
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Advanced Usage
|
|
249
|
+
|
|
250
|
+
### Custom Logger Class
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from cl_logger import CLLogger
|
|
254
|
+
|
|
255
|
+
class AppLogger(CLLogger):
|
|
256
|
+
def log_request(self, request_id, method, path, status, duration):
|
|
257
|
+
self.info("API Request", extra={
|
|
258
|
+
"request_id": request_id,
|
|
259
|
+
"method": method,
|
|
260
|
+
"path": path,
|
|
261
|
+
"status": status,
|
|
262
|
+
"duration_ms": duration
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
# Use custom logger
|
|
266
|
+
logger = AppLogger("api")
|
|
267
|
+
logger.log_request("abc123", "GET", "/api/users", 200, 45.2)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Adding Trace Metadata
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from cl_logger import add_trace_metadata, get_trace_metadata
|
|
274
|
+
|
|
275
|
+
# Add metadata to current trace
|
|
276
|
+
add_trace_metadata("user_id", 123)
|
|
277
|
+
add_trace_metadata("tenant", "acme-corp")
|
|
278
|
+
|
|
279
|
+
# Metadata is automatically included in logs
|
|
280
|
+
logger.info("Processing request")
|
|
281
|
+
# Output includes: {..., "trace_metadata": {"user_id": 123, "tenant": "acme-corp"}}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Best Practices
|
|
285
|
+
|
|
286
|
+
1. **Use Module Names**: Always use `__name__` for logger names
|
|
287
|
+
```python
|
|
288
|
+
logger = get_logger(__name__)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
2. **Structured Context**: Use `extra` parameter for structured data
|
|
292
|
+
```python
|
|
293
|
+
logger.info("User action", extra={"user_id": 123, "action": "login"})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
3. **Consistent Field Names**: Use consistent names for common fields
|
|
297
|
+
- `user_id` for user identifiers
|
|
298
|
+
- `request_id` or `trace_id` for request tracking
|
|
299
|
+
- `duration_ms` for time measurements
|
|
300
|
+
- `status` for operation results
|
|
301
|
+
|
|
302
|
+
4. **Let Middleware Handle Traces**: The middleware automatically manages trace IDs for HTTP requests
|
|
303
|
+
|
|
304
|
+
5. **Use Traced Clients for External Calls**: Use the provided utilities for external calls to maintain trace continuity
|
|
305
|
+
|
|
306
|
+
## Production Deployment
|
|
307
|
+
|
|
308
|
+
For production environments:
|
|
309
|
+
|
|
310
|
+
1. JSON logging is enabled by default (recommended)
|
|
311
|
+
2. Trace IDs are automatically propagated to Sentry
|
|
312
|
+
3. Use structured logging for better searchability and alerting
|
|
313
|
+
4. Configure your log aggregation service to parse JSON logs
|
|
314
|
+
|
|
315
|
+
## Troubleshooting
|
|
316
|
+
|
|
317
|
+
### Logs not appearing
|
|
318
|
+
|
|
319
|
+
1. Check the log level - by default, DEBUG messages are not shown
|
|
320
|
+
2. Ensure the logger is properly initialized: `logger = get_logger(__name__)`
|
|
321
|
+
|
|
322
|
+
### Trace IDs not propagating
|
|
323
|
+
|
|
324
|
+
1. Ensure middleware is properly configured
|
|
325
|
+
2. Use `http_utils` for HTTP requests or `sqs_utils` for SQS messages
|
|
326
|
+
3. Check that trace context is active: `get_trace_id()` should return a value
|
|
327
|
+
|
|
328
|
+
### Performance considerations
|
|
329
|
+
|
|
330
|
+
- The logger is lightweight and adds minimal overhead
|
|
331
|
+
- Trace context uses Python's contextvars for efficient async support
|
|
332
|
+
- Extra fields are only processed when actually logging
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# goldcast-cl-logger
|
|
2
|
+
|
|
3
|
+
A lightweight, flexible logging library for Content Lab projects with distributed tracing support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install from GitHub Packages (recommended)
|
|
9
|
+
pip install goldcast-cl-logger
|
|
10
|
+
|
|
11
|
+
# Install with HTTP extras for traced requests
|
|
12
|
+
pip install goldcast-cl-logger[http]
|
|
13
|
+
|
|
14
|
+
# Add to requirements.txt
|
|
15
|
+
goldcast-cl-logger==0.2.8
|
|
16
|
+
|
|
17
|
+
# Add to pyproject.toml (Poetry)
|
|
18
|
+
[[tool.poetry.source]]
|
|
19
|
+
name = "github"
|
|
20
|
+
url = "https://ghcr.io/goldcast"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.dependencies]
|
|
23
|
+
goldcast-cl-logger = { version = "^0.2.8", source = "github" }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Simple API**: Easy to use with minimal configuration
|
|
29
|
+
- **JSON Logging by Default**: Structured JSON logging for better observability
|
|
30
|
+
- **Distributed Tracing**: Built-in support for trace ID propagation
|
|
31
|
+
- **Sentry Integration**: Compatible with Sentry's distributed tracing
|
|
32
|
+
- **Environment Configuration**: Control logging format via environment variables
|
|
33
|
+
- **Extra Context**: Add custom fields to log entries
|
|
34
|
+
- **File Logging**: Optional file output support
|
|
35
|
+
- **Zero Dependencies**: Uses only Python standard library (requests is optional)
|
|
36
|
+
- **HTTP & SQS Propagation**: Automatic trace ID propagation for external calls
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from cl_logger import get_logger
|
|
42
|
+
|
|
43
|
+
# Get a logger instance
|
|
44
|
+
logger = get_logger(__name__)
|
|
45
|
+
|
|
46
|
+
# Basic logging (JSON by default)
|
|
47
|
+
logger.info("Application started")
|
|
48
|
+
logger.error("Something went wrong")
|
|
49
|
+
|
|
50
|
+
# With extra context
|
|
51
|
+
logger.info("User action", extra={"user_id": 123, "action": "login"})
|
|
52
|
+
|
|
53
|
+
# Exception logging with traceback
|
|
54
|
+
try:
|
|
55
|
+
risky_operation()
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.exception("Operation failed", extra={"operation": "risky_operation"})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Distributed Tracing
|
|
61
|
+
|
|
62
|
+
### Automatic Trace ID Propagation
|
|
63
|
+
|
|
64
|
+
The logger automatically includes trace IDs in all log entries when used with the provided middleware:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# Django middleware automatically creates/extracts trace IDs
|
|
68
|
+
# In settings.py:
|
|
69
|
+
MIDDLEWARE = [
|
|
70
|
+
# ...
|
|
71
|
+
'content_lab_backend.middleware.logging_middleware.RequestLoggingMiddleware',
|
|
72
|
+
# ...
|
|
73
|
+
]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Manual Trace Context
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from cl_logger import TraceContext, get_trace_id
|
|
80
|
+
|
|
81
|
+
# Create a new trace context
|
|
82
|
+
with TraceContext() as ctx:
|
|
83
|
+
logger.info("Starting operation")
|
|
84
|
+
# trace_id is automatically included in logs
|
|
85
|
+
current_trace_id = get_trace_id()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### HTTP Request Propagation
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from cl_logger import http_utils
|
|
92
|
+
|
|
93
|
+
# Use traced HTTP client for automatic trace propagation
|
|
94
|
+
response = http_utils.get("https://api.example.com/data")
|
|
95
|
+
response = http_utils.post("https://api.example.com/users", json={"name": "John"})
|
|
96
|
+
|
|
97
|
+
# Or use a traced session
|
|
98
|
+
session = http_utils.TracedSession()
|
|
99
|
+
response = session.get("https://api.example.com/data")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### SQS Message Propagation
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import boto3
|
|
106
|
+
from cl_logger import sqs_utils
|
|
107
|
+
|
|
108
|
+
# Wrap SQS client for automatic trace propagation
|
|
109
|
+
sqs = boto3.client('sqs')
|
|
110
|
+
traced_sqs = sqs_utils.TracedSQSClient(sqs)
|
|
111
|
+
|
|
112
|
+
# Send message with trace ID
|
|
113
|
+
traced_sqs.send_message(
|
|
114
|
+
QueueUrl='https://sqs.region.amazonaws.com/account/queue',
|
|
115
|
+
MessageBody='Hello World'
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Process incoming SQS message with trace context
|
|
119
|
+
def handle_message(message):
|
|
120
|
+
logger.info("Processing message")
|
|
121
|
+
# trace_id from message is automatically set
|
|
122
|
+
|
|
123
|
+
sqs_utils.process_sqs_message(sqs_message, handle_message)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
### Environment Variables
|
|
129
|
+
|
|
130
|
+
- `CL_JSON_LOGGING`: Set to `"false"` to disable JSON logging (default: `"true"`)
|
|
131
|
+
|
|
132
|
+
### Programmatic Configuration
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from cl_logger import CLLogger
|
|
136
|
+
|
|
137
|
+
# Create logger with specific settings
|
|
138
|
+
logger = CLLogger(
|
|
139
|
+
name="my_app",
|
|
140
|
+
level="DEBUG",
|
|
141
|
+
json_logging=True, # Default
|
|
142
|
+
log_to_file="app.log"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Toggle to normal logging at runtime
|
|
146
|
+
logger.set_json_logging(False)
|
|
147
|
+
|
|
148
|
+
# Change log level
|
|
149
|
+
logger.set_level("WARNING")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Output Examples
|
|
153
|
+
|
|
154
|
+
### JSON Logging (Default)
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"timestamp": "2024-01-15T10:30:45.123Z",
|
|
158
|
+
"level": "INFO",
|
|
159
|
+
"logger": "my_app",
|
|
160
|
+
"message": "User action",
|
|
161
|
+
"module": "views",
|
|
162
|
+
"function": "login",
|
|
163
|
+
"line": 42,
|
|
164
|
+
"trace_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
165
|
+
"user_id": 123,
|
|
166
|
+
"action": "login"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Normal Logging
|
|
171
|
+
```
|
|
172
|
+
2024-01-15 10:30:45,123 - my_app - INFO - [550e8400-e29b-41d4-a716-446655440000] - User action | user_id=123 | action=login
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Django Integration
|
|
176
|
+
|
|
177
|
+
### Settings Configuration
|
|
178
|
+
|
|
179
|
+
In your Django settings:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# settings.py
|
|
183
|
+
import os
|
|
184
|
+
from cl_logger import get_logger
|
|
185
|
+
|
|
186
|
+
# Logger for settings module
|
|
187
|
+
logger = get_logger(__name__)
|
|
188
|
+
|
|
189
|
+
# JSON logging is enabled by default
|
|
190
|
+
# To disable: export CL_JSON_LOGGING=false
|
|
191
|
+
|
|
192
|
+
# Middleware configuration
|
|
193
|
+
MIDDLEWARE = [
|
|
194
|
+
# ...
|
|
195
|
+
'content_lab_backend.middleware.logging_middleware.RequestLoggingMiddleware',
|
|
196
|
+
# ...
|
|
197
|
+
]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Usage in Views
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# views.py
|
|
204
|
+
from cl_logger import get_logger, get_trace_id
|
|
205
|
+
|
|
206
|
+
logger = get_logger(__name__)
|
|
207
|
+
|
|
208
|
+
def my_view(request):
|
|
209
|
+
# Trace ID is automatically available from middleware
|
|
210
|
+
logger.info("View accessed", extra={
|
|
211
|
+
"user_id": request.user.id,
|
|
212
|
+
"method": request.method,
|
|
213
|
+
"path": request.path
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
# Access trace ID if needed
|
|
217
|
+
trace_id = get_trace_id()
|
|
218
|
+
# or
|
|
219
|
+
trace_id = request.trace_id
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Advanced Usage
|
|
223
|
+
|
|
224
|
+
### Custom Logger Class
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from cl_logger import CLLogger
|
|
228
|
+
|
|
229
|
+
class AppLogger(CLLogger):
|
|
230
|
+
def log_request(self, request_id, method, path, status, duration):
|
|
231
|
+
self.info("API Request", extra={
|
|
232
|
+
"request_id": request_id,
|
|
233
|
+
"method": method,
|
|
234
|
+
"path": path,
|
|
235
|
+
"status": status,
|
|
236
|
+
"duration_ms": duration
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
# Use custom logger
|
|
240
|
+
logger = AppLogger("api")
|
|
241
|
+
logger.log_request("abc123", "GET", "/api/users", 200, 45.2)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Adding Trace Metadata
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from cl_logger import add_trace_metadata, get_trace_metadata
|
|
248
|
+
|
|
249
|
+
# Add metadata to current trace
|
|
250
|
+
add_trace_metadata("user_id", 123)
|
|
251
|
+
add_trace_metadata("tenant", "acme-corp")
|
|
252
|
+
|
|
253
|
+
# Metadata is automatically included in logs
|
|
254
|
+
logger.info("Processing request")
|
|
255
|
+
# Output includes: {..., "trace_metadata": {"user_id": 123, "tenant": "acme-corp"}}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Best Practices
|
|
259
|
+
|
|
260
|
+
1. **Use Module Names**: Always use `__name__` for logger names
|
|
261
|
+
```python
|
|
262
|
+
logger = get_logger(__name__)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
2. **Structured Context**: Use `extra` parameter for structured data
|
|
266
|
+
```python
|
|
267
|
+
logger.info("User action", extra={"user_id": 123, "action": "login"})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
3. **Consistent Field Names**: Use consistent names for common fields
|
|
271
|
+
- `user_id` for user identifiers
|
|
272
|
+
- `request_id` or `trace_id` for request tracking
|
|
273
|
+
- `duration_ms` for time measurements
|
|
274
|
+
- `status` for operation results
|
|
275
|
+
|
|
276
|
+
4. **Let Middleware Handle Traces**: The middleware automatically manages trace IDs for HTTP requests
|
|
277
|
+
|
|
278
|
+
5. **Use Traced Clients for External Calls**: Use the provided utilities for external calls to maintain trace continuity
|
|
279
|
+
|
|
280
|
+
## Production Deployment
|
|
281
|
+
|
|
282
|
+
For production environments:
|
|
283
|
+
|
|
284
|
+
1. JSON logging is enabled by default (recommended)
|
|
285
|
+
2. Trace IDs are automatically propagated to Sentry
|
|
286
|
+
3. Use structured logging for better searchability and alerting
|
|
287
|
+
4. Configure your log aggregation service to parse JSON logs
|
|
288
|
+
|
|
289
|
+
## Troubleshooting
|
|
290
|
+
|
|
291
|
+
### Logs not appearing
|
|
292
|
+
|
|
293
|
+
1. Check the log level - by default, DEBUG messages are not shown
|
|
294
|
+
2. Ensure the logger is properly initialized: `logger = get_logger(__name__)`
|
|
295
|
+
|
|
296
|
+
### Trace IDs not propagating
|
|
297
|
+
|
|
298
|
+
1. Ensure middleware is properly configured
|
|
299
|
+
2. Use `http_utils` for HTTP requests or `sqs_utils` for SQS messages
|
|
300
|
+
3. Check that trace context is active: `get_trace_id()` should return a value
|
|
301
|
+
|
|
302
|
+
### Performance considerations
|
|
303
|
+
|
|
304
|
+
- The logger is lightweight and adds minimal overhead
|
|
305
|
+
- Trace context uses Python's contextvars for efficient async support
|
|
306
|
+
- Extra fields are only processed when actually logging
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CL Logger - A lightweight logging library for Content Lab projects with distributed tracing support
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from . import http_utils, sqs_utils
|
|
8
|
+
from .logger import CLLogger, get_logger
|
|
9
|
+
from .trace_context import (
|
|
10
|
+
SENTRY_TRACE_HEADER,
|
|
11
|
+
TRACE_ID_HEADER,
|
|
12
|
+
TraceContext,
|
|
13
|
+
add_trace_metadata,
|
|
14
|
+
generate_trace_id,
|
|
15
|
+
get_trace_id,
|
|
16
|
+
get_trace_metadata,
|
|
17
|
+
set_trace_id,
|
|
18
|
+
set_trace_metadata,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Re-export logging level constants for convenience
|
|
22
|
+
DEBUG = logging.DEBUG
|
|
23
|
+
INFO = logging.INFO
|
|
24
|
+
WARNING = logging.WARNING
|
|
25
|
+
ERROR = logging.ERROR
|
|
26
|
+
CRITICAL = logging.CRITICAL
|
|
27
|
+
|
|
28
|
+
__version__ = "0.2.1"
|
|
29
|
+
__all__ = [
|
|
30
|
+
"CLLogger",
|
|
31
|
+
"get_logger",
|
|
32
|
+
"TraceContext",
|
|
33
|
+
"get_trace_id",
|
|
34
|
+
"set_trace_id",
|
|
35
|
+
"generate_trace_id",
|
|
36
|
+
"get_trace_metadata",
|
|
37
|
+
"set_trace_metadata",
|
|
38
|
+
"add_trace_metadata",
|
|
39
|
+
"TRACE_ID_HEADER",
|
|
40
|
+
"SENTRY_TRACE_HEADER",
|
|
41
|
+
"http_utils",
|
|
42
|
+
"sqs_utils",
|
|
43
|
+
# Logging levels
|
|
44
|
+
"DEBUG",
|
|
45
|
+
"INFO",
|
|
46
|
+
"WARNING",
|
|
47
|
+
"ERROR",
|
|
48
|
+
"CRITICAL",
|
|
49
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP utilities for trace ID propagation
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .logger import get_logger
|
|
10
|
+
from .trace_context import SENTRY_TRACE_HEADER, TRACE_ID_HEADER, get_trace_id
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_trace_headers(headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
16
|
+
"""
|
|
17
|
+
Add trace ID headers to existing headers dict.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
headers: Existing headers dict (optional)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Headers dict with trace ID headers added
|
|
24
|
+
"""
|
|
25
|
+
if headers is None:
|
|
26
|
+
headers = {}
|
|
27
|
+
else:
|
|
28
|
+
headers = headers.copy()
|
|
29
|
+
|
|
30
|
+
trace_id = get_trace_id()
|
|
31
|
+
if trace_id:
|
|
32
|
+
headers[TRACE_ID_HEADER] = trace_id
|
|
33
|
+
# Add Sentry trace format if needed
|
|
34
|
+
# Format: {trace_id}-{span_id}-{sampled}
|
|
35
|
+
headers[SENTRY_TRACE_HEADER] = f"{trace_id}-{trace_id[:16]}-1"
|
|
36
|
+
|
|
37
|
+
return headers
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TracedSession(requests.Session):
|
|
41
|
+
"""
|
|
42
|
+
A requests Session that automatically adds trace headers to all requests.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def request(self, method, url, **kwargs):
|
|
46
|
+
"""Override request to add trace headers"""
|
|
47
|
+
kwargs["headers"] = add_trace_headers(kwargs.get("headers"))
|
|
48
|
+
|
|
49
|
+
trace_id = get_trace_id()
|
|
50
|
+
logger.debug(
|
|
51
|
+
"Making HTTP request",
|
|
52
|
+
extra={
|
|
53
|
+
"method": method,
|
|
54
|
+
"url": url,
|
|
55
|
+
"trace_id": trace_id,
|
|
56
|
+
"host": urlparse(url).netloc,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return super().request(method, url, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def traced_request(method: str, url: str, **kwargs) -> requests.Response:
|
|
64
|
+
"""
|
|
65
|
+
Make an HTTP request with trace headers automatically added.
|
|
66
|
+
|
|
67
|
+
This is a drop-in replacement for requests.request() that adds trace headers.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
method: HTTP method (GET, POST, etc.)
|
|
71
|
+
url: URL to request
|
|
72
|
+
**kwargs: Additional arguments passed to requests.request()
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
requests.Response object
|
|
76
|
+
"""
|
|
77
|
+
kwargs["headers"] = add_trace_headers(kwargs.get("headers"))
|
|
78
|
+
|
|
79
|
+
trace_id = get_trace_id()
|
|
80
|
+
logger.debug(
|
|
81
|
+
"Making traced HTTP request",
|
|
82
|
+
extra={
|
|
83
|
+
"method": method,
|
|
84
|
+
"url": url,
|
|
85
|
+
"trace_id": trace_id,
|
|
86
|
+
"host": urlparse(url).netloc,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return requests.request(method, url, **kwargs)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Convenience methods that mirror requests API
|
|
94
|
+
def get(url: str, **kwargs) -> requests.Response:
|
|
95
|
+
"""GET request with trace headers"""
|
|
96
|
+
return traced_request("GET", url, **kwargs)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def post(url: str, **kwargs) -> requests.Response:
|
|
100
|
+
"""POST request with trace headers"""
|
|
101
|
+
return traced_request("POST", url, **kwargs)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def put(url: str, **kwargs) -> requests.Response:
|
|
105
|
+
"""PUT request with trace headers"""
|
|
106
|
+
return traced_request("PUT", url, **kwargs)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def patch(url: str, **kwargs) -> requests.Response:
|
|
110
|
+
"""PATCH request with trace headers"""
|
|
111
|
+
return traced_request("PATCH", url, **kwargs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def delete(url: str, **kwargs) -> requests.Response:
|
|
115
|
+
"""DELETE request with trace headers"""
|
|
116
|
+
return traced_request("DELETE", url, **kwargs)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def head(url: str, **kwargs) -> requests.Response:
|
|
120
|
+
"""HEAD request with trace headers"""
|
|
121
|
+
return traced_request("HEAD", url, **kwargs)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core logging functionality for CL Logger
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, Optional, Union
|
|
11
|
+
|
|
12
|
+
from .trace_context import get_trace_id, get_trace_metadata
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JsonFormatter(logging.Formatter):
|
|
16
|
+
"""Custom JSON formatter for structured logging with trace support"""
|
|
17
|
+
|
|
18
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
19
|
+
log_data = {
|
|
20
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
21
|
+
"level": record.levelname,
|
|
22
|
+
"logger": record.name,
|
|
23
|
+
"message": record.getMessage(),
|
|
24
|
+
"module": record.module,
|
|
25
|
+
"function": record.funcName,
|
|
26
|
+
"line": record.lineno,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Add trace ID if available
|
|
30
|
+
trace_id = get_trace_id()
|
|
31
|
+
if trace_id:
|
|
32
|
+
log_data["trace_id"] = trace_id
|
|
33
|
+
|
|
34
|
+
# Add trace metadata if available
|
|
35
|
+
trace_metadata = get_trace_metadata()
|
|
36
|
+
if trace_metadata:
|
|
37
|
+
log_data["trace_metadata"] = trace_metadata
|
|
38
|
+
|
|
39
|
+
# Add extra fields if present
|
|
40
|
+
if hasattr(record, "extra_fields"):
|
|
41
|
+
log_data.update(record.extra_fields)
|
|
42
|
+
|
|
43
|
+
# Add exception info if present
|
|
44
|
+
if record.exc_info:
|
|
45
|
+
log_data["exception"] = self.formatException(record.exc_info)
|
|
46
|
+
|
|
47
|
+
return json.dumps(log_data)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CLLogger:
|
|
51
|
+
"""
|
|
52
|
+
A flexible logger that can switch between normal and JSON logging.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
logger = CLLogger("my_app")
|
|
56
|
+
logger.info("Application started")
|
|
57
|
+
|
|
58
|
+
# With extra context
|
|
59
|
+
logger.info("User action", extra={"user_id": 123, "action": "login"})
|
|
60
|
+
|
|
61
|
+
# Switch to normal logging
|
|
62
|
+
logger.set_json_logging(False)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
name: str,
|
|
68
|
+
level: Union[str, int] = logging.INFO,
|
|
69
|
+
json_logging: Optional[bool] = None,
|
|
70
|
+
log_to_file: Optional[str] = None,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Initialize the logger.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: Logger name (typically __name__)
|
|
77
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
78
|
+
json_logging: Enable JSON logging (if None, checks CL_JSON_LOGGING env var, defaults to True)
|
|
79
|
+
log_to_file: Optional file path for logging to file
|
|
80
|
+
"""
|
|
81
|
+
self.logger = logging.getLogger(name)
|
|
82
|
+
self.logger.setLevel(level)
|
|
83
|
+
self.logger.handlers = [] # Clear any existing handlers
|
|
84
|
+
|
|
85
|
+
# Check environment variable if json_logging not explicitly set
|
|
86
|
+
# Default to True (JSON logging) if not specified
|
|
87
|
+
if json_logging is None:
|
|
88
|
+
json_logging = os.getenv("CL_JSON_LOGGING", "true").lower() != "false"
|
|
89
|
+
|
|
90
|
+
self.json_logging = json_logging
|
|
91
|
+
self._setup_handlers(log_to_file)
|
|
92
|
+
|
|
93
|
+
def _setup_handlers(self, log_to_file: Optional[str] = None):
|
|
94
|
+
"""Set up logging handlers based on configuration"""
|
|
95
|
+
# Console handler
|
|
96
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
97
|
+
|
|
98
|
+
if self.json_logging:
|
|
99
|
+
console_handler.setFormatter(JsonFormatter())
|
|
100
|
+
else:
|
|
101
|
+
# Standard format for human-readable logs (includes trace ID)
|
|
102
|
+
formatter = logging.Formatter(
|
|
103
|
+
"%(asctime)s - %(name)s - %(levelname)s - [%(trace_id)s] - %(message)s"
|
|
104
|
+
)
|
|
105
|
+
console_handler.setFormatter(formatter)
|
|
106
|
+
|
|
107
|
+
self.logger.addHandler(console_handler)
|
|
108
|
+
|
|
109
|
+
# File handler if specified
|
|
110
|
+
if log_to_file:
|
|
111
|
+
file_handler = logging.FileHandler(log_to_file)
|
|
112
|
+
if self.json_logging:
|
|
113
|
+
file_handler.setFormatter(JsonFormatter())
|
|
114
|
+
else:
|
|
115
|
+
file_formatter = logging.Formatter(
|
|
116
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - [%(trace_id)s] - %(message)s"
|
|
117
|
+
)
|
|
118
|
+
file_handler.setFormatter(file_formatter)
|
|
119
|
+
self.logger.addHandler(file_handler)
|
|
120
|
+
|
|
121
|
+
def set_json_logging(self, enabled: bool):
|
|
122
|
+
"""Toggle between JSON and normal logging"""
|
|
123
|
+
self.json_logging = enabled
|
|
124
|
+
# Recreate handlers with new format
|
|
125
|
+
handlers = self.logger.handlers.copy()
|
|
126
|
+
log_to_file = None
|
|
127
|
+
for handler in handlers:
|
|
128
|
+
if isinstance(handler, logging.FileHandler):
|
|
129
|
+
log_to_file = handler.baseFilename
|
|
130
|
+
self.logger.handlers = []
|
|
131
|
+
self._setup_handlers(log_to_file)
|
|
132
|
+
|
|
133
|
+
def set_level(self, level: Union[str, int]):
|
|
134
|
+
"""Change the logging level"""
|
|
135
|
+
self.logger.setLevel(level)
|
|
136
|
+
|
|
137
|
+
def _log_with_extra(
|
|
138
|
+
self, level: int, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs
|
|
139
|
+
):
|
|
140
|
+
"""Internal method to log with extra fields and trace context"""
|
|
141
|
+
# Add trace_id to the record for normal formatter
|
|
142
|
+
if not self.json_logging:
|
|
143
|
+
kwargs.setdefault("extra", {})
|
|
144
|
+
kwargs["extra"]["trace_id"] = get_trace_id() or "no-trace"
|
|
145
|
+
|
|
146
|
+
if extra and self.json_logging:
|
|
147
|
+
# Store extra fields in the record for JSON formatter
|
|
148
|
+
kwargs["extra"] = {"extra_fields": extra}
|
|
149
|
+
elif extra:
|
|
150
|
+
# For normal logging, append extra fields to message
|
|
151
|
+
extra_str = " | ".join(f"{k}={v}" for k, v in extra.items())
|
|
152
|
+
msg = f"{msg} | {extra_str}"
|
|
153
|
+
|
|
154
|
+
self.logger.log(level, msg, **kwargs)
|
|
155
|
+
|
|
156
|
+
def debug(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs):
|
|
157
|
+
"""Log debug message"""
|
|
158
|
+
self._log_with_extra(logging.DEBUG, msg, extra, **kwargs)
|
|
159
|
+
|
|
160
|
+
def info(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs):
|
|
161
|
+
"""Log info message"""
|
|
162
|
+
self._log_with_extra(logging.INFO, msg, extra, **kwargs)
|
|
163
|
+
|
|
164
|
+
def warning(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs):
|
|
165
|
+
"""Log warning message"""
|
|
166
|
+
self._log_with_extra(logging.WARNING, msg, extra, **kwargs)
|
|
167
|
+
|
|
168
|
+
def warn(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs):
|
|
169
|
+
"""Log warning message (deprecated alias for warning)"""
|
|
170
|
+
self.warning(msg, extra, **kwargs)
|
|
171
|
+
|
|
172
|
+
def error(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs):
|
|
173
|
+
"""Log error message"""
|
|
174
|
+
self._log_with_extra(logging.ERROR, msg, extra, **kwargs)
|
|
175
|
+
|
|
176
|
+
def critical(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs):
|
|
177
|
+
"""Log critical message"""
|
|
178
|
+
self._log_with_extra(logging.CRITICAL, msg, extra, **kwargs)
|
|
179
|
+
|
|
180
|
+
def exception(self, msg: str, extra: Optional[Dict[str, Any]] = None, **kwargs):
|
|
181
|
+
"""Log exception with traceback"""
|
|
182
|
+
kwargs["exc_info"] = True
|
|
183
|
+
self._log_with_extra(logging.ERROR, msg, extra, **kwargs)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Singleton pattern for easy access
|
|
187
|
+
_loggers: Dict[str, CLLogger] = {}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_logger(name: str, **kwargs) -> CLLogger:
|
|
191
|
+
"""
|
|
192
|
+
Get or create a logger instance.
|
|
193
|
+
|
|
194
|
+
This function maintains a singleton pattern, returning the same logger
|
|
195
|
+
instance for a given name.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
name: Logger name (typically __name__)
|
|
199
|
+
**kwargs: Additional arguments passed to CLLogger constructor
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
CLLogger instance
|
|
203
|
+
"""
|
|
204
|
+
if name not in _loggers:
|
|
205
|
+
_loggers[name] = CLLogger(name, **kwargs)
|
|
206
|
+
return _loggers[name]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQS utilities for trace ID propagation
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .logger import get_logger
|
|
8
|
+
from .trace_context import TRACE_ID_HEADER, get_trace_id, set_trace_id
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_trace_to_message_attributes(
|
|
14
|
+
message_attributes: Optional[Dict[str, Any]] = None
|
|
15
|
+
) -> Dict[str, Any]:
|
|
16
|
+
"""
|
|
17
|
+
Add trace ID to SQS message attributes.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
message_attributes: Existing message attributes (optional)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Message attributes with trace ID added
|
|
24
|
+
"""
|
|
25
|
+
if message_attributes is None:
|
|
26
|
+
message_attributes = {}
|
|
27
|
+
else:
|
|
28
|
+
message_attributes = message_attributes.copy()
|
|
29
|
+
|
|
30
|
+
trace_id = get_trace_id()
|
|
31
|
+
if trace_id:
|
|
32
|
+
message_attributes[TRACE_ID_HEADER] = {
|
|
33
|
+
"StringValue": trace_id,
|
|
34
|
+
"DataType": "String",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return message_attributes
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def extract_trace_from_message_attributes(
|
|
41
|
+
message_attributes: Optional[Dict[str, Any]]
|
|
42
|
+
) -> Optional[str]:
|
|
43
|
+
"""
|
|
44
|
+
Extract trace ID from SQS message attributes.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
message_attributes: Message attributes from SQS message
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Trace ID if found, None otherwise
|
|
51
|
+
"""
|
|
52
|
+
if not message_attributes:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
trace_attr = message_attributes.get(TRACE_ID_HEADER)
|
|
56
|
+
if trace_attr and isinstance(trace_attr, dict):
|
|
57
|
+
return trace_attr.get("StringValue")
|
|
58
|
+
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def send_message_with_trace(sqs_client, **kwargs) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Send an SQS message with trace ID automatically added.
|
|
65
|
+
|
|
66
|
+
This wraps the boto3 SQS client's send_message method.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
sqs_client: boto3 SQS client
|
|
70
|
+
**kwargs: Arguments for send_message
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Response from send_message
|
|
74
|
+
"""
|
|
75
|
+
# Add trace ID to message attributes
|
|
76
|
+
kwargs["MessageAttributes"] = add_trace_to_message_attributes(
|
|
77
|
+
kwargs.get("MessageAttributes")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
trace_id = get_trace_id()
|
|
81
|
+
logger.debug(
|
|
82
|
+
"Sending SQS message",
|
|
83
|
+
extra={
|
|
84
|
+
"queue_url": kwargs.get("QueueUrl"),
|
|
85
|
+
"trace_id": trace_id,
|
|
86
|
+
"message_attributes": list(kwargs.get("MessageAttributes", {}).keys()),
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return sqs_client.send_message(**kwargs)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def send_message_batch_with_trace(sqs_client, **kwargs) -> Dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Send a batch of SQS messages with trace ID automatically added to each.
|
|
96
|
+
|
|
97
|
+
This wraps the boto3 SQS client's send_message_batch method.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
sqs_client: boto3 SQS client
|
|
101
|
+
**kwargs: Arguments for send_message_batch
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Response from send_message_batch
|
|
105
|
+
"""
|
|
106
|
+
# Add trace ID to each message in the batch
|
|
107
|
+
entries = kwargs.get("Entries", [])
|
|
108
|
+
for entry in entries:
|
|
109
|
+
entry["MessageAttributes"] = add_trace_to_message_attributes(
|
|
110
|
+
entry.get("MessageAttributes")
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
trace_id = get_trace_id()
|
|
114
|
+
logger.debug(
|
|
115
|
+
"Sending SQS message batch",
|
|
116
|
+
extra={
|
|
117
|
+
"queue_url": kwargs.get("QueueUrl"),
|
|
118
|
+
"trace_id": trace_id,
|
|
119
|
+
"batch_size": len(entries),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return sqs_client.send_message_batch(**kwargs)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def process_sqs_message(message: Dict[str, Any], handler_func, *args, **kwargs):
|
|
127
|
+
"""
|
|
128
|
+
Process an SQS message with trace context.
|
|
129
|
+
|
|
130
|
+
This extracts the trace ID from the message and sets it in the context
|
|
131
|
+
before calling the handler function.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
message: SQS message dict
|
|
135
|
+
handler_func: Function to process the message
|
|
136
|
+
*args, **kwargs: Arguments passed to handler_func
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Result from handler_func
|
|
140
|
+
"""
|
|
141
|
+
# Extract trace ID from message attributes
|
|
142
|
+
trace_id = extract_trace_from_message_attributes(message.get("MessageAttributes"))
|
|
143
|
+
|
|
144
|
+
if trace_id:
|
|
145
|
+
logger.debug(
|
|
146
|
+
"Processing SQS message with trace",
|
|
147
|
+
extra={"trace_id": trace_id, "message_id": message.get("MessageId")},
|
|
148
|
+
)
|
|
149
|
+
set_trace_id(trace_id)
|
|
150
|
+
else:
|
|
151
|
+
logger.debug(
|
|
152
|
+
"Processing SQS message without trace",
|
|
153
|
+
extra={"message_id": message.get("MessageId")},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Call the handler function
|
|
157
|
+
return handler_func(message, *args, **kwargs)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TracedSQSClient:
|
|
161
|
+
"""
|
|
162
|
+
A wrapper around boto3 SQS client that automatically adds trace IDs to messages.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, sqs_client):
|
|
166
|
+
self.sqs_client = sqs_client
|
|
167
|
+
|
|
168
|
+
def send_message(self, **kwargs) -> Dict[str, Any]:
|
|
169
|
+
"""Send message with trace ID"""
|
|
170
|
+
return send_message_with_trace(self.sqs_client, **kwargs)
|
|
171
|
+
|
|
172
|
+
def send_message_batch(self, **kwargs) -> Dict[str, Any]:
|
|
173
|
+
"""Send message batch with trace IDs"""
|
|
174
|
+
return send_message_batch_with_trace(self.sqs_client, **kwargs)
|
|
175
|
+
|
|
176
|
+
def __getattr__(self, name):
|
|
177
|
+
"""Delegate other methods to the wrapped client"""
|
|
178
|
+
return getattr(self.sqs_client, name)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace context management for distributed tracing
|
|
3
|
+
"""
|
|
4
|
+
import contextvars
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
# Context variable to store the current trace ID
|
|
9
|
+
_trace_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
10
|
+
"trace_id", default=None
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Context variable to store additional trace metadata
|
|
14
|
+
_trace_metadata_var: contextvars.ContextVar[Dict[str, Any]] = contextvars.ContextVar(
|
|
15
|
+
"trace_metadata", default={}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
TRACE_ID_HEADER = "X-Trace-Id"
|
|
19
|
+
SENTRY_TRACE_HEADER = "sentry-trace"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_trace_id() -> str:
|
|
23
|
+
"""Generate a new trace ID"""
|
|
24
|
+
return str(uuid.uuid4())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_trace_id() -> Optional[str]:
|
|
28
|
+
"""Get the current trace ID from context"""
|
|
29
|
+
return _trace_id_var.get()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_trace_id(trace_id: str) -> None:
|
|
33
|
+
"""Set the trace ID in context"""
|
|
34
|
+
_trace_id_var.set(trace_id)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_trace_metadata() -> Dict[str, Any]:
|
|
38
|
+
"""Get additional trace metadata"""
|
|
39
|
+
return _trace_metadata_var.get().copy()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def set_trace_metadata(metadata: Dict[str, Any]) -> None:
|
|
43
|
+
"""Set additional trace metadata"""
|
|
44
|
+
_trace_metadata_var.set(metadata)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def add_trace_metadata(key: str, value: Any) -> None:
|
|
48
|
+
"""Add a single metadata item to trace context"""
|
|
49
|
+
metadata = get_trace_metadata()
|
|
50
|
+
metadata[key] = value
|
|
51
|
+
set_trace_metadata(metadata)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TraceContext:
|
|
55
|
+
"""Context manager for trace ID handling"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, trace_id: Optional[str] = None):
|
|
58
|
+
self.trace_id = trace_id or generate_trace_id()
|
|
59
|
+
self.token = None
|
|
60
|
+
self.metadata_token = None
|
|
61
|
+
|
|
62
|
+
def __enter__(self):
|
|
63
|
+
self.token = _trace_id_var.set(self.trace_id)
|
|
64
|
+
self.metadata_token = _trace_metadata_var.set({})
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
68
|
+
if self.token:
|
|
69
|
+
_trace_id_var.reset(self.token)
|
|
70
|
+
if self.metadata_token:
|
|
71
|
+
_trace_metadata_var.reset(self.metadata_token)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "goldcast-cl-logger"
|
|
3
|
+
version = "0.2.8"
|
|
4
|
+
description = "A lightweight, flexible logging library for Content Lab projects with distributed tracing support"
|
|
5
|
+
authors = ["Goldcast <engineering@goldcast.io>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "cl_logger"}]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
repository = "https://github.com/goldcast/content-lab-backend"
|
|
10
|
+
documentation = "https://github.com/goldcast/content-lab-backend/tree/main/libraries/cl-logger"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
"Topic :: System :: Logging"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.poetry.dependencies]
|
|
24
|
+
python = "^3.10"
|
|
25
|
+
requests = { version = "^2.31.0", optional = true }
|
|
26
|
+
|
|
27
|
+
[tool.poetry.extras]
|
|
28
|
+
http = ["requests"]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["poetry-core"]
|
|
32
|
+
build-backend = "poetry.core.masonry.api"
|
|
33
|
+
|
|
34
|
+
[tool.poetry.urls]
|
|
35
|
+
"Bug Tracker" = "https://github.com/goldcast/content-lab-backend/issues"
|
|
36
|
+
"Source Code" = "https://github.com/goldcast/content-lab-backend/tree/main/libraries/cl-logger"
|
|
37
|
+
|
|
38
|
+
[tool.poetry.scripts]
|