logger-sdk-observability 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logger-sdk-observability
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Structured logging framework with DI, queue pipeline, rotation, RabbitMQ and OpenTelemetry
|
|
5
|
+
Author-email: Evan Flores <e2002florespulido@gmail.com>
|
|
6
|
+
License: MIT License.
|
|
7
|
+
Project-URL: Homepage, https://github.com/EvanFlores/LoggerSDK
|
|
8
|
+
Project-URL: Repository, https://github.com/EvanFlores/LoggerSDK.git
|
|
9
|
+
Keywords: logging,structured-logging,opentelemetry,rabbitmq,tracing,async,queue-handler
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: pydantic>=2.12
|
|
14
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
15
|
+
Requires-Dist: colorlog>=6.7
|
|
16
|
+
Requires-Dist: python-dotenv>=1.0
|
|
17
|
+
Requires-Dist: aiofiles>=24.1
|
|
18
|
+
Provides-Extra: amqp
|
|
19
|
+
Requires-Dist: pika>=1.3; extra == "amqp"
|
|
20
|
+
Requires-Dist: aio-pika>=9.4; extra == "amqp"
|
|
21
|
+
Provides-Extra: otel
|
|
22
|
+
Requires-Dist: opentelemetry-api>=1.27; extra == "otel"
|
|
23
|
+
Requires-Dist: opentelemetry-sdk>=1.27; extra == "otel"
|
|
24
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.27; extra == "otel"
|
|
25
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27; extra == "otel"
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-logging>=0.48b0; extra == "otel"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=6; extra == "dev"
|
|
31
|
+
Requires-Dist: freezegun>=1.5; extra == "dev"
|
|
32
|
+
Requires-Dist: testcontainers>=4.8; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Logger
|
|
38
|
+
|
|
39
|
+
A production-ready Python logging framework for modern backends, microservices, and AI pipelines.
|
|
40
|
+
|
|
41
|
+
It gives you a single, unified API for emitting structured logs that are **traceable, sampled, and shippable** — locally to console/file, asynchronously to a queue, over RabbitMQ, or to any OpenTelemetry-compatible backend.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Table of Contents
|
|
46
|
+
|
|
47
|
+
- [Why](#why)
|
|
48
|
+
- [Features](#features)
|
|
49
|
+
- [Installation](#installation)
|
|
50
|
+
- [Quickstart](#quickstart)
|
|
51
|
+
- [Core Concepts](#core-concepts)
|
|
52
|
+
- [LoggerFactory (DI)](#loggerfactory-di)
|
|
53
|
+
- [LoggerConfig](#loggerconfig)
|
|
54
|
+
- [BoundLogger](#boundlogger)
|
|
55
|
+
- [Tracer & Trace Context](#tracer--trace-context)
|
|
56
|
+
- [Sampling](#sampling)
|
|
57
|
+
- [Caller Resolution](#caller-resolution)
|
|
58
|
+
- [Architecture](#architecture)
|
|
59
|
+
- [Configuration Reference](#configuration-reference)
|
|
60
|
+
- [Handlers](#handlers)
|
|
61
|
+
- [Console](#console)
|
|
62
|
+
- [Rotating File](#rotating-file)
|
|
63
|
+
- [RabbitMQ (sync & async)](#rabbitmq-sync--async)
|
|
64
|
+
- [Decorators](#decorators)
|
|
65
|
+
- [`@function_log`](#function_log)
|
|
66
|
+
- [`@class_log`](#class_log)
|
|
67
|
+
- [OpenTelemetry Integration](#opentelemetry-integration)
|
|
68
|
+
- [Framework Adapters](#framework-adapters)
|
|
69
|
+
- [End-to-End Examples](#end-to-end-examples)
|
|
70
|
+
- [Project Structure](#project-structure)
|
|
71
|
+
- [Testing](#testing)
|
|
72
|
+
- [Deployment](#deployment)
|
|
73
|
+
- [Roadmap](#roadmap)
|
|
74
|
+
- [Contributing](#contributing)
|
|
75
|
+
- [License](#license)
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Why
|
|
80
|
+
|
|
81
|
+
Logs in production break for predictable reasons:
|
|
82
|
+
|
|
83
|
+
- **No structure** — grep-ing free text doesn't scale.
|
|
84
|
+
- **Async + threads** lose call-site context.
|
|
85
|
+
- **No trace correlation** — you can't link a log line to the request that produced it.
|
|
86
|
+
- **Hot loops spam logs** — cost and noise in the log pipeline.
|
|
87
|
+
- **Large codebases** need per-module control.
|
|
88
|
+
- **Hard to extend** with new sinks (RabbitMQ, OTel, …) without forking.
|
|
89
|
+
|
|
90
|
+
This library addresses all of these with one API and one config.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Features
|
|
95
|
+
|
|
96
|
+
- **Structured JSON** output (ELK / GCP / Datadog / Splunk ready)
|
|
97
|
+
- **W3C trace_id / span_id** correlation via OpenTelemetry or built-in `ContextVar`
|
|
98
|
+
- **Non-blocking** `QueueHandler` + `QueueListener` from day one
|
|
99
|
+
- **Rotating file handler** (size + time) with optional gzip rollover
|
|
100
|
+
- **RabbitMQ transport** — sync (`pika`) and async (`aio-pika`), batched, fail-open
|
|
101
|
+
- **OpenTelemetry** OTLP exporter — gRPC and HTTP
|
|
102
|
+
- **Per-module log levels**
|
|
103
|
+
- **Deterministic sampling** by `(trace_id, context, message)`
|
|
104
|
+
- **Caller resolution** — auto `file:line Class.method()` everywhere, including inside decorators
|
|
105
|
+
- **DI-based factory** — no hidden global state
|
|
106
|
+
- **Decorators** for functions and classes
|
|
107
|
+
- **Async-safe** — works in `asyncio` and thread pools
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Installation
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install logger
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
With optional transports:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install "logger[amqp]" # RabbitMQ (pika + aio-pika)
|
|
121
|
+
pip install "logger[otel]" # OpenTelemetry (OTLP gRPC + HTTP)
|
|
122
|
+
pip install "logger[all]" # everything
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
From source:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/EvanFloresLv/Logger.git
|
|
129
|
+
cd Logger
|
|
130
|
+
pip install -e ".[all]"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Requires Python 3.10+.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Quickstart
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from logger import LoggerFactory, LoggerConfig
|
|
141
|
+
|
|
142
|
+
# 1. Build a config
|
|
143
|
+
config = LoggerConfig(
|
|
144
|
+
service_name="billing-api",
|
|
145
|
+
level="INFO",
|
|
146
|
+
directory="logs",
|
|
147
|
+
json_logs=True,
|
|
148
|
+
sampling={"rate": 0.1, "deterministic": True, "min_level": "WARNING"},
|
|
149
|
+
rotation={"max_bytes": 10_000_000, "backup_count": 5, "when": "midnight"},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# 2. Build a factory (one per process)
|
|
153
|
+
factory = LoggerFactory(config)
|
|
154
|
+
|
|
155
|
+
# 3. Get a bound logger
|
|
156
|
+
log = factory.get_logger().bind("startup")
|
|
157
|
+
log.info("service ready", extra={"version": "1.0.0"})
|
|
158
|
+
|
|
159
|
+
# 4. Always shut down at exit to drain the queue and close sinks
|
|
160
|
+
factory.shutdown()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Console output (colored):
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
[10:55:01] [INFO] [billing-api] [startup] - service ready
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
File output (`logs/billing-api.log`, JSON lines):
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{"timestamp":"2026-02-04T10:55:01.140Z","level":"INFO","message":"service ready","logger":"billing-api","service":"billing-api","context":"billing-api | startup","trace_id":"00000000000000000000000000000000","span_id":"0000000000000000","module":"app","function":"<module>","line":12,"process":1234,"thread":140123}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Core Concepts
|
|
178
|
+
|
|
179
|
+
### LoggerFactory (DI)
|
|
180
|
+
|
|
181
|
+
`LoggerFactory` is the **only** entrypoint. You construct it with a `LoggerConfig`, ask it for a `BoundLogger`, and call `shutdown()` at the end of the process.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
factory = LoggerFactory(config)
|
|
185
|
+
log = factory.get_logger()
|
|
186
|
+
log2 = factory.bind(component="auth") # shortcut
|
|
187
|
+
log2.set_trace() # new trace_id
|
|
188
|
+
log2.info("login attempt", extra={"user_id": 42})
|
|
189
|
+
factory.shutdown()
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
There is no global singleton. Two factories can coexist with different configs. Tests can build throwaway factories.
|
|
193
|
+
|
|
194
|
+
### LoggerConfig
|
|
195
|
+
|
|
196
|
+
`LoggerConfig` is a Pydantic v2 `BaseSettings`. It can be built from kwargs, from a `.env` file, or from environment variables prefixed with `LOGGER__` (double underscore for nested keys).
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
# From kwargs
|
|
200
|
+
LoggerConfig(level="DEBUG", json_logs=False)
|
|
201
|
+
|
|
202
|
+
# From env
|
|
203
|
+
# LOGGER__LEVEL=DEBUG
|
|
204
|
+
# LOGGER__SAMPLING__RATE=0.5
|
|
205
|
+
# LOGGER__AMQP__URL=amqp://prod-rabbit:5672/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
All sub-settings are typed and validated at construction time.
|
|
209
|
+
|
|
210
|
+
### BoundLogger
|
|
211
|
+
|
|
212
|
+
A `logging.LoggerAdapter` subclass. Every method (`info`, `warning`, …) accepts an `extra={…}` dict whose fields are merged into the JSON record.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
log = factory.get_logger().bind(request_id="r-123")
|
|
216
|
+
log.info("started")
|
|
217
|
+
log.info("completed", extra={"duration_ms": 47})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`bind()` returns a *new* `BoundLogger` with the new context merged — the original is untouched. Chaining is cheap:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
log.bind(a=1).bind(b=2).info("x") # both a and b are in the record
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Tracer & Trace Context
|
|
227
|
+
|
|
228
|
+
`factory.tracer` (or any `BoundLogger`) exposes:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
log.set_trace() # generate + return a new trace_id
|
|
232
|
+
log.set_trace("custom-id") # use a known id
|
|
233
|
+
log.set_span()
|
|
234
|
+
|
|
235
|
+
# Reading is automatic — every record carries:
|
|
236
|
+
# trace_id (32 hex)
|
|
237
|
+
# span_id (16 hex)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
If OpenTelemetry is installed and configured (`otel.enabled=True`), the logger reads the **active OTel span** from the context — so any instrumented code (DB, HTTP, gRPC) automatically correlates with your logs.
|
|
241
|
+
|
|
242
|
+
If OTel is **not** installed, the logger uses an internal `ContextVar` so the values flow through `asyncio.Tasks` and `concurrent.futures`.
|
|
243
|
+
|
|
244
|
+
### Sampling
|
|
245
|
+
|
|
246
|
+
`SamplingSettings(rate, min_level, deterministic)`:
|
|
247
|
+
|
|
248
|
+
- `rate=0.1` keeps 10% of DEBUG/INFO records.
|
|
249
|
+
- `min_level="WARNING"` always keeps WARNING and above (sampling is below the floor).
|
|
250
|
+
- `deterministic=True` hashes `(trace_id, context, message)` so the same record is consistently kept or dropped — important so a single trace is never partially sampled.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
config = LoggerConfig(
|
|
254
|
+
sampling={"rate": 0.05, "deterministic": True, "min_level": "WARNING"}
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Caller Resolution
|
|
259
|
+
|
|
260
|
+
Every record carries `file:line Class.method()` automatically. The library uses `logging.Logger.findCaller` (stdlib-cached) and walks back through frames to enrich with the class name. Decorator wrappers and adapter methods are filtered out, so the line number and method name always point at **your** code, not the framework's.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Architecture
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
user code
|
|
268
|
+
│
|
|
269
|
+
▼
|
|
270
|
+
LoggerFactory.create(config)
|
|
271
|
+
│
|
|
272
|
+
▼
|
|
273
|
+
BoundLogger (LoggerAdapter)
|
|
274
|
+
├── .bind("ctx") → BoundLogger
|
|
275
|
+
├── .set_trace() / .set_span()
|
|
276
|
+
└── emits via stdlib logging
|
|
277
|
+
│
|
|
278
|
+
▼
|
|
279
|
+
┌─────────────────────┐
|
|
280
|
+
│ QueueHandler │ (in-memory, non-blocking)
|
|
281
|
+
│ + OverflowFilter │
|
|
282
|
+
└──────────┬──────────┘
|
|
283
|
+
│
|
|
284
|
+
▼
|
|
285
|
+
QueueListener
|
|
286
|
+
(single background thread)
|
|
287
|
+
│
|
|
288
|
+
┌──────────────┬───────┴────────┬──────────────┐
|
|
289
|
+
▼ ▼ ▼ ▼
|
|
290
|
+
ConsoleHandler RotatingFile AMQPSyncHandler AMQPAsyncHandler
|
|
291
|
+
(colored) (JSON, rotated) (pika batch) (aio-pika)
|
|
292
|
+
│
|
|
293
|
+
▼
|
|
294
|
+
RabbitMQ
|
|
295
|
+
│
|
|
296
|
+
▼
|
|
297
|
+
OTel Collector
|
|
298
|
+
│
|
|
299
|
+
▼
|
|
300
|
+
OTLPExporter (gRPC or HTTP)
|
|
301
|
+
│
|
|
302
|
+
▼
|
|
303
|
+
Backend (Tempo / Jaeger / ELK)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Key properties:
|
|
307
|
+
|
|
308
|
+
- **One producer path** — your code only ever talks to a `QueueHandler`. Sinks are owned by a single `QueueListener` thread, so a slow file system or a dead broker never blocks the producer.
|
|
309
|
+
- **Console is direct** — the console handler is *not* queued, so developers see logs immediately even if a sink is broken.
|
|
310
|
+
- **OpenTelemetry is optional** — when enabled, it sets the global `TracerProvider` and instruments stdlib `logging`, so all records (from this library and from third-party code) carry the same `trace_id` / `span_id`.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Configuration Reference
|
|
315
|
+
|
|
316
|
+
| Field | Type | Default | Notes |
|
|
317
|
+
|---|---|---|---|
|
|
318
|
+
| `service_name` | `str` | `"app"` | root logger name + OTel `service.name` |
|
|
319
|
+
| `level` | `str` | `"INFO"` | global level (`DEBUG`/`INFO`/…) |
|
|
320
|
+
| `directory` | `str` | `"logs"` | file handler root |
|
|
321
|
+
| `json_logs` | `bool` | `True` | structured file output |
|
|
322
|
+
| `date_format` | `str` | ISO 8601 ms | timestamp format |
|
|
323
|
+
| `module_levels` | `dict[str, str]` | `{}` | per-logger level overrides |
|
|
324
|
+
| `sampling.rate` | `float` | `1.0` | DEBUG/INFO sample ratio |
|
|
325
|
+
| `sampling.deterministic` | `bool` | `False` | hash on (trace, ctx, msg) |
|
|
326
|
+
| `sampling.min_level` | `str` | `"WARNING"` | never sampled below this |
|
|
327
|
+
| `rotation.max_bytes` | `int \| None` | `10_000_000` | size-based rotation |
|
|
328
|
+
| `rotation.backup_count` | `int` | `5` | retained rotated files |
|
|
329
|
+
| `rotation.when` | `str \| None` | `None` | time-based key (`"midnight"`, `"H"`, …) |
|
|
330
|
+
| `rotation.interval` | `int` | `1` | period multiplier |
|
|
331
|
+
| `rotation.utc` | `bool` | `False` | use UTC for time rotation |
|
|
332
|
+
| `rotation.compress` | `bool` | `True` | gzip rotated files |
|
|
333
|
+
| `queue.capacity` | `int` | `10_000` | in-memory queue size |
|
|
334
|
+
| `queue.overflow` | `str` | `"drop_oldest"` | `drop_oldest` / `drop_newest` / `block` |
|
|
335
|
+
| `queue.flush_on_exit` | `bool` | `True` | flush on `factory.shutdown()` |
|
|
336
|
+
| `console.enabled` | `bool` | `True` | |
|
|
337
|
+
| `console.colors` | `bool` | `True` | |
|
|
338
|
+
| `console.destination` | `str` | `"stdout"` | `"stdout"` or `"stderr"` |
|
|
339
|
+
| `amqp` | `AMQPSettings \| None` | `None` | RabbitMQ sink (see below) |
|
|
340
|
+
| `otel` | `OTelSettings` | disabled | OTel exporter (see below) |
|
|
341
|
+
|
|
342
|
+
Environment variable mapping (double underscore = nested key):
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
export LOGGER__SERVICE_NAME=billing-api
|
|
346
|
+
export LOGGER__LEVEL=DEBUG
|
|
347
|
+
export LOGGER__SAMPLING__RATE=0.1
|
|
348
|
+
export LOGGER__SAMPLING__DETERMINISTIC=true
|
|
349
|
+
export LOGGER__AMQP__URL=amqp://prod-rabbit:5672/
|
|
350
|
+
export LOGGER__AMQP__TRANSPORT=async
|
|
351
|
+
export LOGGER__OTEL__ENABLED=true
|
|
352
|
+
export LOGGER__OTEL__PROTOCOL=grpc
|
|
353
|
+
export LOGGER__OTEL__OTLP_ENDPOINT=http://otel-collector:4317
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Handlers
|
|
359
|
+
|
|
360
|
+
### Console
|
|
361
|
+
|
|
362
|
+
Always-on, immediate, colored. Honors `console.destination` so you can route WARNING+ to stderr if you want.
|
|
363
|
+
|
|
364
|
+
### Rotating File
|
|
365
|
+
|
|
366
|
+
- **Size-based** when `rotation.when is None` (default) — `RotatingFileHandler` semantics.
|
|
367
|
+
- **Time-based** when `rotation.when` is set — `TimedRotatingFileHandler` semantics.
|
|
368
|
+
- `rotation.compress=True` gzips rolled files on rollover.
|
|
369
|
+
|
|
370
|
+
Output is one JSON object per line. The file is named `<directory>/<service_name>.log`.
|
|
371
|
+
|
|
372
|
+
### RabbitMQ (sync & async)
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
config = LoggerConfig(
|
|
376
|
+
amqp={
|
|
377
|
+
"url": "amqp://guest:guest@localhost/",
|
|
378
|
+
"exchange": "logs",
|
|
379
|
+
"exchange_type": "fanout", # direct | topic | fanout | headers
|
|
380
|
+
"routing_key": "",
|
|
381
|
+
"queue": "logs",
|
|
382
|
+
"durable": True,
|
|
383
|
+
"batch_size": 100,
|
|
384
|
+
"flush_interval_s": 1.0,
|
|
385
|
+
"transport": "sync", # or "async" (aio-pika)
|
|
386
|
+
"fail_open": True, # degrade to file when broker is down
|
|
387
|
+
"connect_timeout_s": 5.0,
|
|
388
|
+
"max_retries": 5,
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
- **Sync** uses `pika.BlockingConnection` with a batched publish loop. Best for worker processes, scripts, CLIs.
|
|
394
|
+
- **Async** uses `aio-pika` and is safe to use from an event loop. Best for `asyncio` services.
|
|
395
|
+
|
|
396
|
+
Both handlers serialize records as JSON, batch up to `batch_size` records or `flush_interval_s` seconds, and flush on close. Connection failures with `fail_open=True` log a single stderr warning and continue with the file handler.
|
|
397
|
+
|
|
398
|
+
A consumer example lives in [`examples/rabbitmq_consumer.py`](examples/rabbitmq_consumer.py).
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Decorators
|
|
403
|
+
|
|
404
|
+
### `@function_log`
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from logger.decorators import function_log
|
|
408
|
+
|
|
409
|
+
@function_log(show_args=False, show_result=False)
|
|
410
|
+
def add(x, y):
|
|
411
|
+
return x + y
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Logs `Executing` and `Finished` entries with timing, the calling module, the function name, and (if present) the enclosing class. Decorator overhead is negligible — `inspect.getmodule` is cached and the resolved context is reused.
|
|
415
|
+
|
|
416
|
+
### `@class_log`
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from logger.decorators import class_log
|
|
420
|
+
|
|
421
|
+
@class_log()
|
|
422
|
+
class OrderService:
|
|
423
|
+
def place(self, order): ...
|
|
424
|
+
@classmethod
|
|
425
|
+
def from_dict(cls, raw): ...
|
|
426
|
+
@staticmethod
|
|
427
|
+
def _validate(order): ... # private — skipped
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Wraps every **public** callable (instance / classmethod / staticmethod) with `function_log`. Private names (starting with `_`) are skipped. The decorator is `__slots__`- and frozen-class-safe: if `setattr` fails, a `RuntimeWarning` is emitted and that method is left untouched.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## OpenTelemetry Integration
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
config = LoggerConfig(
|
|
438
|
+
service_name="billing-api",
|
|
439
|
+
otel={
|
|
440
|
+
"enabled": True,
|
|
441
|
+
"otlp_endpoint": "http://otel-collector:4317", # gRPC
|
|
442
|
+
"protocol": "grpc", # or "http"
|
|
443
|
+
"http_endpoint": "http://otel-collector:4318", # used when protocol="http"
|
|
444
|
+
"insecure": True,
|
|
445
|
+
"sample_ratio": 0.1,
|
|
446
|
+
"headers": {"x-api-key": "..."},
|
|
447
|
+
},
|
|
448
|
+
)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
When `otel.enabled=True`, the factory:
|
|
452
|
+
|
|
453
|
+
1. Creates a `TracerProvider` with a `Resource` of `service.name=<service_name>`.
|
|
454
|
+
2. Installs a `BatchSpanProcessor` pointing at the chosen OTLP exporter (gRPC port 4317, HTTP port 4318 by default).
|
|
455
|
+
3. Applies `TraceIdRatioBased(sample_ratio)`.
|
|
456
|
+
4. Calls `LoggingInstrumentor().instrument(set_logging_format=False)` so any log emitted through stdlib `logging` (third-party libs included) also gets the active `trace_id` / `span_id`.
|
|
457
|
+
|
|
458
|
+
After this, **every** log line — yours and from any library — carries the same `trace_id` and `span_id` as the active span, formatted as 32-hex / 16-hex per W3C TraceContext.
|
|
459
|
+
|
|
460
|
+
A minimal local stack (Collector + Jaeger) is in [`docker-compose.yml`](docker-compose.yml).
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Framework Adapters
|
|
465
|
+
|
|
466
|
+
### FastAPI
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
from fastapi import FastAPI, Request
|
|
470
|
+
from logger import LoggerFactory, LoggerConfig
|
|
471
|
+
|
|
472
|
+
factory = LoggerFactory(LoggerConfig(
|
|
473
|
+
service_name="api",
|
|
474
|
+
otel={"enabled": True, "otlp_endpoint": "http://otel-collector:4317", "protocol": "grpc"},
|
|
475
|
+
))
|
|
476
|
+
app = FastAPI(lifespan=factory.lifecycle)
|
|
477
|
+
|
|
478
|
+
@app.middleware("http")
|
|
479
|
+
async def access_log(request: Request, call_next):
|
|
480
|
+
log = factory.get_logger().bind(path=request.url.path, method=request.method)
|
|
481
|
+
log.set_trace()
|
|
482
|
+
log.set_span()
|
|
483
|
+
start = time.perf_counter()
|
|
484
|
+
response = await call_next(request)
|
|
485
|
+
log.info("request", extra={
|
|
486
|
+
"type": "access",
|
|
487
|
+
"status": response.status_code,
|
|
488
|
+
"elapsed_ms": round((time.perf_counter() - start) * 1000, 2),
|
|
489
|
+
})
|
|
490
|
+
return response
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`factory.lifecycle` is an `asynccontextmanager` that calls `factory.shutdown()` on app exit.
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## End-to-End Examples
|
|
498
|
+
|
|
499
|
+
### 1. Local development (console + file)
|
|
500
|
+
|
|
501
|
+
```python
|
|
502
|
+
from logger import LoggerFactory, LoggerConfig
|
|
503
|
+
|
|
504
|
+
factory = LoggerFactory(LoggerConfig(service_name="dev", level="DEBUG"))
|
|
505
|
+
log = factory.get_logger().bind(component="auth")
|
|
506
|
+
log.debug("checking token")
|
|
507
|
+
log.info("user logged in", extra={"user_id": 42})
|
|
508
|
+
log.error("db error", extra={"query": "SELECT ..."})
|
|
509
|
+
factory.shutdown()
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### 2. Production with OTel + RabbitMQ
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from logger import LoggerFactory, LoggerConfig
|
|
516
|
+
|
|
517
|
+
factory = LoggerFactory(LoggerConfig(
|
|
518
|
+
service_name="billing",
|
|
519
|
+
level="INFO",
|
|
520
|
+
json_logs=True,
|
|
521
|
+
rotation={"max_bytes": 50_000_000, "backup_count": 10, "when": "midnight"},
|
|
522
|
+
amqp={
|
|
523
|
+
"url": "amqp://rabbit:5672/",
|
|
524
|
+
"exchange": "logs.fanout",
|
|
525
|
+
"queue": "billing-logs",
|
|
526
|
+
"transport": "async",
|
|
527
|
+
},
|
|
528
|
+
otel={
|
|
529
|
+
"enabled": True,
|
|
530
|
+
"otlp_endpoint": "http://otel-collector:4317",
|
|
531
|
+
"protocol": "grpc",
|
|
532
|
+
"sample_ratio": 0.1,
|
|
533
|
+
},
|
|
534
|
+
))
|
|
535
|
+
|
|
536
|
+
log = factory.get_logger().bind(component="invoice")
|
|
537
|
+
with factory.tracer.start_span("create-invoice") as span:
|
|
538
|
+
span.set_attribute("invoice.id", "inv-123")
|
|
539
|
+
log.info("invoice created", extra={"amount": 999.0})
|
|
540
|
+
factory.shutdown()
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 3. With decorators
|
|
544
|
+
|
|
545
|
+
```python
|
|
546
|
+
from logger import LoggerFactory, LoggerConfig
|
|
547
|
+
from logger.decorators import function_log, class_log
|
|
548
|
+
|
|
549
|
+
factory = LoggerFactory(LoggerConfig(service_name="orders"))
|
|
550
|
+
|
|
551
|
+
@class_log()
|
|
552
|
+
class OrderService:
|
|
553
|
+
def place(self, order):
|
|
554
|
+
...
|
|
555
|
+
|
|
556
|
+
@function_log(show_args=False)
|
|
557
|
+
def notify(order_id):
|
|
558
|
+
...
|
|
559
|
+
|
|
560
|
+
svc = OrderService()
|
|
561
|
+
svc.place({"id": 1})
|
|
562
|
+
notify(1)
|
|
563
|
+
factory.shutdown()
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Project Structure
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
src/
|
|
572
|
+
├── __init__.py # Public API re-exports
|
|
573
|
+
├── errors.py # Exception hierarchy (renamed from exceptions.py)
|
|
574
|
+
├── exceptions.py # Backwards-compat shim → errors.py
|
|
575
|
+
├── config/ # Settings models
|
|
576
|
+
│ ├── __init__.py
|
|
577
|
+
│ ├── logger_config.py # LoggerConfig (top-level)
|
|
578
|
+
│ ├── config.py # Backwards-compat shim
|
|
579
|
+
│ └── settings/
|
|
580
|
+
│ ├── sampling.py # SamplingSettings
|
|
581
|
+
│ ├── rotation.py # RotationSettings
|
|
582
|
+
│ ├── queue.py # QueueSettings
|
|
583
|
+
│ ├── console.py # ConsoleSettings
|
|
584
|
+
│ ├── amqp.py # AMQPSettings
|
|
585
|
+
│ └── otel.py # OTelSettings
|
|
586
|
+
├── core/
|
|
587
|
+
│ ├── tracer.py # OTel + ContextVar facade
|
|
588
|
+
│ ├── context.py # BoundLogger
|
|
589
|
+
│ ├── caller.py # frame-walking caller resolution
|
|
590
|
+
│ ├── filters/
|
|
591
|
+
│ │ ├── sampling.py # SamplingFilter, DeterministicSamplingFilter
|
|
592
|
+
│ │ └── overflow.py # OverflowFilter, QueueCapacityProbe
|
|
593
|
+
│ ├── formatters/
|
|
594
|
+
│ │ ├── json_formatter.py # stable JSON for files / structured sinks
|
|
595
|
+
│ │ └── colored_formatter.py # colored console output
|
|
596
|
+
│ └── transport/
|
|
597
|
+
│ ├── serialize.py # record_to_json_bytes() — single source of truth
|
|
598
|
+
│ └── batch.py # BatchBuffer — sync thread-safe buffer
|
|
599
|
+
├── handlers/
|
|
600
|
+
│ ├── console.py # immediate colored handler
|
|
601
|
+
│ ├── queue.py # QueueHandler + QueueListener pipeline
|
|
602
|
+
│ ├── rotating_file.py # size + time + gzip (GzipOnRolloverMixin)
|
|
603
|
+
│ └── amqp/ # AMQP transport package
|
|
604
|
+
│ ├── __init__.py
|
|
605
|
+
│ ├── common.py # serialize_record + settings validation
|
|
606
|
+
│ ├── sync.py # AMQPSyncHandler (pika)
|
|
607
|
+
│ ├── async_handler.py # AMQPAsyncHandler (aio-pika + drain barrier)
|
|
608
|
+
│ ├── async_loop.py # LoopRunner — dedicated event-loop thread
|
|
609
|
+
│ └── factory.py # make_amqp_handler dispatch
|
|
610
|
+
├── factory/ # DI entrypoint package
|
|
611
|
+
│ ├── __init__.py
|
|
612
|
+
│ ├── logger_factory.py # LoggerFactory class
|
|
613
|
+
│ ├── registry.py # active-factory registry
|
|
614
|
+
│ ├── builder.py # build_handlers(config) → HandlerPlan
|
|
615
|
+
│ └── factory.py # Backwards-compat shim
|
|
616
|
+
├── integrations/
|
|
617
|
+
│ └── opentelemetry/ # OTel integration package
|
|
618
|
+
│ ├── __init__.py
|
|
619
|
+
│ ├── provider.py # configure_opentelemetry()
|
|
620
|
+
│ └── exporter.py # OTLPSettingsAdapter (gRPC / HTTP)
|
|
621
|
+
└── decorators/
|
|
622
|
+
├── __init__.py
|
|
623
|
+
├── base.py # active_factory(), build_context_string()
|
|
624
|
+
├── functions.py # @function_log
|
|
625
|
+
└── classes.py # @class_log
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## Testing
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
pip install -e ".[dev]"
|
|
634
|
+
pytest tests/ -v
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
The test suite is split into `tests/unit` (fast, no external services) and `tests/integration` (RabbitMQ + OTel collector via `testcontainers`).
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
RUN_INTEGRATION=1 pytest tests/integration -v
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Deployment
|
|
646
|
+
|
|
647
|
+
The library is a pure-Python package with optional extras. It runs anywhere CPython 3.10+ runs:
|
|
648
|
+
|
|
649
|
+
- **Python SDK library** — `pip install logger[all]`
|
|
650
|
+
- **FastAPI / Flask / Starlette** — use the lifespan example above
|
|
651
|
+
- **Serverless** — Cloud Run / AWS Lambda / Azure Functions. Configure with `directory="/tmp/logs"` and set `rotation.when=None` (no time rotation) since the filesystem is ephemeral.
|
|
652
|
+
- **Workers / CLIs** — `factory.shutdown()` at the end of `main()`.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Roadmap
|
|
657
|
+
|
|
658
|
+
- Pluggable sinks via entry-points (Datadog, Loki, CloudWatch)
|
|
659
|
+
- A `LogQL`/`Grok` examples page
|
|
660
|
+
- A `structlog` adapter for users who want to keep that API
|
|
661
|
+
- Built-in PII redaction filters
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Contributing
|
|
666
|
+
|
|
667
|
+
PRs welcome. Please run `ruff check`, `mypy src/logger`, and `pytest` before submitting. Add tests for new behavior.
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## License
|
|
672
|
+
|
|
673
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Contact
|
|
678
|
+
|
|
679
|
+
Maintainer: **Evan Flores**
|
|
680
|
+
Email: `efloresp06@liverpool.com.mx`
|
|
681
|
+
Organization: **Liverpool**
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
logger_sdk_observability-0.0.1.dist-info/licenses/LICENSE,sha256=lXbdeDrL-YBsDgX64P0ARSkgkGIv_G7TNgeeOyWudok,12
|
|
2
|
+
logger_sdk_observability-0.0.1.dist-info/METADATA,sha256=0YGSVe3plsimPQB3hc-Ry3vKSy4tfQVT9fwi2OMlVZ0,24399
|
|
3
|
+
logger_sdk_observability-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
logger_sdk_observability-0.0.1.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
+
logger_sdk_observability-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|