sincpro-log 1.0.0__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.
- sincpro_log-1.0.0/LICENSE.md +61 -0
- sincpro_log-1.0.0/PKG-INFO +342 -0
- sincpro_log-1.0.0/README.md +326 -0
- sincpro_log-1.0.0/pyproject.toml +27 -0
- sincpro_log-1.0.0/sincpro_log/__init__.py +3 -0
- sincpro_log-1.0.0/sincpro_log/domain/__init__.py +1 -0
- sincpro_log-1.0.0/sincpro_log/infrastructure/__init__.py +1 -0
- sincpro_log-1.0.0/sincpro_log/logger.py +217 -0
- sincpro_log-1.0.0/sincpro_log/use_cases/__init__.py +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Copyright (c) 2024 Sincpro S.R.L
|
|
2
|
+
LICENCIA EMPRESARIAL DE USO DE SOFTWARE
|
|
3
|
+
|
|
4
|
+
Términos y Condiciones
|
|
5
|
+
|
|
6
|
+
1. Definiciones
|
|
7
|
+
|
|
8
|
+
1.1. "Software" hace referencia a cualquier programa de computadora y su documentación relacionada desarrollados
|
|
9
|
+
por Sincpro Developers.
|
|
10
|
+
|
|
11
|
+
1.2. "Cliente" se refiere a cualquier entidad o individuo que obtiene una licencia para utilizar el Software bajo
|
|
12
|
+
los términos y condiciones establecidos en este documento.
|
|
13
|
+
|
|
14
|
+
2. Concesión de Licencia
|
|
15
|
+
Sincpro Developers otorga al Cliente una licencia no exclusiva, no transferible y limitada para utilizar el Software
|
|
16
|
+
de acuerdo con los términos y condiciones establecidos en este documento.
|
|
17
|
+
|
|
18
|
+
3. Propiedad y Derechos de Autor
|
|
19
|
+
|
|
20
|
+
3.1. El Software es propiedad exclusiva de Sincpro Developers y está protegido por leyes de derechos de autor y otras
|
|
21
|
+
leyes de propiedad intelectual.
|
|
22
|
+
|
|
23
|
+
3.2. El Cliente reconoce y acepta que no adquiere ningún derecho de propiedad sobre el Software, excepto los derechos
|
|
24
|
+
limitados otorgados por esta licencia.
|
|
25
|
+
|
|
26
|
+
4. Alcance de Uso
|
|
27
|
+
|
|
28
|
+
4.1. El Cliente está autorizado a utilizar el Software únicamente para los fines y en la forma especificados en la
|
|
29
|
+
documentación proporcionada por Sincpro Developers.
|
|
30
|
+
|
|
31
|
+
4.2. El uso del Software por parte del Cliente está limitado al número de usuarios y ubicaciones geográficas
|
|
32
|
+
especificados en la documentación.
|
|
33
|
+
|
|
34
|
+
5. Restricciones
|
|
35
|
+
|
|
36
|
+
5.1. Queda estrictamente prohibido al Cliente: (a) copiar, modificar o crear trabajos derivados del Software;
|
|
37
|
+
(b) sublicenciar, transferir o distribuir el Software a terceros;
|
|
38
|
+
(c) utilizar el Software de manera que infrinja las leyes aplicables.
|
|
39
|
+
|
|
40
|
+
6. Sanciones y Consecuencias
|
|
41
|
+
|
|
42
|
+
6.1. El uso no autorizado o en violación de los términos de esta licencia puede resultar en acciones legales y
|
|
43
|
+
sanciones que Sincpro Developers considera necesarias y apropiadas.
|
|
44
|
+
|
|
45
|
+
7. Protección Legal
|
|
46
|
+
|
|
47
|
+
7.1. Estos términos y condiciones están sujetos a las leyes y regulaciones aplicables. El Cliente acepta someterse
|
|
48
|
+
a la jurisdicción de los tribunales competentes en caso de disputa relacionada con el uso del Software.
|
|
49
|
+
|
|
50
|
+
8. Cambios y Actualizaciones
|
|
51
|
+
|
|
52
|
+
8.1. Sincpro Developers se reserva el derecho de actualizar o modificar estos términos y condiciones en cualquier
|
|
53
|
+
momento. Dichos cambios serán efectivos a partir de la fecha de notificación al Cliente.
|
|
54
|
+
|
|
55
|
+
9. Aceptación
|
|
56
|
+
|
|
57
|
+
9.1. Al utilizar el Software, el Cliente acepta y se compromete a cumplir con los términos y condiciones establecidos
|
|
58
|
+
en este documento.
|
|
59
|
+
|
|
60
|
+
Para cualquier consulta sobre esta licencia o para solicitar permisos adicionales, comuníquese con Sincpro Developers
|
|
61
|
+
en Bolivia, Cochabamba Mayor rocha #161 y ayacucho o informacion@sincpro.com.bo
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sincpro-log
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A logging module for sincpro applications
|
|
5
|
+
License: LICENSE.md
|
|
6
|
+
Author: Andres Gutierrez
|
|
7
|
+
Author-email: andru1236@gmail.com
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: structlog (>=25.4.0,<26.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# SincPro Logger
|
|
17
|
+
|
|
18
|
+
Biblioteca de logging estructurado para aplicaciones SincPro, construida sobre `structlog` con capacidades avanzadas de registro.
|
|
19
|
+
|
|
20
|
+
## Características principales
|
|
21
|
+
|
|
22
|
+
- **Logging estructurado**: formatos JSON (producción) y consola con colores (desarrollo)
|
|
23
|
+
- **Integración con Grafana Loki**: envío de logs para centralización y alertas
|
|
24
|
+
- **Contexto enriquecido**: bind/unbind de datos contextuales
|
|
25
|
+
- **Tipado seguro**: interfaces completamente tipadas para Python 3.12+
|
|
26
|
+
|
|
27
|
+
## 🚀 Optimizado para Kubernetes y Observabilidad
|
|
28
|
+
|
|
29
|
+
**Sincpro Logger** está especialmente diseñado para aplicaciones containerizadas y sistemas de observabilidad modernos:
|
|
30
|
+
|
|
31
|
+
### Integración con Kubernetes
|
|
32
|
+
- **Formato JSON nativo**: Compatible directamente con FluentD, FluentBit y otros log aggregators
|
|
33
|
+
- **Metadatos estructurados**: Facilita la correlación de logs entre pods y servicios
|
|
34
|
+
- **Context propagation**: Soporte nativo para trace_id y request_id en microservicios
|
|
35
|
+
- **Resource labeling**: Etiquetas automáticas para namespace, pod, container
|
|
36
|
+
|
|
37
|
+
### Sistemas de Observabilidad
|
|
38
|
+
- **Grafana Loki**: Integración directa con push automático y etiquetas dinámicas
|
|
39
|
+
- **OpenTelemetry ready**: Compatible con estándares de trazabilidad distribuida
|
|
40
|
+
- **Structured queries**: Logs optimizados para consultas en Grafana, Kibana y DataDog
|
|
41
|
+
- **Alerting support**: Campos estructurados para configuración de alertas automáticas
|
|
42
|
+
|
|
43
|
+
### Beneficios en Contenedores
|
|
44
|
+
```python
|
|
45
|
+
# Configuración típica para Kubernetes
|
|
46
|
+
logger = create_logger(
|
|
47
|
+
"payment-service",
|
|
48
|
+
namespace="production",
|
|
49
|
+
pod_name=os.getenv("HOSTNAME"),
|
|
50
|
+
version=os.getenv("APP_VERSION", "unknown")
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Context tracing automático para microservicios
|
|
54
|
+
with logger.tracing() as traced_logger:
|
|
55
|
+
traced_logger.info("Processing payment", amount=100.50)
|
|
56
|
+
# trace_id y request_id se propagan automáticamente
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Instalación rápida
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install sincpro-logger
|
|
63
|
+
# o con Poetry
|
|
64
|
+
poetry add sincpro-logger
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 📋 Configuración inicial
|
|
68
|
+
|
|
69
|
+
**⚠️ IMPORTANTE**: La configuración del logger debe ser lo **primero** que se haga en tu aplicación, antes de cualquier import o uso de logging.
|
|
70
|
+
|
|
71
|
+
### 1. Configuración global del sistema de logging
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from sincpro_log import configure_global_logging
|
|
75
|
+
|
|
76
|
+
# Para desarrollo (formato legible con colores)
|
|
77
|
+
configure_global_logging(level="DEBUG")
|
|
78
|
+
|
|
79
|
+
# Para producción (formato JSON estructurado)
|
|
80
|
+
configure_global_logging(level="INFO")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Configuración típica en main.py o app.py
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# main.py
|
|
87
|
+
import os
|
|
88
|
+
from sincpro_log import configure_global_logging, create_logger
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def setup_logging():
|
|
92
|
+
"""Configurar logging según el entorno."""
|
|
93
|
+
# Detectar entorno
|
|
94
|
+
environment = os.getenv("ENVIRONMENT", "development")
|
|
95
|
+
|
|
96
|
+
if environment == "production":
|
|
97
|
+
configure_global_logging(level="INFO") # JSON estructurado
|
|
98
|
+
else:
|
|
99
|
+
configure_global_logging(level="DEBUG") # Formato legible
|
|
100
|
+
|
|
101
|
+
return environment
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main():
|
|
105
|
+
# PASO 1: Configurar logging ANTES que todo
|
|
106
|
+
env = setup_logging()
|
|
107
|
+
|
|
108
|
+
# PASO 2: Crear logger de la aplicación
|
|
109
|
+
logger = create_logger(
|
|
110
|
+
"mi-aplicacion",
|
|
111
|
+
environment=env,
|
|
112
|
+
version=os.getenv("APP_VERSION", "unknown")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
logger.info("Aplicación iniciada", environment=env)
|
|
116
|
+
|
|
117
|
+
# Resto de la aplicación...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
main()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 🏗️ Creación y uso de loggers
|
|
125
|
+
|
|
126
|
+
### Creación básica
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from sincpro_log import create_logger
|
|
130
|
+
|
|
131
|
+
# Logger básico
|
|
132
|
+
logger = create_logger("mi-app")
|
|
133
|
+
|
|
134
|
+
# Logger con contexto inicial
|
|
135
|
+
logger = create_logger(
|
|
136
|
+
"payment-service",
|
|
137
|
+
environment="production",
|
|
138
|
+
version="1.2.3",
|
|
139
|
+
component="api"
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Añadir y remover contexto persistente
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Añadir campos que persisten en todos los logs
|
|
147
|
+
logger.bind(user_id="12345", session_id="abc-def")
|
|
148
|
+
logger.info("Usuario autenticado") # Incluirá user_id y session_id
|
|
149
|
+
|
|
150
|
+
# Remover campos específicos
|
|
151
|
+
logger.unbind("session_id")
|
|
152
|
+
logger.info("Sesión terminada") # Solo incluirá user_id
|
|
153
|
+
|
|
154
|
+
# Contexto temporal (solo dentro del bloque)
|
|
155
|
+
with logger.context(operation="payment", amount=100.50) as temp_logger:
|
|
156
|
+
temp_logger.info("Iniciando pago") # Incluye operation y amount
|
|
157
|
+
temp_logger.error("Error en pago") # Incluye operation y amount
|
|
158
|
+
|
|
159
|
+
logger.info("Pago finalizado") # NO incluye operation ni amount
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Niveles de logging disponibles
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
logger.debug("Información de depuración")
|
|
166
|
+
logger.info("Información general")
|
|
167
|
+
logger.warning("Advertencia")
|
|
168
|
+
logger.error("Error controlado")
|
|
169
|
+
logger.critical("Error crítico")
|
|
170
|
+
logger.exception("Error con stack trace") # Usar dentro de except
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## 🔍 Trazabilidad: trace_id y request_id
|
|
174
|
+
|
|
175
|
+
### ¿Qué son y cuándo usarlos?
|
|
176
|
+
|
|
177
|
+
- **`trace_id`**: Identificador único que sigue una operación completa a través de múltiples servicios
|
|
178
|
+
- **`request_id`**: Identificador único para una petición HTTP específica
|
|
179
|
+
|
|
180
|
+
**Casos de uso típicos:**
|
|
181
|
+
- **Microservicios**: Rastrear una operación que pasa por varios servicios
|
|
182
|
+
- **APIs REST**: Asociar todos los logs de una petición HTTP
|
|
183
|
+
- **Procesamiento asíncrono**: Seguir trabajos en background
|
|
184
|
+
- **Debugging**: Correlacionar logs relacionados en sistemas distribuidos
|
|
185
|
+
|
|
186
|
+
### Uso con IDs existentes (recibidos)
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# Escenario: Recibir trace_id de otro servicio
|
|
190
|
+
incoming_trace_id = request.headers.get("X-Trace-ID")
|
|
191
|
+
incoming_request_id = request.headers.get("X-Request-ID")
|
|
192
|
+
|
|
193
|
+
# Usar IDs existentes
|
|
194
|
+
with logger.tracing(trace_id=incoming_trace_id, request_id=incoming_request_id) as traced_logger:
|
|
195
|
+
traced_logger.info("Procesando petición de otro servicio")
|
|
196
|
+
# Todos los logs tendrán estos IDs específicos
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Uso con IDs auto-generados (cuando no existen)
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# Generar automáticamente si no se proporcionan
|
|
203
|
+
with logger.tracing() as traced_logger:
|
|
204
|
+
traced_logger.info("Nueva operación iniciada")
|
|
205
|
+
# Se generan automáticamente trace_id y request_id únicos
|
|
206
|
+
|
|
207
|
+
# Obtener los IDs generados para enviar a otros servicios
|
|
208
|
+
current_trace = traced_logger.get_current_trace_id()
|
|
209
|
+
current_request = traced_logger.get_current_request_id()
|
|
210
|
+
|
|
211
|
+
# Propagar a servicios downstream
|
|
212
|
+
headers = traced_logger.get_traceability_headers()
|
|
213
|
+
# headers = {"X-Trace-ID": "...", "X-Request-ID": "..."}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Context managers individuales
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
# Solo trace_id
|
|
220
|
+
with logger.trace_id("existing-trace-123") as traced_logger:
|
|
221
|
+
traced_logger.info("Operación con trace específico")
|
|
222
|
+
|
|
223
|
+
# Solo request_id
|
|
224
|
+
with logger.request_id() as request_logger: # Auto-genera si no se especifica
|
|
225
|
+
request_logger.info("Petición con ID único")
|
|
226
|
+
|
|
227
|
+
# Combinados
|
|
228
|
+
with logger.trace_id("trace-abc") as tl:
|
|
229
|
+
with tl.request_id("request-xyz") as full_logger:
|
|
230
|
+
full_logger.info("Con ambos IDs específicos")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Integración con frameworks web
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
# Flask
|
|
237
|
+
from flask import request
|
|
238
|
+
|
|
239
|
+
@app.before_request
|
|
240
|
+
def setup_request_logging():
|
|
241
|
+
trace_id = request.headers.get("X-Trace-ID")
|
|
242
|
+
request_id = request.headers.get("X-Request-ID")
|
|
243
|
+
|
|
244
|
+
g.logger = logger.tracing(trace_id=trace_id, request_id=request_id).__enter__()
|
|
245
|
+
|
|
246
|
+
# FastAPI
|
|
247
|
+
from fastapi import Request
|
|
248
|
+
|
|
249
|
+
@app.middleware("http")
|
|
250
|
+
async def logging_middleware(request: Request, call_next):
|
|
251
|
+
trace_id = request.headers.get("X-Trace-ID")
|
|
252
|
+
request_id = request.headers.get("X-Request-ID")
|
|
253
|
+
|
|
254
|
+
with logger.tracing(trace_id=trace_id, request_id=request_id) as request_logger:
|
|
255
|
+
request.state.logger = request_logger
|
|
256
|
+
response = await call_next(request)
|
|
257
|
+
return response
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Propagar metadatos entre servicios
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
# Servicio A: Enviar petición a Servicio B
|
|
264
|
+
with logger.tracing() as traced_logger:
|
|
265
|
+
traced_logger.info("Llamando al servicio de pagos")
|
|
266
|
+
|
|
267
|
+
# Obtener headers para propagación
|
|
268
|
+
headers = traced_logger.get_traceability_headers()
|
|
269
|
+
|
|
270
|
+
# Hacer petición HTTP con headers de trazabilidad
|
|
271
|
+
response = requests.post(
|
|
272
|
+
"https://payment-service/process",
|
|
273
|
+
json={"amount": 100.50},
|
|
274
|
+
headers=headers # {"X-Trace-ID": "...", "X-Request-ID": "..."}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
traced_logger.info("Respuesta del servicio de pagos", status=response.status_code)
|
|
278
|
+
|
|
279
|
+
# Servicio B: Recibir y usar los IDs
|
|
280
|
+
def process_payment(request):
|
|
281
|
+
# Extraer IDs del request
|
|
282
|
+
trace_id = request.headers.get("X-Trace-ID")
|
|
283
|
+
request_id = request.headers.get("X-Request-ID")
|
|
284
|
+
|
|
285
|
+
# Usar los IDs recibidos
|
|
286
|
+
with logger.tracing(trace_id=trace_id, request_id=request_id) as payment_logger:
|
|
287
|
+
payment_logger.info("Procesando pago recibido")
|
|
288
|
+
# Todos los logs mantendrán la trazabilidad original
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Ejemplo completo: E-commerce checkout
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
def checkout_process(user_id: str, cart_items: list):
|
|
295
|
+
"""Proceso completo de checkout con trazabilidad."""
|
|
296
|
+
|
|
297
|
+
# Iniciar nueva transacción
|
|
298
|
+
with logger.tracing() as checkout_logger:
|
|
299
|
+
checkout_logger.info(
|
|
300
|
+
"Iniciando checkout",
|
|
301
|
+
user_id=user_id,
|
|
302
|
+
items_count=len(cart_items)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
# Validar inventario
|
|
307
|
+
with checkout_logger.context(step="inventory_check") as step_logger:
|
|
308
|
+
step_logger.info("Verificando inventario")
|
|
309
|
+
# validate_inventory(cart_items)
|
|
310
|
+
step_logger.info("Inventario validado")
|
|
311
|
+
|
|
312
|
+
# Procesar pago (enviar a servicio externo)
|
|
313
|
+
payment_headers = checkout_logger.get_traceability_headers()
|
|
314
|
+
with checkout_logger.context(step="payment") as payment_logger:
|
|
315
|
+
payment_logger.info("Procesando pago")
|
|
316
|
+
# payment_response = call_payment_service(headers=payment_headers)
|
|
317
|
+
payment_logger.info("Pago procesado exitosamente")
|
|
318
|
+
|
|
319
|
+
# Actualizar inventario
|
|
320
|
+
with checkout_logger.context(step="inventory_update") as inv_logger:
|
|
321
|
+
inv_logger.info("Actualizando inventario")
|
|
322
|
+
# update_inventory(cart_items)
|
|
323
|
+
inv_logger.info("Inventario actualizado")
|
|
324
|
+
|
|
325
|
+
checkout_logger.info("Checkout completado exitosamente")
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
checkout_logger.exception("Error en checkout", error_step="unknown")
|
|
329
|
+
raise
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Arquitectura
|
|
333
|
+
|
|
334
|
+
Diseñado con Clean Architecture (Domain-Driven Design):
|
|
335
|
+
- **Dominio**: Modelos y entidades centrales
|
|
336
|
+
- **Casos de uso**: Lógica de negocio para logs
|
|
337
|
+
- **Infraestructura**: Integración con servicios externos
|
|
338
|
+
|
|
339
|
+
## Licencia
|
|
340
|
+
|
|
341
|
+
Copyright © 2024 Sincpro S.R.L. Todos los derechos reservados.
|
|
342
|
+
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# SincPro Logger
|
|
2
|
+
|
|
3
|
+
Biblioteca de logging estructurado para aplicaciones SincPro, construida sobre `structlog` con capacidades avanzadas de registro.
|
|
4
|
+
|
|
5
|
+
## Características principales
|
|
6
|
+
|
|
7
|
+
- **Logging estructurado**: formatos JSON (producción) y consola con colores (desarrollo)
|
|
8
|
+
- **Integración con Grafana Loki**: envío de logs para centralización y alertas
|
|
9
|
+
- **Contexto enriquecido**: bind/unbind de datos contextuales
|
|
10
|
+
- **Tipado seguro**: interfaces completamente tipadas para Python 3.12+
|
|
11
|
+
|
|
12
|
+
## 🚀 Optimizado para Kubernetes y Observabilidad
|
|
13
|
+
|
|
14
|
+
**Sincpro Logger** está especialmente diseñado para aplicaciones containerizadas y sistemas de observabilidad modernos:
|
|
15
|
+
|
|
16
|
+
### Integración con Kubernetes
|
|
17
|
+
- **Formato JSON nativo**: Compatible directamente con FluentD, FluentBit y otros log aggregators
|
|
18
|
+
- **Metadatos estructurados**: Facilita la correlación de logs entre pods y servicios
|
|
19
|
+
- **Context propagation**: Soporte nativo para trace_id y request_id en microservicios
|
|
20
|
+
- **Resource labeling**: Etiquetas automáticas para namespace, pod, container
|
|
21
|
+
|
|
22
|
+
### Sistemas de Observabilidad
|
|
23
|
+
- **Grafana Loki**: Integración directa con push automático y etiquetas dinámicas
|
|
24
|
+
- **OpenTelemetry ready**: Compatible con estándares de trazabilidad distribuida
|
|
25
|
+
- **Structured queries**: Logs optimizados para consultas en Grafana, Kibana y DataDog
|
|
26
|
+
- **Alerting support**: Campos estructurados para configuración de alertas automáticas
|
|
27
|
+
|
|
28
|
+
### Beneficios en Contenedores
|
|
29
|
+
```python
|
|
30
|
+
# Configuración típica para Kubernetes
|
|
31
|
+
logger = create_logger(
|
|
32
|
+
"payment-service",
|
|
33
|
+
namespace="production",
|
|
34
|
+
pod_name=os.getenv("HOSTNAME"),
|
|
35
|
+
version=os.getenv("APP_VERSION", "unknown")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Context tracing automático para microservicios
|
|
39
|
+
with logger.tracing() as traced_logger:
|
|
40
|
+
traced_logger.info("Processing payment", amount=100.50)
|
|
41
|
+
# trace_id y request_id se propagan automáticamente
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Instalación rápida
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install sincpro-logger
|
|
48
|
+
# o con Poetry
|
|
49
|
+
poetry add sincpro-logger
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 📋 Configuración inicial
|
|
53
|
+
|
|
54
|
+
**⚠️ IMPORTANTE**: La configuración del logger debe ser lo **primero** que se haga en tu aplicación, antes de cualquier import o uso de logging.
|
|
55
|
+
|
|
56
|
+
### 1. Configuración global del sistema de logging
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sincpro_log import configure_global_logging
|
|
60
|
+
|
|
61
|
+
# Para desarrollo (formato legible con colores)
|
|
62
|
+
configure_global_logging(level="DEBUG")
|
|
63
|
+
|
|
64
|
+
# Para producción (formato JSON estructurado)
|
|
65
|
+
configure_global_logging(level="INFO")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Configuración típica en main.py o app.py
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# main.py
|
|
72
|
+
import os
|
|
73
|
+
from sincpro_log import configure_global_logging, create_logger
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def setup_logging():
|
|
77
|
+
"""Configurar logging según el entorno."""
|
|
78
|
+
# Detectar entorno
|
|
79
|
+
environment = os.getenv("ENVIRONMENT", "development")
|
|
80
|
+
|
|
81
|
+
if environment == "production":
|
|
82
|
+
configure_global_logging(level="INFO") # JSON estructurado
|
|
83
|
+
else:
|
|
84
|
+
configure_global_logging(level="DEBUG") # Formato legible
|
|
85
|
+
|
|
86
|
+
return environment
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
# PASO 1: Configurar logging ANTES que todo
|
|
91
|
+
env = setup_logging()
|
|
92
|
+
|
|
93
|
+
# PASO 2: Crear logger de la aplicación
|
|
94
|
+
logger = create_logger(
|
|
95
|
+
"mi-aplicacion",
|
|
96
|
+
environment=env,
|
|
97
|
+
version=os.getenv("APP_VERSION", "unknown")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
logger.info("Aplicación iniciada", environment=env)
|
|
101
|
+
|
|
102
|
+
# Resto de la aplicación...
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 🏗️ Creación y uso de loggers
|
|
110
|
+
|
|
111
|
+
### Creación básica
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sincpro_log import create_logger
|
|
115
|
+
|
|
116
|
+
# Logger básico
|
|
117
|
+
logger = create_logger("mi-app")
|
|
118
|
+
|
|
119
|
+
# Logger con contexto inicial
|
|
120
|
+
logger = create_logger(
|
|
121
|
+
"payment-service",
|
|
122
|
+
environment="production",
|
|
123
|
+
version="1.2.3",
|
|
124
|
+
component="api"
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Añadir y remover contexto persistente
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Añadir campos que persisten en todos los logs
|
|
132
|
+
logger.bind(user_id="12345", session_id="abc-def")
|
|
133
|
+
logger.info("Usuario autenticado") # Incluirá user_id y session_id
|
|
134
|
+
|
|
135
|
+
# Remover campos específicos
|
|
136
|
+
logger.unbind("session_id")
|
|
137
|
+
logger.info("Sesión terminada") # Solo incluirá user_id
|
|
138
|
+
|
|
139
|
+
# Contexto temporal (solo dentro del bloque)
|
|
140
|
+
with logger.context(operation="payment", amount=100.50) as temp_logger:
|
|
141
|
+
temp_logger.info("Iniciando pago") # Incluye operation y amount
|
|
142
|
+
temp_logger.error("Error en pago") # Incluye operation y amount
|
|
143
|
+
|
|
144
|
+
logger.info("Pago finalizado") # NO incluye operation ni amount
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Niveles de logging disponibles
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
logger.debug("Información de depuración")
|
|
151
|
+
logger.info("Información general")
|
|
152
|
+
logger.warning("Advertencia")
|
|
153
|
+
logger.error("Error controlado")
|
|
154
|
+
logger.critical("Error crítico")
|
|
155
|
+
logger.exception("Error con stack trace") # Usar dentro de except
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 🔍 Trazabilidad: trace_id y request_id
|
|
159
|
+
|
|
160
|
+
### ¿Qué son y cuándo usarlos?
|
|
161
|
+
|
|
162
|
+
- **`trace_id`**: Identificador único que sigue una operación completa a través de múltiples servicios
|
|
163
|
+
- **`request_id`**: Identificador único para una petición HTTP específica
|
|
164
|
+
|
|
165
|
+
**Casos de uso típicos:**
|
|
166
|
+
- **Microservicios**: Rastrear una operación que pasa por varios servicios
|
|
167
|
+
- **APIs REST**: Asociar todos los logs de una petición HTTP
|
|
168
|
+
- **Procesamiento asíncrono**: Seguir trabajos en background
|
|
169
|
+
- **Debugging**: Correlacionar logs relacionados en sistemas distribuidos
|
|
170
|
+
|
|
171
|
+
### Uso con IDs existentes (recibidos)
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# Escenario: Recibir trace_id de otro servicio
|
|
175
|
+
incoming_trace_id = request.headers.get("X-Trace-ID")
|
|
176
|
+
incoming_request_id = request.headers.get("X-Request-ID")
|
|
177
|
+
|
|
178
|
+
# Usar IDs existentes
|
|
179
|
+
with logger.tracing(trace_id=incoming_trace_id, request_id=incoming_request_id) as traced_logger:
|
|
180
|
+
traced_logger.info("Procesando petición de otro servicio")
|
|
181
|
+
# Todos los logs tendrán estos IDs específicos
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Uso con IDs auto-generados (cuando no existen)
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# Generar automáticamente si no se proporcionan
|
|
188
|
+
with logger.tracing() as traced_logger:
|
|
189
|
+
traced_logger.info("Nueva operación iniciada")
|
|
190
|
+
# Se generan automáticamente trace_id y request_id únicos
|
|
191
|
+
|
|
192
|
+
# Obtener los IDs generados para enviar a otros servicios
|
|
193
|
+
current_trace = traced_logger.get_current_trace_id()
|
|
194
|
+
current_request = traced_logger.get_current_request_id()
|
|
195
|
+
|
|
196
|
+
# Propagar a servicios downstream
|
|
197
|
+
headers = traced_logger.get_traceability_headers()
|
|
198
|
+
# headers = {"X-Trace-ID": "...", "X-Request-ID": "..."}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Context managers individuales
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# Solo trace_id
|
|
205
|
+
with logger.trace_id("existing-trace-123") as traced_logger:
|
|
206
|
+
traced_logger.info("Operación con trace específico")
|
|
207
|
+
|
|
208
|
+
# Solo request_id
|
|
209
|
+
with logger.request_id() as request_logger: # Auto-genera si no se especifica
|
|
210
|
+
request_logger.info("Petición con ID único")
|
|
211
|
+
|
|
212
|
+
# Combinados
|
|
213
|
+
with logger.trace_id("trace-abc") as tl:
|
|
214
|
+
with tl.request_id("request-xyz") as full_logger:
|
|
215
|
+
full_logger.info("Con ambos IDs específicos")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Integración con frameworks web
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# Flask
|
|
222
|
+
from flask import request
|
|
223
|
+
|
|
224
|
+
@app.before_request
|
|
225
|
+
def setup_request_logging():
|
|
226
|
+
trace_id = request.headers.get("X-Trace-ID")
|
|
227
|
+
request_id = request.headers.get("X-Request-ID")
|
|
228
|
+
|
|
229
|
+
g.logger = logger.tracing(trace_id=trace_id, request_id=request_id).__enter__()
|
|
230
|
+
|
|
231
|
+
# FastAPI
|
|
232
|
+
from fastapi import Request
|
|
233
|
+
|
|
234
|
+
@app.middleware("http")
|
|
235
|
+
async def logging_middleware(request: Request, call_next):
|
|
236
|
+
trace_id = request.headers.get("X-Trace-ID")
|
|
237
|
+
request_id = request.headers.get("X-Request-ID")
|
|
238
|
+
|
|
239
|
+
with logger.tracing(trace_id=trace_id, request_id=request_id) as request_logger:
|
|
240
|
+
request.state.logger = request_logger
|
|
241
|
+
response = await call_next(request)
|
|
242
|
+
return response
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Propagar metadatos entre servicios
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# Servicio A: Enviar petición a Servicio B
|
|
249
|
+
with logger.tracing() as traced_logger:
|
|
250
|
+
traced_logger.info("Llamando al servicio de pagos")
|
|
251
|
+
|
|
252
|
+
# Obtener headers para propagación
|
|
253
|
+
headers = traced_logger.get_traceability_headers()
|
|
254
|
+
|
|
255
|
+
# Hacer petición HTTP con headers de trazabilidad
|
|
256
|
+
response = requests.post(
|
|
257
|
+
"https://payment-service/process",
|
|
258
|
+
json={"amount": 100.50},
|
|
259
|
+
headers=headers # {"X-Trace-ID": "...", "X-Request-ID": "..."}
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
traced_logger.info("Respuesta del servicio de pagos", status=response.status_code)
|
|
263
|
+
|
|
264
|
+
# Servicio B: Recibir y usar los IDs
|
|
265
|
+
def process_payment(request):
|
|
266
|
+
# Extraer IDs del request
|
|
267
|
+
trace_id = request.headers.get("X-Trace-ID")
|
|
268
|
+
request_id = request.headers.get("X-Request-ID")
|
|
269
|
+
|
|
270
|
+
# Usar los IDs recibidos
|
|
271
|
+
with logger.tracing(trace_id=trace_id, request_id=request_id) as payment_logger:
|
|
272
|
+
payment_logger.info("Procesando pago recibido")
|
|
273
|
+
# Todos los logs mantendrán la trazabilidad original
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Ejemplo completo: E-commerce checkout
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
def checkout_process(user_id: str, cart_items: list):
|
|
280
|
+
"""Proceso completo de checkout con trazabilidad."""
|
|
281
|
+
|
|
282
|
+
# Iniciar nueva transacción
|
|
283
|
+
with logger.tracing() as checkout_logger:
|
|
284
|
+
checkout_logger.info(
|
|
285
|
+
"Iniciando checkout",
|
|
286
|
+
user_id=user_id,
|
|
287
|
+
items_count=len(cart_items)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
# Validar inventario
|
|
292
|
+
with checkout_logger.context(step="inventory_check") as step_logger:
|
|
293
|
+
step_logger.info("Verificando inventario")
|
|
294
|
+
# validate_inventory(cart_items)
|
|
295
|
+
step_logger.info("Inventario validado")
|
|
296
|
+
|
|
297
|
+
# Procesar pago (enviar a servicio externo)
|
|
298
|
+
payment_headers = checkout_logger.get_traceability_headers()
|
|
299
|
+
with checkout_logger.context(step="payment") as payment_logger:
|
|
300
|
+
payment_logger.info("Procesando pago")
|
|
301
|
+
# payment_response = call_payment_service(headers=payment_headers)
|
|
302
|
+
payment_logger.info("Pago procesado exitosamente")
|
|
303
|
+
|
|
304
|
+
# Actualizar inventario
|
|
305
|
+
with checkout_logger.context(step="inventory_update") as inv_logger:
|
|
306
|
+
inv_logger.info("Actualizando inventario")
|
|
307
|
+
# update_inventory(cart_items)
|
|
308
|
+
inv_logger.info("Inventario actualizado")
|
|
309
|
+
|
|
310
|
+
checkout_logger.info("Checkout completado exitosamente")
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
checkout_logger.exception("Error en checkout", error_step="unknown")
|
|
314
|
+
raise
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Arquitectura
|
|
318
|
+
|
|
319
|
+
Diseñado con Clean Architecture (Domain-Driven Design):
|
|
320
|
+
- **Dominio**: Modelos y entidades centrales
|
|
321
|
+
- **Casos de uso**: Lógica de negocio para logs
|
|
322
|
+
- **Infraestructura**: Integración con servicios externos
|
|
323
|
+
|
|
324
|
+
## Licencia
|
|
325
|
+
|
|
326
|
+
Copyright © 2024 Sincpro S.R.L. Todos los derechos reservados.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "sincpro-log"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "A logging module for sincpro applications"
|
|
5
|
+
authors = ["Andres Gutierrez <andru1236@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "LICENSE.md"
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.12"
|
|
11
|
+
structlog = "^25.4.0"
|
|
12
|
+
|
|
13
|
+
[tool.poetry.group.dev.dependencies]
|
|
14
|
+
pytest = "^8.4.1"
|
|
15
|
+
black = "^25.1.0"
|
|
16
|
+
jupyterlab = "^4.4.0"
|
|
17
|
+
pyright = "^1.1.403"
|
|
18
|
+
autoflake = "^2.3.1"
|
|
19
|
+
isort = "^6.0.1"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["poetry-core"]
|
|
23
|
+
build-backend = "poetry.core.masonry.api"
|
|
24
|
+
|
|
25
|
+
[tool.black]
|
|
26
|
+
line-length = 100
|
|
27
|
+
target-version = ["py312"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Domain layer for sincpro_logger."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Infrastructure components for sincpro_logger."""
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clean structured logging library with direct kwargs support.
|
|
3
|
+
|
|
4
|
+
Minimal and non-invasive core:
|
|
5
|
+
- Does not modify root logger handlers (third-party loggers remain intact).
|
|
6
|
+
- ``bind`` / ``unbind``.
|
|
7
|
+
- Declarative context managers:
|
|
8
|
+
- ``logger.context(**fields)``
|
|
9
|
+
- ``logger.trace_id(trace_id: str | None = None)``
|
|
10
|
+
- ``logger.request_id(request_id: str | None = None)``
|
|
11
|
+
- ``logger.tracing(trace_id: str | None = None, request_id: str | None = None)``
|
|
12
|
+
- Error tracebacks normalized to the ``traceback`` field in JSON output.
|
|
13
|
+
- Context priority: per-log kwargs > temporary context > persistent fields.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from functools import partial
|
|
19
|
+
from typing import Any, Callable, Dict, Generator, Literal, Optional
|
|
20
|
+
from uuid import uuid4
|
|
21
|
+
|
|
22
|
+
import structlog
|
|
23
|
+
|
|
24
|
+
LogMethod = Callable[..., None]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LoggerProxy:
|
|
28
|
+
"""Proxy logger"""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("_name", "_logger_fields", "_temporal_fields")
|
|
31
|
+
|
|
32
|
+
def __init__(self, app_name: str, **extra_fields: Any) -> None:
|
|
33
|
+
self._name = app_name
|
|
34
|
+
self._logger_fields: Dict[str, Any] = {"app_name": app_name, **extra_fields}
|
|
35
|
+
self._temporal_fields: Dict[str, Any] = dict()
|
|
36
|
+
|
|
37
|
+
def bind(self, **fields: Any) -> "LoggerProxy":
|
|
38
|
+
self._logger_fields.update(fields)
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def unbind(self, field: str) -> "LoggerProxy":
|
|
42
|
+
self._logger_fields.pop(field, None)
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def logger_fields(self) -> Dict[str, Any]:
|
|
47
|
+
"""Return the persistent fields attached to the logger."""
|
|
48
|
+
return {**self._logger_fields, **self._temporal_fields}
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def debug(self) -> Callable[..., None]:
|
|
52
|
+
return partial(structlog.get_logger(self._name).bind(**self.logger_fields).debug)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def info(self) -> Callable[..., None]:
|
|
56
|
+
return partial(structlog.get_logger(self._name).bind(**self.logger_fields).info)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def warning(self) -> Callable[..., None]:
|
|
60
|
+
return partial(structlog.get_logger(self._name).bind(**self.logger_fields).warning)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def error(self) -> Callable[..., None]:
|
|
64
|
+
return partial(structlog.get_logger(self._name).bind(**self.logger_fields).error)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def exception(self) -> Callable[..., None]:
|
|
68
|
+
return partial(structlog.get_logger(self._name).bind(**self.logger_fields).exception)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def critical(self) -> Callable[..., None]:
|
|
72
|
+
return partial(structlog.get_logger(self._name).bind(**self.logger_fields).critical)
|
|
73
|
+
|
|
74
|
+
@contextmanager
|
|
75
|
+
def context(self, **extra_fields: Any) -> Generator["LoggerProxy", None, None]:
|
|
76
|
+
"""Add temporal fields to the logger context."""
|
|
77
|
+
old = self._temporal_fields.copy()
|
|
78
|
+
try:
|
|
79
|
+
self._temporal_fields.update(extra_fields)
|
|
80
|
+
yield self
|
|
81
|
+
finally:
|
|
82
|
+
self._temporal_fields = old
|
|
83
|
+
|
|
84
|
+
@contextmanager
|
|
85
|
+
def trace_id(self, trace_id: Optional[str] = None) -> Generator["LoggerProxy", None, None]:
|
|
86
|
+
"""Provide a ``trace_id`` within the context."""
|
|
87
|
+
value = trace_id or str(uuid4())
|
|
88
|
+
with self.context(trace_id=value) as logger:
|
|
89
|
+
yield logger
|
|
90
|
+
|
|
91
|
+
@contextmanager
|
|
92
|
+
def request_id(self, request_id: Optional[str] = None) -> Generator["LoggerProxy", None, None]:
|
|
93
|
+
"""Provide a ``request_id`` within the context."""
|
|
94
|
+
value = request_id or str(uuid4())
|
|
95
|
+
with self.context(request_id=value) as logger:
|
|
96
|
+
yield logger
|
|
97
|
+
|
|
98
|
+
@contextmanager
|
|
99
|
+
def tracing(
|
|
100
|
+
self,
|
|
101
|
+
trace_id: Optional[str] = None,
|
|
102
|
+
request_id: Optional[str] = None,
|
|
103
|
+
) -> Generator["LoggerProxy", None, None]:
|
|
104
|
+
"""Provide both ``trace_id`` and ``request_id`` within the context.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
trace_id (str | None): Trace identifier. Auto-generated if ``None``.
|
|
108
|
+
request_id (str | None): Request identifier. Auto-generated if ``None``.
|
|
109
|
+
|
|
110
|
+
Yields:
|
|
111
|
+
LoggerProxy: The same logger instance with both identifiers set.
|
|
112
|
+
"""
|
|
113
|
+
resolved_trace_id = trace_id or str(uuid4())
|
|
114
|
+
resolved_request_id = request_id or str(uuid4())
|
|
115
|
+
with self.context(trace_id=resolved_trace_id, request_id=resolved_request_id) as logger:
|
|
116
|
+
yield logger
|
|
117
|
+
|
|
118
|
+
def get_traceability_headers(self) -> Dict[str, str]:
|
|
119
|
+
"""Return HTTP headers for distributed tracing.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dict[str, str]: Mapping suitable for use as HTTP headers. Includes
|
|
123
|
+
``X-Trace-ID`` and ``X-Request-ID`` when present in the current context.
|
|
124
|
+
"""
|
|
125
|
+
ctx = {**self._logger_fields, **self._temporal_fields}
|
|
126
|
+
headers: Dict[str, str] = {}
|
|
127
|
+
if "trace_id" in ctx:
|
|
128
|
+
headers["X-Trace-ID"] = str(ctx["trace_id"])
|
|
129
|
+
if "request_id" in ctx:
|
|
130
|
+
headers["X-Request-ID"] = str(ctx["request_id"])
|
|
131
|
+
return headers
|
|
132
|
+
|
|
133
|
+
def get_current_trace_id(self) -> Optional[str]:
|
|
134
|
+
"""Return the active ``trace_id`` if set, otherwise ``None``."""
|
|
135
|
+
return ({**self._logger_fields, **self._temporal_fields}).get("trace_id", None)
|
|
136
|
+
|
|
137
|
+
def get_current_request_id(self) -> Optional[str]:
|
|
138
|
+
"""Return the active ``request_id`` if set, otherwise ``None``."""
|
|
139
|
+
return ({**self._logger_fields, **self._temporal_fields}).get("request_id", None)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _rename_exc_to_traceback(_: Any, __: str, event_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
143
|
+
"""Normalize exception fields to traceback.
|
|
144
|
+
|
|
145
|
+
structlog.processors.format_exc_info may place the formatted traceback in
|
|
146
|
+
exception or exc_info. This processor moves the value into a single
|
|
147
|
+
canonical traceback key, which simplifies querying in backends such as
|
|
148
|
+
Grafana/Loki.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
_ (Any): Unused, required by structlog processor signature.
|
|
152
|
+
__ (str): Unused event name.
|
|
153
|
+
event_dict (Dict[str, Any]): Event payload to be mutated.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dict[str, Any]: The updated event payload.
|
|
157
|
+
"""
|
|
158
|
+
if "exception" in event_dict and event_dict["exception"] is not None:
|
|
159
|
+
event_dict["traceback"] = event_dict.pop("exception")
|
|
160
|
+
elif "exc_info" in event_dict and event_dict["exc_info"] is not None:
|
|
161
|
+
event_dict["traceback"] = event_dict.pop("exc_info")
|
|
162
|
+
return event_dict
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def configure_global_logging(level: Literal["DEBUG", "INFO"] = "INFO") -> None:
|
|
166
|
+
"""Configure structlog processors only.
|
|
167
|
+
|
|
168
|
+
This function is non-invasive: it does not alter root logger handlers. Third-party
|
|
169
|
+
logging configurations remain untouched. The configured processors control how
|
|
170
|
+
structlog events are rendered.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
level (Literal["DEBUG", "INFO"]): Rendering level. ``DEBUG`` enables a
|
|
174
|
+
human-friendly console renderer. ``INFO`` outputs JSON.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
processors = [
|
|
178
|
+
structlog.processors.CallsiteParameterAdder(
|
|
179
|
+
parameters=[
|
|
180
|
+
structlog.processors.CallsiteParameter.FUNC_NAME,
|
|
181
|
+
structlog.processors.CallsiteParameter.FILENAME,
|
|
182
|
+
structlog.processors.CallsiteParameter.LINENO,
|
|
183
|
+
]
|
|
184
|
+
),
|
|
185
|
+
structlog.processors.add_log_level,
|
|
186
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
187
|
+
structlog.processors.format_exc_info,
|
|
188
|
+
_rename_exc_to_traceback,
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
match level:
|
|
192
|
+
case "DEBUG":
|
|
193
|
+
log_level = logging.DEBUG
|
|
194
|
+
processors.append(structlog.dev.ConsoleRenderer())
|
|
195
|
+
case "INFO":
|
|
196
|
+
log_level = logging.INFO
|
|
197
|
+
processors.append(structlog.processors.JSONRenderer())
|
|
198
|
+
case _:
|
|
199
|
+
raise ValueError(f"Invalid log level: {level}")
|
|
200
|
+
|
|
201
|
+
structlog.configure(
|
|
202
|
+
processors=processors,
|
|
203
|
+
wrapper_class=structlog.make_filtering_bound_logger(log_level),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def create_logger(name: str, **context: Any) -> LoggerProxy:
|
|
208
|
+
"""Create a structured logger instance.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
name (str): Logger name. Emitted as ``app_name`` and used by stdlib logger.
|
|
212
|
+
**context (Any): Persistent fields to attach to every log.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
LoggerProxy: Configured logger instance.
|
|
216
|
+
"""
|
|
217
|
+
return LoggerProxy(name, **context)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Use cases for sincpro_logger."""
|