pepe-logger 0.0.3__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.
- pepe_logger-0.0.3/LICENSE +21 -0
- pepe_logger-0.0.3/PKG-INFO +61 -0
- pepe_logger-0.0.3/README.md +34 -0
- pepe_logger-0.0.3/pplogger/__init__.py +24 -0
- pepe_logger-0.0.3/pplogger/context.py +41 -0
- pepe_logger-0.0.3/pplogger/formatters.py +118 -0
- pepe_logger-0.0.3/pplogger/logger.py +165 -0
- pepe_logger-0.0.3/pyproject.toml +68 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jose Luis Alonzo Velazquez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pepe-logger
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Structured JSON logging for distributed systems, with a Go log shipper.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: logging,json,observability,distributed-systems,structured-logging
|
|
8
|
+
Author: Jose Luis Alonzo Velazquez
|
|
9
|
+
Author-email: pepemxl@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: System :: Logging
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Project-URL: Documentation, https://github.com/pepemxl/pplogger
|
|
24
|
+
Project-URL: Homepage, https://github.com/pepemxl/pplogger
|
|
25
|
+
Project-URL: Repository, https://github.com/pepemxl/pplogger.git
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# PPLogger
|
|
29
|
+
|
|
30
|
+
A logger package for scaled distributed systems.
|
|
31
|
+
|
|
32
|
+
The goal is to standarize logs in distribuited systems and permit scale logging process.
|
|
33
|
+
|
|
34
|
+
This package converts log records into JSON messages.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install pepe-logger
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The PyPI distribution is named **`pepe-logger`**; the importable package is
|
|
43
|
+
**`pplogger`** (`from pplogger import initializer_logger`).
|
|
44
|
+
|
|
45
|
+
An example of a log file:
|
|
46
|
+
- `/tmp/service_api.module_pepe_logs.2024_07_13.log`
|
|
47
|
+
|
|
48
|
+
Example of usage:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import logging
|
|
52
|
+
from pplogger import initializer_logger
|
|
53
|
+
|
|
54
|
+
initializer_logger()
|
|
55
|
+
log = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
log.info("This log info message")
|
|
58
|
+
log.debug("This log info message")
|
|
59
|
+
log.error("This log error message")
|
|
60
|
+
log.exception("This log exception message")
|
|
61
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# PPLogger
|
|
2
|
+
|
|
3
|
+
A logger package for scaled distributed systems.
|
|
4
|
+
|
|
5
|
+
The goal is to standarize logs in distribuited systems and permit scale logging process.
|
|
6
|
+
|
|
7
|
+
This package converts log records into JSON messages.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pepe-logger
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The PyPI distribution is named **`pepe-logger`**; the importable package is
|
|
16
|
+
**`pplogger`** (`from pplogger import initializer_logger`).
|
|
17
|
+
|
|
18
|
+
An example of a log file:
|
|
19
|
+
- `/tmp/service_api.module_pepe_logs.2024_07_13.log`
|
|
20
|
+
|
|
21
|
+
Example of usage:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import logging
|
|
25
|
+
from pplogger import initializer_logger
|
|
26
|
+
|
|
27
|
+
initializer_logger()
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
log.info("This log info message")
|
|
31
|
+
log.debug("This log info message")
|
|
32
|
+
log.error("This log error message")
|
|
33
|
+
log.exception("This log exception message")
|
|
34
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""pplogger - common logger for observability of distributed systems."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from pplogger.context import bind_context, clear_context, context, get_context
|
|
6
|
+
from pplogger.logger import build_log_path, initializer_logger
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
# Single source of truth: the version declared in pyproject.toml, read from
|
|
10
|
+
# the installed distribution metadata. The distribution is named
|
|
11
|
+
# "pepe-logger" on PyPI even though the import package is "pplogger".
|
|
12
|
+
__version__ = version("pepe-logger")
|
|
13
|
+
except PackageNotFoundError: # running from a source tree that isn't installed
|
|
14
|
+
__version__ = "0.0.0+unknown"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"build_log_path",
|
|
18
|
+
"initializer_logger",
|
|
19
|
+
"bind_context",
|
|
20
|
+
"clear_context",
|
|
21
|
+
"context",
|
|
22
|
+
"get_context",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Per-context structured fields for log correlation.
|
|
2
|
+
|
|
3
|
+
Fields bound here (e.g. ``request_id``, ``trace_id``) are merged into every log
|
|
4
|
+
record emitted from the same thread / asyncio task, so callers don't have to
|
|
5
|
+
thread them through ``extra={...}`` on every log call. Explicit ``extra`` fields
|
|
6
|
+
take precedence over bound context fields.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from contextvars import ContextVar
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
_context: ContextVar[dict[str, Any] | None] = ContextVar("pplogger_context", default=None)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_context() -> dict[str, Any]:
|
|
20
|
+
"""Return a copy of the fields currently bound to this context."""
|
|
21
|
+
return dict(_context.get() or {})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bind_context(**fields: Any) -> None:
|
|
25
|
+
"""Merge ``fields`` into the current context (until overwritten/cleared)."""
|
|
26
|
+
_context.set({**get_context(), **fields})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def clear_context() -> None:
|
|
30
|
+
"""Remove all bound context fields."""
|
|
31
|
+
_context.set(None)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@contextlib.contextmanager
|
|
35
|
+
def context(**fields: Any) -> Iterator[None]:
|
|
36
|
+
"""Bind ``fields`` for the duration of the ``with`` block, then restore."""
|
|
37
|
+
token = _context.set({**get_context(), **fields})
|
|
38
|
+
try:
|
|
39
|
+
yield
|
|
40
|
+
finally:
|
|
41
|
+
_context.reset(token)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""JSON formatter for pplogger.
|
|
2
|
+
|
|
3
|
+
Each log record is serialized to a single-line JSON document so downstream
|
|
4
|
+
processors (e.g. the Go shipper in ./processor) can consume it line by line.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import datetime as dt
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import socket
|
|
14
|
+
import traceback
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pplogger.context import get_context
|
|
18
|
+
|
|
19
|
+
# Process id is cached to avoid a syscall per record, but refreshed after fork()
|
|
20
|
+
# so child processes report their own pid instead of inheriting the parent's.
|
|
21
|
+
_PID = os.getpid()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _refresh_pid() -> None:
|
|
25
|
+
global _PID
|
|
26
|
+
_PID = os.getpid()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if hasattr(os, "register_at_fork"):
|
|
30
|
+
os.register_at_fork(after_in_child=_refresh_pid)
|
|
31
|
+
|
|
32
|
+
# logging.LogRecord built-in attributes — anything not in this set is treated
|
|
33
|
+
# as user-supplied `extra={...}` and copied into the JSON payload.
|
|
34
|
+
_RESERVED_RECORD_ATTRS = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"name",
|
|
37
|
+
"msg",
|
|
38
|
+
"args",
|
|
39
|
+
"levelname",
|
|
40
|
+
"levelno",
|
|
41
|
+
"pathname",
|
|
42
|
+
"filename",
|
|
43
|
+
"module",
|
|
44
|
+
"exc_info",
|
|
45
|
+
"exc_text",
|
|
46
|
+
"stack_info",
|
|
47
|
+
"lineno",
|
|
48
|
+
"funcName",
|
|
49
|
+
"created",
|
|
50
|
+
"msecs",
|
|
51
|
+
"relativeCreated",
|
|
52
|
+
"thread",
|
|
53
|
+
"threadName",
|
|
54
|
+
"processName",
|
|
55
|
+
"process",
|
|
56
|
+
"message",
|
|
57
|
+
"asctime",
|
|
58
|
+
"taskName",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class JSONFormatter(logging.Formatter):
|
|
64
|
+
"""Serialize log records as JSON documents."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, service: str, module: str, hostname: str | None = None) -> None:
|
|
67
|
+
super().__init__()
|
|
68
|
+
self.service = service
|
|
69
|
+
self.module = module
|
|
70
|
+
self.hostname = hostname or socket.gethostname()
|
|
71
|
+
|
|
72
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
73
|
+
timestamp = dt.datetime.fromtimestamp(record.created, tz=dt.timezone.utc)
|
|
74
|
+
payload: dict[str, Any] = {
|
|
75
|
+
"timestamp": timestamp.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
|
76
|
+
"level": record.levelname,
|
|
77
|
+
"logger": record.name,
|
|
78
|
+
"message": record.getMessage(),
|
|
79
|
+
"service": self.service,
|
|
80
|
+
"module": self.module,
|
|
81
|
+
"hostname": self.hostname,
|
|
82
|
+
"pid": _PID,
|
|
83
|
+
"source_module": record.module,
|
|
84
|
+
"function": record.funcName,
|
|
85
|
+
"line": record.lineno,
|
|
86
|
+
"thread": record.threadName,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if record.exc_info:
|
|
90
|
+
exc_type, exc_value, exc_tb = record.exc_info
|
|
91
|
+
payload["exception"] = {
|
|
92
|
+
"type": exc_type.__name__ if exc_type else None,
|
|
93
|
+
"message": str(exc_value) if exc_value else None,
|
|
94
|
+
"traceback": "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Collect user `extra={...}` fields, then merge them over the bound
|
|
98
|
+
# context so explicit per-call fields win on key collisions.
|
|
99
|
+
extras = {
|
|
100
|
+
key: _safe(value)
|
|
101
|
+
for key, value in record.__dict__.items()
|
|
102
|
+
if key not in _RESERVED_RECORD_ATTRS and not key.startswith("_") and key not in payload
|
|
103
|
+
}
|
|
104
|
+
for key, value in {**get_context(), **extras}.items():
|
|
105
|
+
if key in payload:
|
|
106
|
+
continue
|
|
107
|
+
payload[key] = _safe(value)
|
|
108
|
+
|
|
109
|
+
return json.dumps(payload, ensure_ascii=False, default=str)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _safe(value: Any) -> Any:
|
|
113
|
+
"""Best-effort conversion of `extra` values to JSON-friendly types."""
|
|
114
|
+
try:
|
|
115
|
+
json.dumps(value)
|
|
116
|
+
return value
|
|
117
|
+
except (TypeError, ValueError):
|
|
118
|
+
return repr(value)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Logger initialization for pplogger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from logging.handlers import RotatingFileHandler
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from pplogger.formatters import JSONFormatter
|
|
14
|
+
|
|
15
|
+
DEFAULT_LOG_DIR = Path(os.environ.get("PPLOGGER_DIR", "/tmp"))
|
|
16
|
+
DEFAULT_SERVICE = os.environ.get("PPLOGGER_SERVICE", "service_api")
|
|
17
|
+
DEFAULT_MODULE = os.environ.get("PPLOGGER_MODULE", "module_pepe")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_log_path(
|
|
21
|
+
service: str = DEFAULT_SERVICE,
|
|
22
|
+
module: str = DEFAULT_MODULE,
|
|
23
|
+
log_dir: str | os.PathLike[str] = DEFAULT_LOG_DIR,
|
|
24
|
+
when: dt.date | None = None,
|
|
25
|
+
) -> Path:
|
|
26
|
+
"""Return the daily log file path, e.g. /tmp/service_api.module_pepe_logs.2024_07_13.log"""
|
|
27
|
+
day = (when or dt.date.today()).strftime("%Y_%m_%d")
|
|
28
|
+
return Path(log_dir) / f"{service}.{module}_logs.{day}.log"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DailyDatedFileHandler(logging.FileHandler):
|
|
32
|
+
"""A ``FileHandler`` that rolls over to a new date-stamped file at midnight.
|
|
33
|
+
|
|
34
|
+
Unlike ``TimedRotatingFileHandler`` (which renames the active file and keeps
|
|
35
|
+
a fixed base name), this preserves pplogger's convention of putting the day
|
|
36
|
+
in the filename: each calendar day gets its own
|
|
37
|
+
``<service>.<module>_logs.<YYYY_MM_DD>.log``. When the local date changes,
|
|
38
|
+
the current stream is closed and a new dated file is opened.
|
|
39
|
+
|
|
40
|
+
``clock`` is injectable for testing; it defaults to ``datetime.date.today``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
service: str,
|
|
46
|
+
module: str,
|
|
47
|
+
log_dir: str | os.PathLike[str],
|
|
48
|
+
encoding: str | None = None,
|
|
49
|
+
clock: Callable[[], dt.date] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self._service = service
|
|
52
|
+
self._module = module
|
|
53
|
+
self._log_dir = log_dir
|
|
54
|
+
self._clock = clock or dt.date.today
|
|
55
|
+
self._current_day = self._clock()
|
|
56
|
+
path = build_log_path(service, module, log_dir, self._current_day)
|
|
57
|
+
super().__init__(path, encoding=encoding)
|
|
58
|
+
|
|
59
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
60
|
+
today = self._clock()
|
|
61
|
+
if today != self._current_day:
|
|
62
|
+
self._roll_to(today)
|
|
63
|
+
super().emit(record)
|
|
64
|
+
|
|
65
|
+
def _roll_to(self, day: dt.date) -> None:
|
|
66
|
+
self.acquire()
|
|
67
|
+
try:
|
|
68
|
+
if self.stream:
|
|
69
|
+
self.stream.close()
|
|
70
|
+
self.stream = None
|
|
71
|
+
self._current_day = day
|
|
72
|
+
new_path = build_log_path(self._service, self._module, self._log_dir, day)
|
|
73
|
+
self.baseFilename = os.path.abspath(str(new_path))
|
|
74
|
+
self.stream = self._open()
|
|
75
|
+
finally:
|
|
76
|
+
self.release()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def initializer_logger(
|
|
80
|
+
service: str = DEFAULT_SERVICE,
|
|
81
|
+
module: str = DEFAULT_MODULE,
|
|
82
|
+
log_dir: str | os.PathLike[str] = DEFAULT_LOG_DIR,
|
|
83
|
+
debug: bool = False,
|
|
84
|
+
console: bool = True,
|
|
85
|
+
level: int | str | None = None,
|
|
86
|
+
max_bytes: int = 0,
|
|
87
|
+
backup_count: int = 0,
|
|
88
|
+
hostname: str | None = None,
|
|
89
|
+
rotate_daily: bool = False,
|
|
90
|
+
) -> Path:
|
|
91
|
+
"""Configure the root logger to emit JSON records to a daily file.
|
|
92
|
+
|
|
93
|
+
Returns the path of the active log file so callers can hand it to the
|
|
94
|
+
Go shipper or to tests.
|
|
95
|
+
|
|
96
|
+
:param level: explicit level (e.g. ``logging.WARNING`` or ``"WARNING"``).
|
|
97
|
+
Takes precedence over ``debug`` when given.
|
|
98
|
+
:param max_bytes: when > 0, rotate the file once it reaches this size using
|
|
99
|
+
``RotatingFileHandler`` instead of a plain ``FileHandler``.
|
|
100
|
+
:param backup_count: number of rotated backups to keep (only with
|
|
101
|
+
``max_bytes``).
|
|
102
|
+
:param hostname: override the ``hostname`` field (defaults to
|
|
103
|
+
``socket.gethostname()``); useful in containers where the host id is
|
|
104
|
+
injected via the environment.
|
|
105
|
+
:param rotate_daily: when True (and ``max_bytes`` is 0), roll over to a new
|
|
106
|
+
date-stamped file at midnight via :class:`DailyDatedFileHandler`.
|
|
107
|
+
Ignored when ``max_bytes`` > 0 (size-based rotation takes precedence).
|
|
108
|
+
"""
|
|
109
|
+
log_path = build_log_path(service=service, module=module, log_dir=log_dir)
|
|
110
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
|
|
112
|
+
resolved_level = _resolve_level(level, debug)
|
|
113
|
+
formatter = JSONFormatter(
|
|
114
|
+
service=service,
|
|
115
|
+
module=module,
|
|
116
|
+
hostname=hostname or os.environ.get("PPLOGGER_HOSTNAME"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
root = logging.getLogger()
|
|
120
|
+
root.setLevel(resolved_level)
|
|
121
|
+
|
|
122
|
+
# Replace any handlers we previously installed so repeat calls are idempotent.
|
|
123
|
+
for handler in list(root.handlers):
|
|
124
|
+
if getattr(handler, "_pplogger", False):
|
|
125
|
+
root.removeHandler(handler)
|
|
126
|
+
handler.close()
|
|
127
|
+
|
|
128
|
+
file_handler: logging.FileHandler
|
|
129
|
+
if max_bytes > 0:
|
|
130
|
+
file_handler = RotatingFileHandler(
|
|
131
|
+
log_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8"
|
|
132
|
+
)
|
|
133
|
+
elif rotate_daily:
|
|
134
|
+
file_handler = DailyDatedFileHandler(service, module, log_dir, encoding="utf-8")
|
|
135
|
+
else:
|
|
136
|
+
file_handler = logging.FileHandler(log_path, encoding="utf-8")
|
|
137
|
+
file_handler.setLevel(resolved_level)
|
|
138
|
+
file_handler.setFormatter(formatter)
|
|
139
|
+
file_handler._pplogger = True # type: ignore[attr-defined]
|
|
140
|
+
root.addHandler(file_handler)
|
|
141
|
+
|
|
142
|
+
if console:
|
|
143
|
+
stream_handler = logging.StreamHandler(stream=sys.stdout)
|
|
144
|
+
stream_handler.setLevel(resolved_level)
|
|
145
|
+
stream_handler.setFormatter(formatter)
|
|
146
|
+
stream_handler._pplogger = True # type: ignore[attr-defined]
|
|
147
|
+
root.addHandler(stream_handler)
|
|
148
|
+
|
|
149
|
+
return log_path
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _resolve_level(level: int | str | None, debug: bool) -> int:
|
|
153
|
+
"""Resolve the effective numeric level from explicit/debug/env inputs."""
|
|
154
|
+
if level is not None:
|
|
155
|
+
if isinstance(level, str):
|
|
156
|
+
num = logging.getLevelName(level.upper())
|
|
157
|
+
if not isinstance(num, int):
|
|
158
|
+
raise ValueError(f"unknown log level: {level!r}")
|
|
159
|
+
return num
|
|
160
|
+
return int(level)
|
|
161
|
+
return logging.DEBUG if debug or _env_truthy("PPLOGGER_DEBUG") else logging.INFO
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _env_truthy(name: str) -> bool:
|
|
165
|
+
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
# Distribution name on PyPI (the importable package is still `pplogger`).
|
|
3
|
+
name = "pepe-logger"
|
|
4
|
+
description = "Structured JSON logging for distributed systems, with a Go log shipper."
|
|
5
|
+
authors = ["Jose Luis Alonzo Velazquez <pepemxl@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
homepage = "https://github.com/pepemxl/pplogger"
|
|
9
|
+
repository = "https://github.com/pepemxl/pplogger.git"
|
|
10
|
+
documentation = "https://github.com/pepemxl/pplogger"
|
|
11
|
+
keywords = ["logging", "json", "observability", "distributed-systems", "structured-logging"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Topic :: System :: Logging",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
version = "0.0.3"
|
|
20
|
+
packages = [ {include="pplogger"}]
|
|
21
|
+
[tool.poetry.dependencies]
|
|
22
|
+
python = ">=3.10,<4.0"
|
|
23
|
+
|
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
|
25
|
+
pytest = '*'
|
|
26
|
+
pytest-cov = '*'
|
|
27
|
+
ipython = '*'
|
|
28
|
+
coverage = '*'
|
|
29
|
+
pre-commit = '*'
|
|
30
|
+
mypy = '*'
|
|
31
|
+
ruff = '*'
|
|
32
|
+
mkdocs = '*'
|
|
33
|
+
mkdocs-material = '*'
|
|
34
|
+
|
|
35
|
+
[tool.poetry.group.test.dependencies]
|
|
36
|
+
pytest = "*"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["poetry-core>=1.0.0"]
|
|
40
|
+
build-backend = "poetry.core.masonry.api"
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
target-version = "py310"
|
|
44
|
+
line-length = 100
|
|
45
|
+
src = ["pplogger", "tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
python_version = "3.10"
|
|
52
|
+
files = ["pplogger"]
|
|
53
|
+
warn_unused_ignores = true
|
|
54
|
+
warn_redundant_casts = true
|
|
55
|
+
warn_unused_configs = true
|
|
56
|
+
disallow_untyped_defs = true
|
|
57
|
+
no_implicit_optional = true
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
addopts = "--cov=pplogger --cov-report=term-missing --cov-fail-under=85"
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
|
|
63
|
+
[tool.coverage.run]
|
|
64
|
+
branch = true
|
|
65
|
+
source = ["pplogger"]
|
|
66
|
+
|
|
67
|
+
[tool.coverage.report]
|
|
68
|
+
show_missing = true
|