xakki-log-python 0.4.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.
log_python/__init__.py ADDED
@@ -0,0 +1,160 @@
1
+ """xakki-log-python — structured logging library for Python.
2
+
3
+ GELF/Graylog-oriented, Python port of xakki/laralog (PHP/Laravel).
4
+
5
+ Includes the shared field/type contract, redaction, enrichment, trace capture,
6
+ wire formatters, explicit stdlib ``logging`` setup, scoped context and optional
7
+ Redis/SQLAlchemy integrations.
8
+ """
9
+
10
+ import logging
11
+ from typing import TextIO
12
+
13
+ from log_python.config import ExtraConfig, PylogConfig, TraceConfig, TraceDepthConfig
14
+ from log_python.const import (
15
+ LOGGER_COUNT,
16
+ LOGGER_MCTIME,
17
+ LOGGER_MEMORY,
18
+ LOGGER_MEMORY_PEAK,
19
+ LOGGER_MONITORING,
20
+ LOGGER_SKIP_TRACE,
21
+ LOGGER_STATUS,
22
+ LOGGER_SUCCESS,
23
+ LOGGER_SUM,
24
+ LOGGER_TAG,
25
+ LOGGER_TIME,
26
+ LOGGER_VER,
27
+ )
28
+ from log_python.context import (
29
+ CONTEXT_FALSE_VALUES,
30
+ CONTEXT_MAX_STRING_BYTES,
31
+ CONTEXT_TYPE_BOOL,
32
+ CONTEXT_TYPE_FLOAT,
33
+ CONTEXT_TYPE_INT,
34
+ CONTEXT_TYPE_SKIP,
35
+ CONTEXT_TYPE_STRING,
36
+ ContextTypeCorrector,
37
+ bind,
38
+ bound_context,
39
+ clear,
40
+ get_bound_context,
41
+ remote_ip,
42
+ )
43
+ from log_python.formatters import ConsoleFormatter, ExtraFilter, JsonFormatter, LineFormatter, trim_log
44
+ from log_python.heartbeat import Heartbeat
45
+ from log_python.logtype import LogType, handlers_installed, install_handlers, uninstall_handlers
46
+ from log_python.manager import (
47
+ EXCEPTION_CHAIN_FIELD,
48
+ LogManager,
49
+ get_or_create_request_id,
50
+ reset_request_id,
51
+ set_request_id,
52
+ )
53
+ from log_python.redactor import BUILTIN_NEEDLES, MASK, Redactor
54
+ from log_python.trace import CapturedTrace, TraceCapture, exception_class_name, exception_origin, should_skip_trace
55
+
56
+ __version__ = LOGGER_VER
57
+
58
+ _SETUP_HANDLER: logging.Handler | None = None
59
+
60
+
61
+ class _PylogStreamHandler(logging.StreamHandler[TextIO]):
62
+ _pylog_handler = True
63
+
64
+
65
+ def setup(
66
+ *,
67
+ config: PylogConfig | None = None,
68
+ level: int = logging.INFO,
69
+ stream: TextIO | None = None,
70
+ formatter: str | logging.Formatter = "json",
71
+ replace: bool = False,
72
+ ) -> logging.Handler:
73
+ """Configure one idempotent structured handler on the root logger."""
74
+ global _SETUP_HANDLER
75
+ resolved = config or PylogConfig.from_env()
76
+ root = logging.getLogger()
77
+ if _SETUP_HANDLER is not None and _SETUP_HANDLER in root.handlers and not replace:
78
+ return _SETUP_HANDLER
79
+ if _SETUP_HANDLER is not None:
80
+ root.removeHandler(_SETUP_HANDLER)
81
+ handler = _PylogStreamHandler(stream)
82
+ handler.terminator = ""
83
+ handler.setLevel(level)
84
+ handler.addFilter(ExtraFilter(resolved))
85
+ if isinstance(formatter, logging.Formatter):
86
+ selected = formatter
87
+ elif formatter == "json":
88
+ selected = JsonFormatter(resolved)
89
+ elif formatter == "line":
90
+ selected = LineFormatter(resolved)
91
+ elif formatter == "console":
92
+ selected = ConsoleFormatter(resolved)
93
+ else:
94
+ raise ValueError(f"Unknown pylog formatter: {formatter!r}")
95
+ handler.setFormatter(selected)
96
+ root.addHandler(handler)
97
+ if root.level == logging.NOTSET or root.level > level:
98
+ root.setLevel(level)
99
+ _SETUP_HANDLER = handler
100
+ if resolved.capture_handlers:
101
+ install_handlers()
102
+ return handler
103
+
104
+
105
+ __all__ = [
106
+ "BUILTIN_NEEDLES",
107
+ "CONTEXT_FALSE_VALUES",
108
+ "CONTEXT_MAX_STRING_BYTES",
109
+ "CONTEXT_TYPE_BOOL",
110
+ "CONTEXT_TYPE_FLOAT",
111
+ "CONTEXT_TYPE_INT",
112
+ "CONTEXT_TYPE_SKIP",
113
+ "CONTEXT_TYPE_STRING",
114
+ "EXCEPTION_CHAIN_FIELD",
115
+ "LOGGER_COUNT",
116
+ "LOGGER_MCTIME",
117
+ "LOGGER_MEMORY",
118
+ "LOGGER_MEMORY_PEAK",
119
+ "LOGGER_MONITORING",
120
+ "LOGGER_SKIP_TRACE",
121
+ "LOGGER_STATUS",
122
+ "LOGGER_SUCCESS",
123
+ "LOGGER_SUM",
124
+ "LOGGER_TAG",
125
+ "LOGGER_TIME",
126
+ "LOGGER_VER",
127
+ "MASK",
128
+ "CapturedTrace",
129
+ "ConsoleFormatter",
130
+ "ContextTypeCorrector",
131
+ "ExtraConfig",
132
+ "ExtraFilter",
133
+ "Heartbeat",
134
+ "JsonFormatter",
135
+ "LineFormatter",
136
+ "LogManager",
137
+ "LogType",
138
+ "PylogConfig",
139
+ "Redactor",
140
+ "TraceCapture",
141
+ "TraceConfig",
142
+ "TraceDepthConfig",
143
+ "__version__",
144
+ "bind",
145
+ "bound_context",
146
+ "clear",
147
+ "exception_class_name",
148
+ "exception_origin",
149
+ "get_bound_context",
150
+ "get_or_create_request_id",
151
+ "handlers_installed",
152
+ "install_handlers",
153
+ "remote_ip",
154
+ "reset_request_id",
155
+ "set_request_id",
156
+ "setup",
157
+ "should_skip_trace",
158
+ "trim_log",
159
+ "uninstall_handlers",
160
+ ]
log_python/config.py ADDED
@@ -0,0 +1,144 @@
1
+ """Runtime configuration ported from the reference `config/logger.php`.
2
+
3
+ The PHP original reads `env()` at config-definition time on purpose (its
4
+ comment: values are captured by `php artisan config:cache`, so they survive
5
+ into production, unlike an `env()` call from runtime code once the cache is
6
+ warm). The Python equivalent hazard is `os.environ` mutation after startup;
7
+ `PylogConfig.from_env()` mirrors the intent by taking a single snapshot at
8
+ construction time via an explicitly injectable mapping, so tests never need
9
+ to mutate the real process environment.
10
+
11
+ Every config object here is a frozen dataclass: build one at process start
12
+ and pass it around, never mutate it. `redact` and `context_type_rules` are
13
+ merge-on-top overrides consumed by later phases (Redactor, context type
14
+ coercion respectively) — they default to empty here.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ from collections.abc import Mapping
21
+ from dataclasses import dataclass, field
22
+ from types import MappingProxyType
23
+
24
+ from log_python.const import LOGGER_VER
25
+
26
+ _TRUE_VALUES = frozenset({"1", "true", "yes", "on"})
27
+ _FALSE_VALUES = frozenset({"0", "false", "no", "off", ""})
28
+
29
+ # Python-side substitution for the PHP `excluded_partials` default
30
+ # (`Monolog`, `Illuminate/Log/`, `vendor/`) — see mapping.md.
31
+ _DEFAULT_EXCLUDED_PARTIALS: tuple[str, ...] = ("logging/", "site-packages/", "log_python/")
32
+
33
+ _EMPTY_CONTEXT_TYPE_RULES: Mapping[str, Mapping[str, str]] = MappingProxyType({})
34
+
35
+
36
+ def _parse_bool(env: Mapping[str, str], key: str, default: bool) -> bool:
37
+ """Parse a boolean env var.
38
+
39
+ Accepts (case-insensitive) ``1``/``true``/``yes``/``on`` as True and
40
+ ``0``/``false``/``no``/``off``/empty-string as False. Any other value
41
+ raises ``ValueError`` naming the offending variable.
42
+ """
43
+ if key not in env:
44
+ return default
45
+ raw = env[key].strip().lower()
46
+ if raw in _TRUE_VALUES:
47
+ return True
48
+ if raw in _FALSE_VALUES:
49
+ return False
50
+ raise ValueError(f"Environment variable {key!r} is not a valid boolean: {env[key]!r}")
51
+
52
+
53
+ def _parse_int(env: Mapping[str, str], key: str, default: int) -> int:
54
+ """Parse an integer env var, raising ``ValueError`` naming the variable on non-integer input."""
55
+ if key not in env:
56
+ return default
57
+ raw = env[key].strip()
58
+ try:
59
+ return int(raw)
60
+ except ValueError as exc:
61
+ raise ValueError(f"Environment variable {key!r} is not a valid integer: {env[key]!r}") from exc
62
+
63
+
64
+ def _parse_optional_str(env: Mapping[str, str], key: str) -> str | None:
65
+ """Return the raw env var value, or None when unset (mirrors PHP `env()`)."""
66
+ return env.get(key)
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class ExtraConfig:
71
+ """Stable-per-process fields dumped onto every record (spec §4.2 extra)."""
72
+
73
+ app_name: str | None = None
74
+ app_env: str | None = None
75
+ app_ver: str | None = None
76
+ log_ver: str = LOGGER_VER
77
+ tier: str | None = None
78
+ release_tag: str | None = None
79
+ release_time: str | None = None
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class TraceDepthConfig:
84
+ """Trace depth (frames captured) per severity level."""
85
+
86
+ warning: int = 5
87
+ error: int = 10
88
+ critical: int = 20 # also alert / emergency
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class TraceConfig:
93
+ """Stack trace capture settings (spec §3.7 / §4.6)."""
94
+
95
+ excluded_partials: tuple[str, ...] = _DEFAULT_EXCLUDED_PARTIALS
96
+ depth: TraceDepthConfig = field(default_factory=TraceDepthConfig)
97
+ arg_limit: int = 128
98
+
99
+
100
+ @dataclass(frozen=True, slots=True)
101
+ class PylogConfig:
102
+ """Top-level configuration, ported 1:1 (keys and defaults) from `config/logger.php`."""
103
+
104
+ message_limit: int = 3024
105
+ allow_memory: bool = False
106
+ extra: ExtraConfig = field(default_factory=ExtraConfig)
107
+ trace: TraceConfig = field(default_factory=TraceConfig)
108
+ # Extra redaction needles, merged on top of the Redactor built-ins (phase 2).
109
+ redact: tuple[str, ...] = ()
110
+ snake_case: bool = False
111
+ # Project overrides merged on top of the context type-coercion table (phase 3).
112
+ context_type_rules: Mapping[str, Mapping[str, str]] = field(default_factory=lambda: _EMPTY_CONTEXT_TYPE_RULES)
113
+ capture_handlers: bool = False
114
+
115
+ @classmethod
116
+ def from_env(cls, env: Mapping[str, str] | None = None) -> PylogConfig:
117
+ """Build a config snapshot from environment variables, read exactly once.
118
+
119
+ `env` defaults to `os.environ`; pass an explicit mapping (e.g. in
120
+ tests) so the real process environment is never touched.
121
+ """
122
+ resolved_env: Mapping[str, str] = os.environ if env is None else env
123
+
124
+ extra = ExtraConfig(
125
+ app_name=_parse_optional_str(resolved_env, "APP_NAME"),
126
+ app_env=_parse_optional_str(resolved_env, "APP_ENV"),
127
+ app_ver=_parse_optional_str(resolved_env, "APP_VERSION"),
128
+ # log_ver is a compile-time constant in the reference, not env()-backed.
129
+ log_ver=LOGGER_VER,
130
+ tier=_parse_optional_str(resolved_env, "LOG_TIER"),
131
+ release_tag=_parse_optional_str(resolved_env, "RELEASE_TAG"),
132
+ release_time=_parse_optional_str(resolved_env, "RELEASE_TIME"),
133
+ )
134
+
135
+ return cls(
136
+ message_limit=_parse_int(resolved_env, "LOG_MESSAGE_LIMIT", 3024),
137
+ allow_memory=_parse_bool(resolved_env, "LOG_ALLOW_MEMORY", default=False),
138
+ extra=extra,
139
+ trace=TraceConfig(),
140
+ redact=(),
141
+ snake_case=_parse_bool(resolved_env, "LOG_SNAKE_CASE", default=False),
142
+ context_type_rules=_EMPTY_CONTEXT_TYPE_RULES,
143
+ capture_handlers=_parse_bool(resolved_env, "LOG_CAPTURE_HANDLERS", default=False),
144
+ )
log_python/const.py ADDED
@@ -0,0 +1,41 @@
1
+ """Field-name contract ported from the reference `src/const.php`.
2
+
3
+ Each ``LOGGER_*`` constant is a context/record field NAME (a dict key), not a
4
+ value — they must stay byte-for-byte identical to the PHP reference so a
5
+ mixed PHP/Python fleet emits the same field names on the wire.
6
+
7
+ ``LOGGER_VER`` is the odd one out: it is the installed library version stamped
8
+ into the ``log_ver`` extra field (see ``config.ExtraConfig.log_ver``). It is
9
+ read from standard distribution metadata generated from ``pyproject.toml``;
10
+ ``log_python.__version__`` aliases it, so releases have one authoritative version
11
+ declaration compatible with Hatchling, Poetry, PDM, Pipenv and pip.
12
+
13
+ PHP's ``DS`` (``DIRECTORY_SEPARATOR``) constant is PHP-only cruft and is
14
+ deliberately not ported.
15
+ """
16
+
17
+ from importlib.metadata import PackageNotFoundError, version
18
+ from typing import Final
19
+
20
+
21
+ def _package_version() -> str:
22
+ try:
23
+ return version("xakki-log-python")
24
+ except PackageNotFoundError:
25
+ # Source-tree fallback for tools that import without installing the project.
26
+ return "0+unknown"
27
+
28
+
29
+ LOGGER_VER: Final[str] = _package_version()
30
+
31
+ LOGGER_TIME: Final[str] = "time"
32
+ LOGGER_MCTIME: Final[str] = "mctime" # float seconds, e.g. 1.000001
33
+ LOGGER_MONITORING: Final[str] = "monitoring"
34
+ LOGGER_STATUS: Final[str] = "status"
35
+ LOGGER_COUNT: Final[str] = "count"
36
+ LOGGER_SUM: Final[str] = "sum"
37
+ LOGGER_MEMORY: Final[str] = "memory_usage"
38
+ LOGGER_MEMORY_PEAK: Final[str] = "memory_peak"
39
+ LOGGER_TAG: Final[str] = "tag"
40
+ LOGGER_SUCCESS: Final[str] = "success"
41
+ LOGGER_SKIP_TRACE: Final[str] = "skipTrace"