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.
@@ -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]