fastlog-io 0.1.0__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.

Potentially problematic release.


This version of fastlog-io might be problematic. Click here for more details.

fastlog/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """logs package – import‑time ready logger with Prometheus metrics helper.
2
+
3
+ Public API:
4
+ configure – reconfigure sinks, level, etc.
5
+ get_log(name=None) – get bound logger
6
+ log – the global loguru log
7
+ start_metrics_server – start a Prometheus /metrics endpoint
8
+ """
9
+ from .core import configure, get_log
10
+ from .metrics import start_metrics_server
11
+
12
+ __all__ = ['configure', 'get_log', 'log', 'start_metrics_server']
13
+
14
+ log = get_log('app')
fastlog/config.py ADDED
@@ -0,0 +1,26 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+
6
+ @dataclass
7
+ class Config:
8
+ '''Holds runtime configuration for logging.'''
9
+
10
+ # Directory for log files. Defaults to None or $LOG_DIR env var
11
+ log_dir: Path | None = Path(os.getenv('LOG_DIR')) if os.getenv('LOG_DIR') else None
12
+ # Minimum log level. Can be overwritten with $LOG_LEVEL
13
+ level: str = os.getenv('LOG_LEVEL', 'INFO').upper()
14
+ # Loguru rotation policy (e.g. '100 MB' or '1 day').
15
+ rotation: str = os.getenv('LOG_ROTATION', '100 MB')
16
+ # Loguru retention policy (e.g. '30 days' or '10 files').
17
+ retention: str = os.getenv('LOG_RETENTION', '90 days')
18
+
19
+ format: str = (
20
+ '<green>{time:YYYY-MM-DD HH:mm:ss.SSS!UTC}</green> | '
21
+ '<level>{level:<8}</level> | '
22
+ '<yellow>{extra[name]:<5}</yellow> | '
23
+ '<light-black>{extra[trace_id]}</light-black> | '
24
+ '<cyan>{module}.{function}:{line}</cyan> | '
25
+ '<level>{message}</level>{exception}'
26
+ )
fastlog/core.py ADDED
@@ -0,0 +1,125 @@
1
+ import sys
2
+ from contextlib import contextmanager
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+ from types import MethodType
6
+ from typing import ContextManager, Iterator, cast
7
+
8
+ from loguru import logger as _logger
9
+ from loguru._logger import Logger as _Logger
10
+ from loguru._logger import context as _context
11
+
12
+ from .config import Config
13
+ from .helpers import generate_id
14
+ from .intercept import reset_std_logging
15
+ from .metrics import metrics_patch
16
+
17
+ __all__ = ['configure', 'get_log', 'trace_ctx']
18
+
19
+ logger = _logger
20
+
21
+
22
+ # --------------------------------------------------------------------------- #
23
+ # Record patches
24
+ # --------------------------------------------------------------------------- #
25
+
26
+
27
+ def _patch_trace_and_name(record: dict) -> None:
28
+ trace_id = record['extra'].get('trace_id')
29
+ sub_trace_id = record['extra'].get('sub_trace_id')
30
+ if trace_id and sub_trace_id:
31
+ trace_id = f'{trace_id}:{sub_trace_id}'
32
+ elif sub_trace_id:
33
+ trace_id = sub_trace_id
34
+ elif not trace_id:
35
+ trace_id = f'-{generate_id(7)}'
36
+ record['extra']['trace_id'] = trace_id
37
+ record['extra'].setdefault('name', '-')
38
+
39
+
40
+ # --------------------------------------------------------------------------- #
41
+ # Trace Func
42
+ # --------------------------------------------------------------------------- #
43
+
44
+
45
+ @contextmanager
46
+ def _trace_ctx(self: _Logger, trace_id: str | None = None) -> Iterator[None]:
47
+ trace_id = trace_id or generate_id(8)
48
+ if _context.get().get('trace_id'):
49
+ with self.contextualize(sub_trace_id=trace_id):
50
+ yield
51
+ else:
52
+ with self.contextualize(trace_id=trace_id):
53
+ yield
54
+
55
+
56
+ _Logger.trace_ctx = MethodType(_trace_ctx, logger)
57
+
58
+
59
+ # Add static type checking support
60
+ class Logger(_Logger):
61
+ def trace_ctx(self, trace_id: str | None = None) -> ContextManager[None]: ...
62
+
63
+
64
+ logger = cast(Logger, logger)
65
+
66
+
67
+ # --------------------------------------------------------------------------- #
68
+ # Config
69
+ # --------------------------------------------------------------------------- #
70
+
71
+ _configure: bool = False
72
+
73
+ def configure(
74
+ log_dir: str | Path | None = None,
75
+ level: str | None = None,
76
+ rotation: str | None = None,
77
+ retention: str | None = None,
78
+ ) -> None:
79
+ cfg_kwargs = dict()
80
+ if log_dir is not None:
81
+ cfg_kwargs['log_dir'] = Path(log_dir)
82
+ if level is not None:
83
+ cfg_kwargs['level'] = level.upper()
84
+ if rotation is not None:
85
+ cfg_kwargs['rotation'] = rotation
86
+ if retention is not None:
87
+ cfg_kwargs['retention'] = retention
88
+
89
+ cfg = Config(**cfg_kwargs)
90
+
91
+ global logger
92
+
93
+ logger.remove()
94
+
95
+ logger = logger.patch(_patch_trace_and_name).patch(metrics_patch)
96
+
97
+ logger.add(
98
+ sys.stderr,
99
+ format=cfg.format,
100
+ level=cfg.level,
101
+ colorize=True,
102
+ enqueue=False,
103
+ )
104
+
105
+ if cfg.log_dir:
106
+ enqueue = sys.platform.startswith('linux')
107
+ logger.add(
108
+ cfg.log_dir / 'app.log',
109
+ format=cfg.format,
110
+ level=cfg.level,
111
+ rotation=cfg.rotation,
112
+ retention=cfg.retention,
113
+ enqueue=enqueue,
114
+ )
115
+
116
+ reset_std_logging()
117
+
118
+ global _configure
119
+ _configure = True
120
+
121
+ @lru_cache
122
+ def get_log(name: str | None = None):
123
+ if not _configure:
124
+ configure()
125
+ return logger.bind(name=name) if name else logger
fastlog/helpers.py ADDED
@@ -0,0 +1,11 @@
1
+ import string
2
+
3
+ from nanoid import generate
4
+
5
+ __all__ = ['generate_id']
6
+
7
+
8
+ def generate_id(size: int = 6, *, digits: bool = False) -> str:
9
+ '''Return a random NanoID (digits‑only when *digits* is True).'''
10
+ alphabet = string.digits if digits else string.ascii_letters + string.digits
11
+ return generate(alphabet, size=size)
fastlog/intercept.py ADDED
@@ -0,0 +1,47 @@
1
+ import logging
2
+
3
+ from loguru import logger
4
+
5
+ from .helpers import generate_id
6
+ class InterceptHandler(logging.Handler):
7
+ """ A handler that forwards standard logging records to Loguru. """
8
+
9
+ _cache: dict[str, str] = {}
10
+
11
+ def emit(self, record: logging.LogRecord) -> None: # noqa: D401
12
+ try:
13
+ level = logger.level(record.levelname).name
14
+ except ValueError:
15
+ level = record.levelno
16
+
17
+ frame, depth = logging.currentframe(), 2
18
+ while frame and frame.f_code.co_filename == logging.__file__:
19
+ frame = frame.f_back # type: ignore[assignment]
20
+ depth += 1
21
+
22
+ logger.opt(depth=depth, exception=record.exc_info).bind(
23
+ action=f'[{record.name}]{record.module}.{record.funcName}:{record.lineno}',
24
+ trace_id=self._track_id(record.name),
25
+ ).log(level, record.getMessage())
26
+
27
+ @classmethod
28
+ def _track_id(cls, name: str) -> str:
29
+ if name not in cls._cache:
30
+ cls._cache[name] = f'-{generate_id(6, digits=True)}-'
31
+ return cls._cache[name]
32
+
33
+
34
+ def reset_std_logging() -> None:
35
+ """ Replace the root logger's handlers with a single `InterceptHandler`. """
36
+ root = logging.getLogger()
37
+ root.handlers.clear()
38
+ root.addHandler(InterceptHandler())
39
+ root.setLevel(logging.WARNING)
40
+
41
+
42
+ def reset_uvicorn_logging() -> None:
43
+ # Capture the uvicorn log correctly to prevent duplicate output
44
+ for name in ('uvicorn', 'uvicorn.error', 'uvicorn.access'):
45
+ logging.getLogger(name).handlers.clear()
46
+ logging.getLogger(name).propagate = False
47
+ logging.getLogger('uvicorn').handlers = [InterceptHandler()]
fastlog/metrics.py ADDED
@@ -0,0 +1,33 @@
1
+ """Prometheus metrics integration for logs package.
2
+
3
+ Expose counters to let Grafana/Prometheus track log volume by level and logger name.
4
+ """
5
+ try:
6
+ from prometheus_client import Counter, start_http_server
7
+
8
+ LOG_COUNTER = Counter(
9
+ 'log_messages_total',
10
+ 'Total log messages',
11
+ labelnames=('level', 'name'),
12
+ )
13
+ except ImportError: # pragma: no cover – optional dependency
14
+ LOG_COUNTER = None
15
+ Counter = None
16
+
17
+ __all__ = ['start_metrics_server', 'metrics_patch']
18
+
19
+
20
+ def metrics_patch(record: dict) -> None:
21
+ '''Loguru patch: increment Prometheus counter for each record.'''
22
+ if LOG_COUNTER is None:
23
+ return
24
+ level = record['level'].name.lower()
25
+ name = record['extra'].get('name', '-')
26
+ LOG_COUNTER.labels(level, name).inc()
27
+
28
+
29
+ def start_metrics_server(port: int = 8000):
30
+ '''Start a background HTTP server on *port* exposing /metrics.'''
31
+ if Counter is None:
32
+ raise RuntimeError('prometheus_client is not installed')
33
+ start_http_server(port)
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastlog-io
3
+ Version: 0.1.0
4
+ Summary: Thin wrapper around loguru with Prometheus metrics
5
+ Author: owq
6
+ License: MIT License
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: Implementation :: CPython
15
+ Classifier: Topic :: System :: Logging
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: loguru>=0.7.3
18
+ Requires-Dist: nanoid>=2.0.0
19
+ Provides-Extra: metrics
20
+ Requires-Dist: prometheus-client>=0.22.1; extra == 'metrics'
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <a href="https://pypi.org/project/fastlog-io" target="_blank">
25
+ <img src="https://img.owq.world/2025/fastlog-logo-857f7d.png" alt="FastLOG">
26
+ </a>
27
+ </p>
28
+ <p align="center">
29
+ <em>FastLOG is a lightweight wrapper around <a href="https://github.com/Delgan/loguru" target="_blank">Loguru</a> that offers Prometheus metrics, automatic `trace_id`, colourful output, and a dead‑simple configuration API.</em>
30
+ </p>
31
+ <p align="center">
32
+ <a href="https://pypi.org/project/fastlog-io" target="_blank">
33
+ <img src="https://img.shields.io/pypi/v/fastlog-io" alt="Package version">
34
+ </a>
35
+ <span> | <span>
36
+ <a href="https://pypi.org/project/fastlog-io" target="_blank">
37
+ <img src="https://img.shields.io/pypi/status/fastlog-io" alt="Development Status">
38
+ </a>
39
+ <span> | <span>
40
+ <a href="https://pypi.org/project/fastlog-io" target="_blank">
41
+ <img src="https://img.shields.io/pypi/pyversions/fastlog-io" alt="Supported Python versions">
42
+ </a>
43
+ </p>
44
+
45
+
46
+ ---
47
+
48
+ ## ✨ Features
49
+
50
+ * **Zero‑config out of the box** – `from fastlog-io import log; log.info("hello")` works instantly.
51
+ * **Unified interface** – `configure()` + `get_log(name)` keep formatting consistent across modules.
52
+ * **Automatic `trace_id`** – generates a 7‑char NanoID when none is bound, perfect for request tracing.
53
+ * **Prometheus ready** – one call to `start_metrics_server()` exposes `/metrics` with log counters.
54
+ * **File rotation & retention** – default `rotation="100 MB"`; tune it via `configure()`.
55
+ * **Stdlib compatibility** – built‑in `reset_std_logging()` redirects the `logging` module to fastlog-io.
56
+
57
+ ---
58
+
59
+ ## 🚀 Installation
60
+
61
+ ```bash
62
+ pip install fastlog-io
63
+ # Use the prometheus dependency version
64
+ pip install fastlog-io[metrics]
65
+ ```
66
+
67
+ **Requirements**
68
+
69
+ * Python ≥ 3.12
70
+ * [loguru](https://pypi.org/project/loguru/)
71
+ * Optional: [prometheus‑client](https://pypi.org/project/prometheus-client/) for metric export
72
+
73
+ ---
74
+
75
+ ## ⚡ Quickstart
76
+
77
+ ```python
78
+ from fastlog import log, configure, start_metrics_server
79
+
80
+ # override defaults if needed
81
+ configure(level="DEBUG", log_dir="./logs")
82
+ start_metrics_server(port=9100)
83
+
84
+ log.info("service started")
85
+
86
+ # child logger
87
+ api_log = log.bind(name="api")
88
+ api_log.debug("new request")
89
+ ```
90
+
91
+ Console output (colours stripped):
92
+
93
+ ```
94
+ 2025-06-21 09:57:56.510 | INFO | app | -cFZrY2V | main.<module>:7 | service started
95
+ 2025-06-21 09:57:56.511 | DEBUG | api | -25bJVku | main.<module>:11 | new request
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 🔧 Environment variables
101
+
102
+ | Variable | Default | Description |
103
+ | -------------- | --------- | ----------------------------------------------------- |
104
+ | `LOG_DIR` | *(empty)* | Log file directory; leave blank to log to stderr only |
105
+ | `LOG_LEVEL` | `INFO` | Minimum log level |
106
+ | `LOG_ROTATION` | `100 MB` | File rotation policy (Loguru syntax) |
107
+
108
+ > These can also be passed directly to `configure()` and override environment values.
109
+
110
+ ---
111
+
112
+ ## 📊 Metrics
113
+
114
+ * **`log_messages_total{level, name}`** – total log messages, labelled by level and logger name.
115
+
116
+ Prometheus scrape example:
117
+
118
+ ```yaml
119
+ targets:
120
+ - job_name: fastlog-io
121
+ static_configs:
122
+ - targets: ["127.0.0.1:9100"]
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 🛠 Development & Tests (powered by [uv](https://github.com/astral-sh/uv))
128
+
129
+ ```bash
130
+ # run tests in a temp venv with test extras installed
131
+ uv run --with '.[test]' pytest -q
132
+
133
+ # build wheel + sdist
134
+ uv build
135
+
136
+ # verify the built wheel in a clean env
137
+ uv run --with dist/*.whl --with pytest pytest -q
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 📄 License
143
+
144
+ [MIT](LICENSE) © 2025 OWQ
@@ -0,0 +1,10 @@
1
+ fastlog/__init__.py,sha256=liEBajqZD3wmqILPpAbfVZ1xyn4vLvy5LG5SOeMfAjo,487
2
+ fastlog/config.py,sha256=9PKil_bQxk_Y4KhL_6sdRf3HIDEECR4lfqm2RFCv-NE,987
3
+ fastlog/core.py,sha256=vZR6tJq8N68vtAxLdUtVvAFyjmiUZmWya0BP-Ud6_Og,3345
4
+ fastlog/helpers.py,sha256=ywzfhMFD1uiyXrIvZX9FTgT0y39QX6HvJGo-WfZeaMw,330
5
+ fastlog/intercept.py,sha256=rf9BbRWvAJ2Z6whqsLRXnzPq2l4zHDwA-mtTQ8pEdNY,1649
6
+ fastlog/metrics.py,sha256=g3_hA7m5jVI6scGOGC5fo9Yb71BugL8km-V9SNQa-r8,1015
7
+ fastlog_io-0.1.0.dist-info/METADATA,sha256=dS5JHTmhduI-zDV-4FqeWIGmc2EojIn6bdeO8HTpjOM,4427
8
+ fastlog_io-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ fastlog_io-0.1.0.dist-info/licenses/LICENSE,sha256=H5l4pEKXYzeobqnqWgyXrmsb2rVtlmHQWTjxY7jlW-M,1055
10
+ fastlog_io-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.