stdlogkit 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.
stdlogkit/__init__.py ADDED
@@ -0,0 +1,44 @@
1
+ """Auto-configuring helpers for Python's standard :mod:`logging` package.
2
+
3
+ Importing :mod:`stdlogkit` configures standard-library logging immediately
4
+ unless ``STDLOGKIT_CONFIGURE_LOGGING=0`` is set.
5
+ """
6
+
7
+ from stdlogkit.access_log_filter import (
8
+ UvicornAccessLogFilter,
9
+ create_uvicorn_log_config,
10
+ )
11
+ from stdlogkit.formatter import ColoredFormatter, NewLineFormatter
12
+ from stdlogkit.lazy import lazy
13
+ from stdlogkit.logger import (
14
+ LogScope,
15
+ configure_logging,
16
+ current_formatter_type,
17
+ get_logger,
18
+ init_logger,
19
+ install_logger_methods,
20
+ reset_once_cache,
21
+ suppress_logging,
22
+ )
23
+ from stdlogkit.timing import logtime
24
+
25
+ __version__ = "0.1.0"
26
+
27
+ configure_logging()
28
+
29
+ __all__ = [
30
+ "ColoredFormatter",
31
+ "LogScope",
32
+ "NewLineFormatter",
33
+ "UvicornAccessLogFilter",
34
+ "configure_logging",
35
+ "create_uvicorn_log_config",
36
+ "current_formatter_type",
37
+ "get_logger",
38
+ "init_logger",
39
+ "install_logger_methods",
40
+ "lazy",
41
+ "logtime",
42
+ "reset_once_cache",
43
+ "suppress_logging",
44
+ ]
@@ -0,0 +1,89 @@
1
+ """Optional uvicorn access-log filtering helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from urllib.parse import urlparse
7
+
8
+
9
+ class UvicornAccessLogFilter(logging.Filter):
10
+ """Filter uvicorn access logs for exact endpoint paths."""
11
+
12
+ def __init__(self, excluded_paths: list[str] | None = None):
13
+ super().__init__()
14
+ self.excluded_paths = set(excluded_paths or [])
15
+
16
+ def filter(self, record: logging.LogRecord) -> bool:
17
+ if not self.excluded_paths or record.name != "uvicorn.access":
18
+ return True
19
+
20
+ log_args = record.args
21
+ if isinstance(log_args, tuple) and len(log_args) >= 3:
22
+ path_with_query = log_args[2]
23
+ if isinstance(path_with_query, str):
24
+ path = urlparse(path_with_query).path
25
+ return path not in self.excluded_paths
26
+
27
+ return True
28
+
29
+
30
+ def create_uvicorn_log_config(
31
+ excluded_paths: list[str] | None = None,
32
+ log_level: str = "info",
33
+ ) -> dict:
34
+ """Create a uvicorn ``log_config`` dictionary with access-log filtering."""
35
+
36
+ return {
37
+ "version": 1,
38
+ "disable_existing_loggers": False,
39
+ "filters": {
40
+ "access_log_filter": {
41
+ "()": UvicornAccessLogFilter,
42
+ "excluded_paths": excluded_paths or [],
43
+ },
44
+ },
45
+ "formatters": {
46
+ "default": {
47
+ "()": "uvicorn.logging.DefaultFormatter",
48
+ "fmt": "%(levelprefix)s %(message)s",
49
+ "use_colors": None,
50
+ },
51
+ "access": {
52
+ "()": "uvicorn.logging.AccessFormatter",
53
+ "fmt": (
54
+ '%(levelprefix)s %(client_addr)s - '
55
+ '"%(request_line)s" %(status_code)s'
56
+ ),
57
+ },
58
+ },
59
+ "handlers": {
60
+ "default": {
61
+ "formatter": "default",
62
+ "class": "logging.StreamHandler",
63
+ "stream": "ext://sys.stderr",
64
+ },
65
+ "access": {
66
+ "formatter": "access",
67
+ "class": "logging.StreamHandler",
68
+ "stream": "ext://sys.stdout",
69
+ "filters": ["access_log_filter"],
70
+ },
71
+ },
72
+ "loggers": {
73
+ "uvicorn": {
74
+ "handlers": ["default"],
75
+ "level": log_level.upper(),
76
+ "propagate": False,
77
+ },
78
+ "uvicorn.error": {
79
+ "handlers": ["default"],
80
+ "level": log_level.upper(),
81
+ "propagate": False,
82
+ },
83
+ "uvicorn.access": {
84
+ "handlers": ["access"],
85
+ "level": log_level.upper(),
86
+ "propagate": False,
87
+ },
88
+ },
89
+ }
stdlogkit/env.py ADDED
@@ -0,0 +1,46 @@
1
+ """Environment-backed settings for stdlogkit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+
9
+ def _bool_from_env(name: str, default: bool) -> bool:
10
+ raw = os.getenv(name)
11
+ if raw is None:
12
+ return default
13
+ return raw.strip().lower() not in {"0", "false", "no", "off", ""}
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Settings:
18
+ """Logging settings loaded from environment variables."""
19
+
20
+ configure_logging: bool = True
21
+ logging_level: str = "INFO"
22
+ logging_prefix: str = ""
23
+ logging_stream: str = "ext://sys.stdout"
24
+ logging_config_path: str | None = None
25
+ logging_color: str = "auto"
26
+ logger_name: str = ""
27
+ root_dir: str | None = None
28
+ show_rel_path: str = "debug"
29
+ no_color: bool = False
30
+
31
+ @classmethod
32
+ def from_env(cls) -> "Settings":
33
+ """Build settings from ``STDLOGKIT_*`` environment variables."""
34
+
35
+ return cls(
36
+ configure_logging=_bool_from_env("STDLOGKIT_CONFIGURE_LOGGING", True),
37
+ logging_level=os.getenv("STDLOGKIT_LOGGING_LEVEL", "INFO").upper(),
38
+ logging_prefix=os.getenv("STDLOGKIT_LOGGING_PREFIX", ""),
39
+ logging_stream=os.getenv("STDLOGKIT_LOGGING_STREAM", "ext://sys.stdout"),
40
+ logging_config_path=os.getenv("STDLOGKIT_LOGGING_CONFIG_PATH"),
41
+ logging_color=os.getenv("STDLOGKIT_LOGGING_COLOR", "auto").lower(),
42
+ logger_name=os.getenv("STDLOGKIT_LOGGER_NAME", ""),
43
+ root_dir=os.getenv("STDLOGKIT_ROOT_DIR"),
44
+ show_rel_path=os.getenv("STDLOGKIT_SHOW_REL_PATH", "debug").lower(),
45
+ no_color=_bool_from_env("NO_COLOR", False),
46
+ )
stdlogkit/formatter.py ADDED
@@ -0,0 +1,89 @@
1
+ """Formatters used by stdlogkit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from stdlogkit.env import Settings
10
+
11
+
12
+ def _shrink_path(relpath: Path) -> str:
13
+ parts = list(relpath.parts)
14
+ if len(parts) <= 4:
15
+ return "/".join(parts)
16
+ return "/".join(parts[:1] + ["..."] + parts[-2:])
17
+
18
+
19
+ class NewLineFormatter(logging.Formatter):
20
+ """Formatter that aligns every line in a multiline log message."""
21
+
22
+ def __init__(self, fmt: str, datefmt: str | None = None, style: str = "%"):
23
+ super().__init__(fmt, datefmt, style)
24
+ settings = Settings.from_env()
25
+ self.show_rel_path = settings.show_rel_path
26
+ self.root_dir = Path(settings.root_dir or os.getcwd()).resolve()
27
+
28
+ def _use_relpath(self, record: logging.LogRecord) -> bool:
29
+ if self.show_rel_path == "always":
30
+ return True
31
+ if self.show_rel_path == "never":
32
+ return False
33
+ return record.levelno <= logging.DEBUG
34
+
35
+ def _fileinfo(self, record: logging.LogRecord) -> str:
36
+ if not self._use_relpath(record):
37
+ return record.filename
38
+
39
+ pathname = getattr(record, "pathname", None)
40
+ if not pathname:
41
+ return record.filename
42
+
43
+ try:
44
+ relpath = Path(pathname).resolve().relative_to(self.root_dir)
45
+ except Exception:
46
+ relpath = Path(record.filename)
47
+ return _shrink_path(relpath)
48
+
49
+ def format(self, record: logging.LogRecord) -> str:
50
+ record.fileinfo = self._fileinfo(record)
51
+ msg = super().format(record)
52
+ message = record.getMessage()
53
+ if message:
54
+ prefix = msg.partition(message)[0]
55
+ msg = msg.replace("\n", "\r\n" + prefix)
56
+ return msg
57
+
58
+
59
+ class ColoredFormatter(NewLineFormatter):
60
+ """Formatter that colors level names and static metadata with ANSI codes."""
61
+
62
+ COLORS = {
63
+ "DEBUG": "\033[37m",
64
+ "INFO": "\033[32m",
65
+ "WARNING": "\033[33m",
66
+ "ERROR": "\033[31m",
67
+ "CRITICAL": "\033[35m",
68
+ }
69
+ GREY = "\033[90m"
70
+ RESET = "\033[0m"
71
+
72
+ def __init__(self, fmt: str, datefmt: str | None = None, style: str = "%"):
73
+ if fmt:
74
+ fmt = fmt.replace("%(asctime)s", f"{self.GREY}%(asctime)s{self.RESET}")
75
+ fmt = fmt.replace(
76
+ "[%(fileinfo)s:%(lineno)d]",
77
+ f"{self.GREY}[%(fileinfo)s:%(lineno)d]{self.RESET}",
78
+ )
79
+ super().__init__(fmt, datefmt, style)
80
+
81
+ def format(self, record: logging.LogRecord) -> str:
82
+ original_levelname = record.levelname
83
+ color = self.COLORS.get(original_levelname)
84
+ if color is not None:
85
+ record.levelname = f"{color}{original_levelname}{self.RESET}"
86
+ try:
87
+ return super().format(record)
88
+ finally:
89
+ record.levelname = original_levelname
stdlogkit/lazy.py ADDED
@@ -0,0 +1,21 @@
1
+ """Lazy log message arguments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+
9
+ class lazy:
10
+ """Wrap a zero-argument callable evaluated only during log formatting."""
11
+
12
+ __slots__ = ("_factory",)
13
+
14
+ def __init__(self, factory: Callable[[], Any]) -> None:
15
+ self._factory = factory
16
+
17
+ def __str__(self) -> str:
18
+ return str(self._factory())
19
+
20
+ def __repr__(self) -> str:
21
+ return str(self)
stdlogkit/logger.py ADDED
@@ -0,0 +1,298 @@
1
+ """Configuration and extensions for Python's standard logging package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import sys
9
+ import threading
10
+ from collections.abc import Generator
11
+ from contextlib import contextmanager
12
+ from logging import Logger
13
+ from logging.config import dictConfig
14
+ from pathlib import Path
15
+ from typing import Any, Literal, TypeVar, cast
16
+
17
+ from stdlogkit.env import Settings
18
+ from stdlogkit.formatter import ColoredFormatter, NewLineFormatter
19
+
20
+ _FORMAT_TEMPLATE = (
21
+ "{prefix}%(levelname)s %(asctime)s "
22
+ "[%(fileinfo)s:%(lineno)d] %(message)s"
23
+ )
24
+ _DATE_FORMAT = "%m-%d %H:%M:%S"
25
+
26
+ LogScope = Literal["process", "global", "local"]
27
+ _LoggerT = TypeVar("_LoggerT", bound=Logger)
28
+ _CONFIGURED = False
29
+ _ONCE_LOCK = threading.RLock()
30
+ _ONCE_KEYS: set[tuple[str, int, str, tuple[str, ...], str]] = set()
31
+
32
+
33
+ def _env_rank(names: tuple[str, ...]) -> int:
34
+ for name in names:
35
+ raw = os.getenv(name)
36
+ if raw is None:
37
+ continue
38
+ try:
39
+ return int(raw)
40
+ except ValueError:
41
+ return 0
42
+ return 0
43
+
44
+
45
+ def _should_log_with_scope(scope: LogScope) -> bool:
46
+ if scope == "process":
47
+ return True
48
+ if scope == "global":
49
+ return _env_rank(("RANK", "WORLD_RANK", "SLURM_PROCID")) == 0
50
+ if scope == "local":
51
+ return (
52
+ _env_rank(
53
+ ("LOCAL_RANK", "MPI_LOCALRANKID", "OMPI_COMM_WORLD_LOCAL_RANK")
54
+ )
55
+ == 0
56
+ )
57
+ return True
58
+
59
+
60
+ def _safe_key(value: Any) -> str:
61
+ try:
62
+ return repr(value)
63
+ except Exception:
64
+ return f"<unrepresentable {type(value).__name__}>"
65
+
66
+
67
+ def _log_once(
68
+ logger: Logger,
69
+ level: int,
70
+ msg: str,
71
+ args: tuple[Any, ...],
72
+ scope: LogScope,
73
+ kwargs: dict[str, Any],
74
+ ) -> None:
75
+ if not _should_log_with_scope(scope):
76
+ return
77
+
78
+ key = (logger.name, level, msg, tuple(_safe_key(arg) for arg in args), scope)
79
+ with _ONCE_LOCK:
80
+ if key in _ONCE_KEYS:
81
+ return
82
+ _ONCE_KEYS.add(key)
83
+
84
+ kwargs.setdefault("stacklevel", 3)
85
+ logger.log(level, msg, *args, **kwargs)
86
+
87
+
88
+ class StdLogKitLogger(Logger):
89
+ """Logger type used for static typing of stdlogkit helper methods."""
90
+
91
+ def debug_once(
92
+ self,
93
+ msg: str,
94
+ *args: Any,
95
+ scope: LogScope = "local",
96
+ **kwargs: Any,
97
+ ) -> None:
98
+ """Log a DEBUG message once per logger/message/argument tuple."""
99
+
100
+ _log_once(self, logging.DEBUG, msg, args, scope, kwargs)
101
+
102
+ def info_once(
103
+ self,
104
+ msg: str,
105
+ *args: Any,
106
+ scope: LogScope = "local",
107
+ **kwargs: Any,
108
+ ) -> None:
109
+ """Log an INFO message once per logger/message/argument tuple."""
110
+
111
+ _log_once(self, logging.INFO, msg, args, scope, kwargs)
112
+
113
+ def warning_once(
114
+ self,
115
+ msg: str,
116
+ *args: Any,
117
+ scope: LogScope = "local",
118
+ **kwargs: Any,
119
+ ) -> None:
120
+ """Log a WARNING message once per logger/message/argument tuple."""
121
+
122
+ _log_once(self, logging.WARNING, msg, args, scope, kwargs)
123
+
124
+
125
+ _METHODS_TO_PATCH = {
126
+ "debug_once": StdLogKitLogger.debug_once,
127
+ "info_once": StdLogKitLogger.info_once,
128
+ "warning_once": StdLogKitLogger.warning_once,
129
+ }
130
+
131
+
132
+ def install_logger_methods(*, overwrite: bool = False) -> None:
133
+ """Install ``*_once`` methods onto standard-library logger classes."""
134
+
135
+ for target in (logging.Logger, logging.RootLogger):
136
+ for name, method in _METHODS_TO_PATCH.items():
137
+ if overwrite or not hasattr(target, name):
138
+ setattr(target, name, method)
139
+
140
+
141
+ def _use_color(settings: Settings) -> bool:
142
+ if settings.no_color or settings.logging_color == "0":
143
+ return False
144
+ if settings.logging_color == "1":
145
+ return True
146
+ if settings.logging_color != "auto":
147
+ return False
148
+
149
+ if settings.logging_stream == "ext://sys.stdout":
150
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
151
+ if settings.logging_stream == "ext://sys.stderr":
152
+ return hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
153
+ return False
154
+
155
+
156
+ def _default_logging_config(settings: Settings) -> dict[str, Any]:
157
+ formatter_name = "stdlogkit_color" if _use_color(settings) else "stdlogkit"
158
+ config: dict[str, Any] = {
159
+ "version": 1,
160
+ "disable_existing_loggers": False,
161
+ "formatters": {
162
+ "stdlogkit": {
163
+ "class": "stdlogkit.formatter.NewLineFormatter",
164
+ "datefmt": _DATE_FORMAT,
165
+ "format": _FORMAT_TEMPLATE.format(prefix=settings.logging_prefix),
166
+ },
167
+ "stdlogkit_color": {
168
+ "class": "stdlogkit.formatter.ColoredFormatter",
169
+ "datefmt": _DATE_FORMAT,
170
+ "format": _FORMAT_TEMPLATE.format(prefix=settings.logging_prefix),
171
+ },
172
+ },
173
+ "handlers": {
174
+ "stdlogkit": {
175
+ "class": "logging.StreamHandler",
176
+ "formatter": formatter_name,
177
+ "level": settings.logging_level,
178
+ "stream": settings.logging_stream,
179
+ },
180
+ },
181
+ }
182
+
183
+ if settings.logger_name:
184
+ config["loggers"] = {
185
+ settings.logger_name: {
186
+ "handlers": ["stdlogkit"],
187
+ "level": settings.logging_level,
188
+ "propagate": False,
189
+ },
190
+ }
191
+ else:
192
+ config["root"] = {
193
+ "handlers": ["stdlogkit"],
194
+ "level": settings.logging_level,
195
+ }
196
+
197
+ return config
198
+
199
+
200
+ def _load_custom_config(path: str) -> dict[str, Any]:
201
+ config_path = Path(path)
202
+ if not config_path.exists():
203
+ raise RuntimeError(
204
+ "Could not load logging config. File does not exist: "
205
+ f"{path}"
206
+ )
207
+
208
+ with config_path.open(encoding="utf-8") as file:
209
+ custom_config = json.loads(file.read())
210
+
211
+ if not isinstance(custom_config, dict):
212
+ raise ValueError(
213
+ "Invalid logging config. Expected dict, got "
214
+ f"{type(custom_config).__name__}."
215
+ )
216
+ return custom_config
217
+
218
+
219
+ def configure_logging(*, force: bool = False, settings: Settings | None = None) -> None:
220
+ """Configure standard-library logging from environment-backed settings.
221
+
222
+ Importing :mod:`stdlogkit` calls this once automatically. Call it manually
223
+ with ``force=True`` after changing environment variables in tests or tools.
224
+ """
225
+
226
+ global _CONFIGURED
227
+
228
+ install_logger_methods()
229
+ if _CONFIGURED and not force:
230
+ return
231
+
232
+ settings = settings or Settings.from_env()
233
+ if not settings.configure_logging and settings.logging_config_path:
234
+ raise RuntimeError(
235
+ "STDLOGKIT_CONFIGURE_LOGGING evaluated to false, but "
236
+ "STDLOGKIT_LOGGING_CONFIG_PATH was given. Enable "
237
+ "STDLOGKIT_CONFIGURE_LOGGING or unset STDLOGKIT_LOGGING_CONFIG_PATH."
238
+ )
239
+
240
+ if not settings.configure_logging:
241
+ _CONFIGURED = True
242
+ return
243
+
244
+ logging_config = (
245
+ _load_custom_config(settings.logging_config_path)
246
+ if settings.logging_config_path
247
+ else _default_logging_config(settings)
248
+ )
249
+ dictConfig(logging_config)
250
+ _CONFIGURED = True
251
+
252
+
253
+ def init_logger(name: str) -> StdLogKitLogger:
254
+ """Return a logger with stdlogkit's ``*_once`` helper methods available."""
255
+
256
+ install_logger_methods()
257
+ return cast(StdLogKitLogger, logging.getLogger(name))
258
+
259
+
260
+ def get_logger(name: str) -> StdLogKitLogger:
261
+ """Alias for :func:`init_logger`."""
262
+
263
+ return init_logger(name)
264
+
265
+
266
+ def reset_once_cache() -> None:
267
+ """Clear the de-duplication cache used by ``*_once`` methods."""
268
+
269
+ with _ONCE_LOCK:
270
+ _ONCE_KEYS.clear()
271
+
272
+
273
+ @contextmanager
274
+ def suppress_logging(level: int = logging.INFO) -> Generator[None, Any, None]:
275
+ """Temporarily suppress logging up to ``level``."""
276
+
277
+ current_level = logging.root.manager.disable
278
+ logging.disable(level)
279
+ try:
280
+ yield
281
+ finally:
282
+ logging.disable(current_level)
283
+
284
+
285
+ def current_formatter_type(logger: Logger) -> Literal["color", "newline", None]:
286
+ """Return the stdlogkit formatter type found on ``logger`` or its parents."""
287
+
288
+ current: Logger | None = logger
289
+ while current is not None:
290
+ for handler in current.handlers:
291
+ if handler.name == "stdlogkit":
292
+ formatter = handler.formatter
293
+ if isinstance(formatter, ColoredFormatter):
294
+ return "color"
295
+ if isinstance(formatter, NewLineFormatter):
296
+ return "newline"
297
+ current = current.parent
298
+ return None
stdlogkit/py.typed ADDED
@@ -0,0 +1 @@
1
+
stdlogkit/timing.py ADDED
@@ -0,0 +1,34 @@
1
+ """Timing helpers for logging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import time
7
+ from collections.abc import Callable
8
+ from typing import ParamSpec, TypeVar
9
+
10
+ P = ParamSpec("P")
11
+ R = TypeVar("R")
12
+
13
+
14
+ def logtime(logger, msg: str | None = None):
15
+ """Log the execution time of the decorated function at DEBUG level."""
16
+
17
+ def _inner(func: Callable[P, R]) -> Callable[P, R]:
18
+ @functools.wraps(func)
19
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
20
+ start = time.perf_counter()
21
+ result = func(*args, **kwargs)
22
+ elapsed = time.perf_counter() - start
23
+
24
+ prefix = (
25
+ f"Function '{func.__module__}.{func.__qualname__}'"
26
+ if msg is None
27
+ else msg
28
+ )
29
+ logger.debug("%s: Elapsed time %.7f secs", prefix, elapsed)
30
+ return result
31
+
32
+ return _wrapper
33
+
34
+ return _inner
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: stdlogkit
3
+ Version: 0.1.0
4
+ Summary: A small stdlib logging auto-configuration kit with colored multiline logs and log-once helpers.
5
+ Author-email: Your Name <you@example.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/your-org/stdlogkit
8
+ Project-URL: Repository, https://github.com/your-org/stdlogkit
9
+ Project-URL: Issues, https://github.com/your-org/stdlogkit/issues
10
+ Keywords: logging,stdlib,colored-logs,python-logging
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: System :: Logging
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2; extra == "dev"
25
+ Requires-Dist: pytest>=8; extra == "dev"
26
+ Requires-Dist: twine>=5; extra == "dev"
27
+
28
+ # stdlogkit
29
+
30
+ `stdlogkit` is a tiny, vLLM-independent wrapper around Python's standard
31
+ `logging` package.
32
+
33
+ Importing it configures standard logging immediately:
34
+
35
+ ```python
36
+ import logging
37
+ import stdlogkit # noqa: F401
38
+
39
+ logger = logging.getLogger(__name__)
40
+ logger.info("hello")
41
+ logger.warning_once("this warning appears once")
42
+ ```
43
+
44
+ Default output:
45
+
46
+ ```text
47
+ INFO 06-24 12:00:01 [example.py:5] hello
48
+ WARNING 06-24 12:00:01 [example.py:6] this warning appears once
49
+ ```
50
+
51
+ ## Features
52
+
53
+ - Uses only Python's standard `logging` machinery.
54
+ - Auto-configures logging on `import stdlogkit`.
55
+ - Configures the root logger by default, so normal `logging.getLogger(...)`
56
+ calls work immediately.
57
+ - Adds `debug_once`, `info_once`, and `warning_once` to standard logger
58
+ instances.
59
+ - Supports multiline log alignment.
60
+ - Supports ANSI colors with `auto`, forced-on, and forced-off modes.
61
+ - Supports custom JSON config through `logging.config.dictConfig`.
62
+ - Includes optional uvicorn access-log filtering helpers.
63
+ - Has no runtime dependency on vLLM or any other third-party package.
64
+
65
+ ## Environment Variables
66
+
67
+ | Variable | Default | Description |
68
+ | --- | --- | --- |
69
+ | `STDLOGKIT_CONFIGURE_LOGGING` | `1` | Set `0` to disable auto configuration. |
70
+ | `STDLOGKIT_LOGGING_LEVEL` | `INFO` | Root or named logger level. |
71
+ | `STDLOGKIT_LOGGING_STREAM` | `ext://sys.stdout` | Handler stream. |
72
+ | `STDLOGKIT_LOGGING_PREFIX` | empty | Prefix prepended to every log line. |
73
+ | `STDLOGKIT_LOGGING_COLOR` | `auto` | `auto`, `1`, or `0`. |
74
+ | `STDLOGKIT_LOGGING_CONFIG_PATH` | unset | Path to a JSON `dictConfig` file. |
75
+ | `STDLOGKIT_LOGGER_NAME` | empty | Configure a named logger instead of root. |
76
+ | `STDLOGKIT_ROOT_DIR` | current working directory | Base path for relative file display. |
77
+ | `STDLOGKIT_SHOW_REL_PATH` | `debug` | `debug`, `always`, or `never`. |
78
+ | `NO_COLOR` | `0` | Standard flag to disable ANSI colors. |
79
+
80
+ ## Custom JSON Config
81
+
82
+ ```json
83
+ {
84
+ "version": 1,
85
+ "disable_existing_loggers": false,
86
+ "formatters": {
87
+ "plain": {
88
+ "class": "stdlogkit.formatter.NewLineFormatter",
89
+ "format": "%(levelname)s %(asctime)s [%(fileinfo)s:%(lineno)d] %(message)s",
90
+ "datefmt": "%m-%d %H:%M:%S"
91
+ }
92
+ },
93
+ "handlers": {
94
+ "console": {
95
+ "class": "logging.StreamHandler",
96
+ "formatter": "plain",
97
+ "level": "INFO",
98
+ "stream": "ext://sys.stdout"
99
+ }
100
+ },
101
+ "root": {
102
+ "handlers": ["console"],
103
+ "level": "INFO"
104
+ }
105
+ }
106
+ ```
107
+
108
+ Run with:
109
+
110
+ ```bash
111
+ STDLOGKIT_LOGGING_CONFIG_PATH=/path/to/logging.json python app.py
112
+ ```
113
+
114
+ ## Named Logger Mode
115
+
116
+ By default, stdlogkit configures the root logger. If you want behavior closer
117
+ to a framework-specific logger, configure only a named logger:
118
+
119
+ ```bash
120
+ STDLOGKIT_LOGGER_NAME=my_app python app.py
121
+ ```
122
+
123
+ Then loggers under `my_app.*` propagate to that logger:
124
+
125
+ ```python
126
+ import logging
127
+ import stdlogkit # noqa: F401
128
+
129
+ logger = logging.getLogger("my_app.service")
130
+ logger.info("hello")
131
+ ```
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ cd standalone_logging_pkg
137
+ uv venv --python 3.12
138
+ uv pip install -e ".[dev]"
139
+ .venv/bin/python -m pytest -q
140
+ uv build
141
+ ```
142
+
143
+ ## Publish to PyPI
144
+
145
+ Before publishing, make sure the project name in `pyproject.toml` is available
146
+ on PyPI and replace the placeholder author/repository metadata.
147
+
148
+ ```bash
149
+ cd standalone_logging_pkg
150
+ uv build
151
+ uv publish --token "$PYPI_TOKEN"
152
+ ```
153
+
154
+ For TestPyPI:
155
+
156
+ ```bash
157
+ uv publish --publish-url https://test.pypi.org/legacy/ --token "$TEST_PYPI_TOKEN"
158
+ ```
@@ -0,0 +1,12 @@
1
+ stdlogkit/__init__.py,sha256=mvZx0HZ0BUERIxylPdjs7KT_iHLaNUd-hEjiqILjJH4,1029
2
+ stdlogkit/access_log_filter.py,sha256=vxjtbnVZEU_IsZXo4jAc1USG4pf8UW8yTebfUfLrIKE,2772
3
+ stdlogkit/env.py,sha256=gwjlOyuAJ6J-ZwFnq5zU6iPZ9gXzW406Ifz4800R7HI,1653
4
+ stdlogkit/formatter.py,sha256=004yCc5f24QTJpavqcIQbUtv0dRPUac9LcZQGuTWB-M,2867
5
+ stdlogkit/lazy.py,sha256=EXf0nO50E0yMD2DNeY8J9fTCtpH7C-DMRCzMurkXYKo,471
6
+ stdlogkit/logger.py,sha256=l-yY79NqwSGsGly7P2m9W52EcS2eoNtDT55zBLrOnz4,8621
7
+ stdlogkit/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
+ stdlogkit/timing.py,sha256=mBSmAbMpbScRmb6ie8dcKKSZt1PTHVGT1pRuvD_kiS4,921
9
+ stdlogkit-0.1.0.dist-info/METADATA,sha256=xOLojyZ-j_mnCw3KEI8ZIxQ3TAKKCaz9whHpIni5lBg,4619
10
+ stdlogkit-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ stdlogkit-0.1.0.dist-info/top_level.txt,sha256=3xP_hFyWZBuhl7bfMMxPGY9ubjIukESA7OkZr6HT1CI,10
12
+ stdlogkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ stdlogkit