stdlogkit 0.1.0__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.
@@ -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,131 @@
1
+ # stdlogkit
2
+
3
+ `stdlogkit` is a tiny, vLLM-independent wrapper around Python's standard
4
+ `logging` package.
5
+
6
+ Importing it configures standard logging immediately:
7
+
8
+ ```python
9
+ import logging
10
+ import stdlogkit # noqa: F401
11
+
12
+ logger = logging.getLogger(__name__)
13
+ logger.info("hello")
14
+ logger.warning_once("this warning appears once")
15
+ ```
16
+
17
+ Default output:
18
+
19
+ ```text
20
+ INFO 06-24 12:00:01 [example.py:5] hello
21
+ WARNING 06-24 12:00:01 [example.py:6] this warning appears once
22
+ ```
23
+
24
+ ## Features
25
+
26
+ - Uses only Python's standard `logging` machinery.
27
+ - Auto-configures logging on `import stdlogkit`.
28
+ - Configures the root logger by default, so normal `logging.getLogger(...)`
29
+ calls work immediately.
30
+ - Adds `debug_once`, `info_once`, and `warning_once` to standard logger
31
+ instances.
32
+ - Supports multiline log alignment.
33
+ - Supports ANSI colors with `auto`, forced-on, and forced-off modes.
34
+ - Supports custom JSON config through `logging.config.dictConfig`.
35
+ - Includes optional uvicorn access-log filtering helpers.
36
+ - Has no runtime dependency on vLLM or any other third-party package.
37
+
38
+ ## Environment Variables
39
+
40
+ | Variable | Default | Description |
41
+ | --- | --- | --- |
42
+ | `STDLOGKIT_CONFIGURE_LOGGING` | `1` | Set `0` to disable auto configuration. |
43
+ | `STDLOGKIT_LOGGING_LEVEL` | `INFO` | Root or named logger level. |
44
+ | `STDLOGKIT_LOGGING_STREAM` | `ext://sys.stdout` | Handler stream. |
45
+ | `STDLOGKIT_LOGGING_PREFIX` | empty | Prefix prepended to every log line. |
46
+ | `STDLOGKIT_LOGGING_COLOR` | `auto` | `auto`, `1`, or `0`. |
47
+ | `STDLOGKIT_LOGGING_CONFIG_PATH` | unset | Path to a JSON `dictConfig` file. |
48
+ | `STDLOGKIT_LOGGER_NAME` | empty | Configure a named logger instead of root. |
49
+ | `STDLOGKIT_ROOT_DIR` | current working directory | Base path for relative file display. |
50
+ | `STDLOGKIT_SHOW_REL_PATH` | `debug` | `debug`, `always`, or `never`. |
51
+ | `NO_COLOR` | `0` | Standard flag to disable ANSI colors. |
52
+
53
+ ## Custom JSON Config
54
+
55
+ ```json
56
+ {
57
+ "version": 1,
58
+ "disable_existing_loggers": false,
59
+ "formatters": {
60
+ "plain": {
61
+ "class": "stdlogkit.formatter.NewLineFormatter",
62
+ "format": "%(levelname)s %(asctime)s [%(fileinfo)s:%(lineno)d] %(message)s",
63
+ "datefmt": "%m-%d %H:%M:%S"
64
+ }
65
+ },
66
+ "handlers": {
67
+ "console": {
68
+ "class": "logging.StreamHandler",
69
+ "formatter": "plain",
70
+ "level": "INFO",
71
+ "stream": "ext://sys.stdout"
72
+ }
73
+ },
74
+ "root": {
75
+ "handlers": ["console"],
76
+ "level": "INFO"
77
+ }
78
+ }
79
+ ```
80
+
81
+ Run with:
82
+
83
+ ```bash
84
+ STDLOGKIT_LOGGING_CONFIG_PATH=/path/to/logging.json python app.py
85
+ ```
86
+
87
+ ## Named Logger Mode
88
+
89
+ By default, stdlogkit configures the root logger. If you want behavior closer
90
+ to a framework-specific logger, configure only a named logger:
91
+
92
+ ```bash
93
+ STDLOGKIT_LOGGER_NAME=my_app python app.py
94
+ ```
95
+
96
+ Then loggers under `my_app.*` propagate to that logger:
97
+
98
+ ```python
99
+ import logging
100
+ import stdlogkit # noqa: F401
101
+
102
+ logger = logging.getLogger("my_app.service")
103
+ logger.info("hello")
104
+ ```
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ cd standalone_logging_pkg
110
+ uv venv --python 3.12
111
+ uv pip install -e ".[dev]"
112
+ .venv/bin/python -m pytest -q
113
+ uv build
114
+ ```
115
+
116
+ ## Publish to PyPI
117
+
118
+ Before publishing, make sure the project name in `pyproject.toml` is available
119
+ on PyPI and replace the placeholder author/repository metadata.
120
+
121
+ ```bash
122
+ cd standalone_logging_pkg
123
+ uv build
124
+ uv publish --token "$PYPI_TOKEN"
125
+ ```
126
+
127
+ For TestPyPI:
128
+
129
+ ```bash
130
+ uv publish --publish-url https://test.pypi.org/legacy/ --token "$TEST_PYPI_TOKEN"
131
+ ```
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stdlogkit"
7
+ version = "0.1.0"
8
+ description = "A small stdlib logging auto-configuration kit with colored multiline logs and log-once helpers."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0"
12
+ authors = [
13
+ { name = "Your Name", email = "you@example.com" },
14
+ ]
15
+ keywords = ["logging", "stdlib", "colored-logs", "python-logging"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: System :: Logging",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "build>=1.2",
32
+ "pytest>=8",
33
+ "twine>=5",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/your-org/stdlogkit"
38
+ Repository = "https://github.com/your-org/stdlogkit"
39
+ Issues = "https://github.com/your-org/stdlogkit/issues"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ stdlogkit = ["py.typed"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ }
@@ -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
+ )
@@ -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
@@ -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)
@@ -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
@@ -0,0 +1 @@
1
+
@@ -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,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/stdlogkit/__init__.py
4
+ src/stdlogkit/access_log_filter.py
5
+ src/stdlogkit/env.py
6
+ src/stdlogkit/formatter.py
7
+ src/stdlogkit/lazy.py
8
+ src/stdlogkit/logger.py
9
+ src/stdlogkit/py.typed
10
+ src/stdlogkit/timing.py
11
+ src/stdlogkit.egg-info/PKG-INFO
12
+ src/stdlogkit.egg-info/SOURCES.txt
13
+ src/stdlogkit.egg-info/dependency_links.txt
14
+ src/stdlogkit.egg-info/requires.txt
15
+ src/stdlogkit.egg-info/top_level.txt
16
+ tests/test_stdlogkit.py
@@ -0,0 +1,5 @@
1
+
2
+ [dev]
3
+ build>=1.2
4
+ pytest>=8
5
+ twine>=5
@@ -0,0 +1 @@
1
+ stdlogkit
@@ -0,0 +1,177 @@
1
+ import importlib
2
+ import json
3
+ import logging
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ import stdlogkit
11
+ from stdlogkit.env import Settings
12
+ from stdlogkit.formatter import ColoredFormatter, NewLineFormatter
13
+ from stdlogkit.logger import configure_logging, current_formatter_type, reset_once_cache
14
+
15
+
16
+ class EnvTestCase(unittest.TestCase):
17
+ def setUp(self):
18
+ self._old_env = dict(os.environ)
19
+ for key in list(os.environ):
20
+ if key.startswith("STDLOGKIT_") or key == "NO_COLOR":
21
+ os.environ.pop(key, None)
22
+ reset_once_cache()
23
+
24
+ def tearDown(self):
25
+ os.environ.clear()
26
+ os.environ.update(self._old_env)
27
+ reset_once_cache()
28
+
29
+
30
+ class StdLogKitTests(EnvTestCase):
31
+ def test_import_configures_root_logger(self):
32
+ os.environ["STDLOGKIT_LOGGING_COLOR"] = "0"
33
+ configure_logging(force=True)
34
+
35
+ root = logging.getLogger()
36
+ self.assertEqual(root.level, logging.INFO)
37
+ self.assertEqual(len(root.handlers), 1)
38
+
39
+ handler = root.handlers[0]
40
+ self.assertEqual(handler.name, "stdlogkit")
41
+ self.assertIs(handler.stream, sys.stdout)
42
+ self.assertIsInstance(handler.formatter, NewLineFormatter)
43
+
44
+ def test_standard_loggers_have_once_methods(self):
45
+ logger = logging.getLogger("example")
46
+
47
+ self.assertTrue(hasattr(logger, "debug_once"))
48
+ self.assertTrue(hasattr(logger, "info_once"))
49
+ self.assertTrue(hasattr(logger, "warning_once"))
50
+
51
+ def test_once_methods_deduplicate_unhashable_args(self):
52
+ records = []
53
+
54
+ class CollectHandler(logging.Handler):
55
+ def emit(self, record):
56
+ records.append(record)
57
+
58
+ handler = CollectHandler()
59
+ root = logging.getLogger()
60
+ root.handlers = [handler]
61
+ root.setLevel(logging.DEBUG)
62
+
63
+ logger = logging.getLogger("example.once")
64
+ logger.warning_once("payload=%s", {"items": [1, 2]})
65
+ logger.warning_once("payload=%s", {"items": [1, 2]})
66
+
67
+ self.assertEqual(len(records), 1)
68
+ self.assertEqual(records[0].getMessage(), "payload={'items': [1, 2]}")
69
+
70
+ def test_custom_json_config_is_used(self):
71
+ config = {
72
+ "version": 1,
73
+ "disable_existing_loggers": False,
74
+ "handlers": {
75
+ "null": {
76
+ "class": "logging.NullHandler",
77
+ },
78
+ },
79
+ "root": {
80
+ "handlers": ["null"],
81
+ "level": "ERROR",
82
+ },
83
+ }
84
+
85
+ with tempfile.TemporaryDirectory() as tmpdir:
86
+ config_path = Path(tmpdir) / "logging.json"
87
+ config_path.write_text(json.dumps(config), encoding="utf-8")
88
+ os.environ["STDLOGKIT_LOGGING_CONFIG_PATH"] = str(config_path)
89
+
90
+ configure_logging(force=True)
91
+
92
+ root = logging.getLogger()
93
+ self.assertEqual(root.level, logging.ERROR)
94
+ self.assertIsInstance(root.handlers[0], logging.NullHandler)
95
+
96
+ def test_config_path_requires_configuration_enabled(self):
97
+ with tempfile.TemporaryDirectory() as tmpdir:
98
+ config_path = Path(tmpdir) / "logging.json"
99
+ config_path.write_text("{}", encoding="utf-8")
100
+
101
+ os.environ["STDLOGKIT_CONFIGURE_LOGGING"] = "0"
102
+ os.environ["STDLOGKIT_LOGGING_CONFIG_PATH"] = str(config_path)
103
+
104
+ with self.assertRaisesRegex(RuntimeError, "STDLOGKIT_CONFIGURE_LOGGING"):
105
+ configure_logging(force=True)
106
+
107
+ def test_formatter_aligns_multiline_messages(self):
108
+ os.environ["STDLOGKIT_LOGGING_COLOR"] = "0"
109
+ formatter = NewLineFormatter(
110
+ "%(levelname)s %(asctime)s [%(fileinfo)s:%(lineno)d] %(message)s",
111
+ "%m-%d %H:%M:%S",
112
+ )
113
+ record = logging.LogRecord(
114
+ name="example",
115
+ level=logging.INFO,
116
+ pathname=__file__,
117
+ lineno=123,
118
+ msg="hello\nworld",
119
+ args=(),
120
+ exc_info=None,
121
+ )
122
+
123
+ formatted = formatter.format(record)
124
+
125
+ self.assertIn("hello", formatted)
126
+ self.assertIn("\r\nINFO ", formatted)
127
+ self.assertIn("world", formatted)
128
+
129
+ def test_color_formatter_restores_levelname(self):
130
+ formatter = ColoredFormatter("%(levelname)s %(message)s")
131
+ record = logging.LogRecord(
132
+ name="example",
133
+ level=logging.WARNING,
134
+ pathname=__file__,
135
+ lineno=1,
136
+ msg="hello",
137
+ args=(),
138
+ exc_info=None,
139
+ )
140
+
141
+ formatted = formatter.format(record)
142
+
143
+ self.assertIn("\033[33mWARNING\033[0m", formatted)
144
+ self.assertEqual(record.levelname, "WARNING")
145
+
146
+ def test_named_logger_mode(self):
147
+ os.environ["STDLOGKIT_LOGGER_NAME"] = "my_app"
148
+ os.environ["STDLOGKIT_LOGGING_COLOR"] = "0"
149
+ configure_logging(force=True)
150
+
151
+ app_logger = logging.getLogger("my_app")
152
+ child_logger = logging.getLogger("my_app.service")
153
+
154
+ self.assertTrue(app_logger.handlers)
155
+ self.assertFalse(child_logger.handlers)
156
+ self.assertTrue(child_logger.propagate)
157
+ self.assertEqual(current_formatter_type(child_logger), "newline")
158
+
159
+ def test_settings_are_read_at_configure_time(self):
160
+ os.environ["STDLOGKIT_LOGGING_LEVEL"] = "DEBUG"
161
+ settings = Settings.from_env()
162
+
163
+ self.assertEqual(settings.logging_level, "DEBUG")
164
+
165
+ def test_module_can_be_reloaded_without_double_configuring(self):
166
+ os.environ["STDLOGKIT_LOGGING_COLOR"] = "0"
167
+ configure_logging(force=True)
168
+ root = logging.getLogger()
169
+ before = list(root.handlers)
170
+
171
+ importlib.reload(stdlogkit)
172
+
173
+ self.assertEqual(logging.getLogger().handlers, before)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ unittest.main()