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.
@@ -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,3 @@
1
+ """sincpro_logger: A logging library for sending logs to Loki."""
2
+
3
+ from .logger import configure_global_logging, create_logger
@@ -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."""