utilityhub-logging 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.
- utilityhub_logging/__init__.py +26 -0
- utilityhub_logging/cleanup.py +58 -0
- utilityhub_logging/context.py +82 -0
- utilityhub_logging/formatters.py +49 -0
- utilityhub_logging/paths.py +49 -0
- utilityhub_logging/scopes.py +58 -0
- utilityhub_logging/setup.py +85 -0
- utilityhub_logging/types.py +29 -0
- utilityhub_logging-0.1.0.dist-info/METADATA +82 -0
- utilityhub_logging-0.1.0.dist-info/RECORD +12 -0
- utilityhub_logging-0.1.0.dist-info/WHEEL +4 -0
- utilityhub_logging-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Composable logging utilities built on Python's standard library logging package."""
|
|
2
|
+
|
|
3
|
+
from utilityhub_logging.cleanup import cleanup_logging
|
|
4
|
+
from utilityhub_logging.context import bind_context
|
|
5
|
+
from utilityhub_logging.formatters import JsonFormatter, PlainTextFormatter
|
|
6
|
+
from utilityhub_logging.paths import resolve_logs_path
|
|
7
|
+
from utilityhub_logging.scopes import begin_scope_logging, end_scope_logging
|
|
8
|
+
from utilityhub_logging.setup import configure_app_logging
|
|
9
|
+
from utilityhub_logging.types import LogFormat, LogPathConvention
|
|
10
|
+
|
|
11
|
+
__all__: list[str] = [
|
|
12
|
+
"JsonFormatter",
|
|
13
|
+
"LogFormat",
|
|
14
|
+
"LogPathConvention",
|
|
15
|
+
"PlainTextFormatter",
|
|
16
|
+
"begin_scope_logging",
|
|
17
|
+
"bind_context",
|
|
18
|
+
"cleanup_logging",
|
|
19
|
+
"configure_app_logging",
|
|
20
|
+
"end_scope_logging",
|
|
21
|
+
"resolve_logs_path",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
print("utilityhub-logging: import and configure via `configure_app_logging()`")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from utilityhub_logging.context import clear_context
|
|
7
|
+
from utilityhub_logging.types import ManagedHandlerKind
|
|
8
|
+
|
|
9
|
+
_MANAGED_HANDLER_FLAG = "_utilityhub_managed"
|
|
10
|
+
_MANAGED_HANDLER_KIND = "_utilityhub_kind"
|
|
11
|
+
_MANAGED_HANDLER_PATH = "_utilityhub_log_path"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def mark_handler(
|
|
15
|
+
handler: logging.Handler,
|
|
16
|
+
*,
|
|
17
|
+
kind: ManagedHandlerKind,
|
|
18
|
+
file_path: Path | None = None,
|
|
19
|
+
) -> logging.Handler:
|
|
20
|
+
setattr(handler, _MANAGED_HANDLER_FLAG, True)
|
|
21
|
+
setattr(handler, _MANAGED_HANDLER_KIND, kind.value)
|
|
22
|
+
setattr(handler, _MANAGED_HANDLER_PATH, file_path)
|
|
23
|
+
return handler
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_managed_handler(handler: logging.Handler, *, kind: ManagedHandlerKind | None = None) -> bool:
|
|
27
|
+
if not getattr(handler, _MANAGED_HANDLER_FLAG, False):
|
|
28
|
+
return False
|
|
29
|
+
if kind is None:
|
|
30
|
+
return True
|
|
31
|
+
return getattr(handler, _MANAGED_HANDLER_KIND, None) == kind.value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cleanup_logging(
|
|
35
|
+
logger: logging.Logger | None = None,
|
|
36
|
+
*,
|
|
37
|
+
kind: ManagedHandlerKind | None = None,
|
|
38
|
+
close_all_loggers: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
if close_all_loggers:
|
|
41
|
+
logger_dict = logging.root.manager.loggerDict
|
|
42
|
+
for candidate in [logging.getLogger()] + [
|
|
43
|
+
value for value in logger_dict.values() if isinstance(value, logging.Logger)
|
|
44
|
+
]:
|
|
45
|
+
cleanup_logging(candidate, kind=kind)
|
|
46
|
+
clear_context()
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
target = logging.getLogger() if logger is None else logger
|
|
50
|
+
handlers = list(target.handlers)
|
|
51
|
+
for handler in handlers:
|
|
52
|
+
if not is_managed_handler(handler, kind=kind):
|
|
53
|
+
continue
|
|
54
|
+
target.removeHandler(handler)
|
|
55
|
+
try:
|
|
56
|
+
handler.flush()
|
|
57
|
+
finally:
|
|
58
|
+
handler.close()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from contextlib import AbstractContextManager
|
|
7
|
+
from contextvars import Token
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_LOG_CONTEXT: contextvars.ContextVar[dict[str, Any] | None] = contextvars.ContextVar(
|
|
11
|
+
"utilityhub_logging_context",
|
|
12
|
+
default=None,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_RESERVED_LOG_KEYS = {
|
|
16
|
+
"args",
|
|
17
|
+
"asctime",
|
|
18
|
+
"created",
|
|
19
|
+
"exc_info",
|
|
20
|
+
"exc_text",
|
|
21
|
+
"filename",
|
|
22
|
+
"funcName",
|
|
23
|
+
"levelname",
|
|
24
|
+
"levelno",
|
|
25
|
+
"lineno",
|
|
26
|
+
"message",
|
|
27
|
+
"module",
|
|
28
|
+
"msecs",
|
|
29
|
+
"msg",
|
|
30
|
+
"name",
|
|
31
|
+
"pathname",
|
|
32
|
+
"process",
|
|
33
|
+
"processName",
|
|
34
|
+
"relativeCreated",
|
|
35
|
+
"stack_info",
|
|
36
|
+
"thread",
|
|
37
|
+
"threadName",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_context() -> dict[str, Any]:
|
|
42
|
+
return dict(_LOG_CONTEXT.get() or {})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BoundContext(AbstractContextManager["BoundContext"]):
|
|
46
|
+
def __init__(self, token: Token[dict[str, Any] | None]) -> None:
|
|
47
|
+
self._token = token
|
|
48
|
+
self._closed = False
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> BoundContext:
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
54
|
+
self.close()
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
if not self._closed:
|
|
58
|
+
_LOG_CONTEXT.reset(self._token)
|
|
59
|
+
self._closed = True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bind_context(**context: Any) -> BoundContext:
|
|
63
|
+
current = dict(_LOG_CONTEXT.get() or {})
|
|
64
|
+
current.update({key: value for key, value in context.items() if value is not None})
|
|
65
|
+
return BoundContext(_LOG_CONTEXT.set(current))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def clear_context() -> None:
|
|
69
|
+
_LOG_CONTEXT.set({})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ContextFilter(logging.Filter):
|
|
73
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
74
|
+
merged_context = get_context()
|
|
75
|
+
extra_context = getattr(record, "utilityhub_context", None)
|
|
76
|
+
if isinstance(extra_context, Mapping):
|
|
77
|
+
merged_context.update(extra_context)
|
|
78
|
+
record.utilityhub_context = merged_context
|
|
79
|
+
for key, value in merged_context.items():
|
|
80
|
+
if key.isidentifier() and key not in _RESERVED_LOG_KEYS:
|
|
81
|
+
setattr(record, key, value)
|
|
82
|
+
return True
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _format_timestamp(created: float) -> str:
|
|
10
|
+
dt = datetime.fromtimestamp(created, tz=UTC)
|
|
11
|
+
return dt.isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PlainTextFormatter(logging.Formatter):
|
|
15
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
16
|
+
timestamp = _format_timestamp(record.created)
|
|
17
|
+
message = record.getMessage()
|
|
18
|
+
context = getattr(record, "utilityhub_context", {})
|
|
19
|
+
context_text = ""
|
|
20
|
+
if context:
|
|
21
|
+
pairs = " ".join(f"{key}={value}" for key, value in sorted(context.items()))
|
|
22
|
+
context_text = f" [{pairs}]"
|
|
23
|
+
|
|
24
|
+
parts = [timestamp, record.levelname, record.name, "-", message]
|
|
25
|
+
rendered = " ".join(parts) + context_text
|
|
26
|
+
|
|
27
|
+
if record.exc_info:
|
|
28
|
+
rendered = f"{rendered}\n{self.formatException(record.exc_info)}"
|
|
29
|
+
if record.stack_info:
|
|
30
|
+
rendered = f"{rendered}\n{self.formatStack(record.stack_info)}"
|
|
31
|
+
return rendered
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JsonFormatter(logging.Formatter):
|
|
35
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
36
|
+
payload: dict[str, Any] = {
|
|
37
|
+
"timestamp": _format_timestamp(record.created),
|
|
38
|
+
"level": record.levelname,
|
|
39
|
+
"logger": record.name,
|
|
40
|
+
"message": record.getMessage(),
|
|
41
|
+
}
|
|
42
|
+
context = getattr(record, "utilityhub_context", {})
|
|
43
|
+
if context:
|
|
44
|
+
payload["context"] = dict(sorted(context.items()))
|
|
45
|
+
if record.exc_info:
|
|
46
|
+
payload["exception"] = self.formatException(record.exc_info)
|
|
47
|
+
if record.stack_info:
|
|
48
|
+
payload["stack"] = self.formatStack(record.stack_info)
|
|
49
|
+
return json.dumps(payload, ensure_ascii=False, sort_keys=True)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from utilityhub_logging.types import LogPathConvention
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_logs_path(
|
|
11
|
+
app_name: str,
|
|
12
|
+
logs_path: str | os.PathLike[str] | None = None,
|
|
13
|
+
*,
|
|
14
|
+
default_convention: LogPathConvention | str = LogPathConvention.PLATFORM,
|
|
15
|
+
create: bool = True,
|
|
16
|
+
) -> Path:
|
|
17
|
+
if not app_name.strip():
|
|
18
|
+
msg = "app_name must be a non-empty string"
|
|
19
|
+
raise ValueError(msg)
|
|
20
|
+
|
|
21
|
+
if logs_path is not None:
|
|
22
|
+
path = Path(logs_path).expanduser()
|
|
23
|
+
else:
|
|
24
|
+
convention = LogPathConvention(default_convention)
|
|
25
|
+
path = _resolve_default_logs_path(app_name=app_name, convention=convention)
|
|
26
|
+
|
|
27
|
+
if create:
|
|
28
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
|
|
30
|
+
return path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_default_logs_path(app_name: str, *, convention: LogPathConvention) -> Path:
|
|
34
|
+
if convention is LogPathConvention.CWD:
|
|
35
|
+
return Path.cwd() / "logs" / app_name
|
|
36
|
+
|
|
37
|
+
if convention is LogPathConvention.HOME_HIDDEN:
|
|
38
|
+
return Path.home() / f".{app_name}" / "logs"
|
|
39
|
+
|
|
40
|
+
if os.name == "nt":
|
|
41
|
+
local_app_data = os.getenv("LOCALAPPDATA")
|
|
42
|
+
if local_app_data:
|
|
43
|
+
return Path(local_app_data) / app_name / "Logs"
|
|
44
|
+
return Path.home() / "AppData" / "Local" / app_name / "Logs"
|
|
45
|
+
|
|
46
|
+
if sys.platform == "darwin":
|
|
47
|
+
return Path.home() / "Library" / "Logs" / app_name
|
|
48
|
+
|
|
49
|
+
return Path.home() / ".local" / "state" / app_name / "logs"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from utilityhub_logging.cleanup import cleanup_logging, mark_handler
|
|
7
|
+
from utilityhub_logging.context import ContextFilter, bind_context
|
|
8
|
+
from utilityhub_logging.paths import resolve_logs_path
|
|
9
|
+
from utilityhub_logging.setup import _build_formatter, _normalize_level, _slugify, _utc_now_stamp
|
|
10
|
+
from utilityhub_logging.types import LogFormat, LogPathConvention, ManagedHandlerKind
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def begin_scope_logging(
|
|
14
|
+
scope_type: str,
|
|
15
|
+
scope_id: str,
|
|
16
|
+
*,
|
|
17
|
+
app_name: str,
|
|
18
|
+
level: int | str = "INFO",
|
|
19
|
+
logs_path: str | Path | None = None,
|
|
20
|
+
default_convention: LogPathConvention | str = LogPathConvention.PLATFORM,
|
|
21
|
+
log_format: LogFormat | str = LogFormat.PLAIN,
|
|
22
|
+
logger: logging.Logger | None = None,
|
|
23
|
+
propagate: bool = False,
|
|
24
|
+
) -> tuple[logging.Logger, Path]:
|
|
25
|
+
if not scope_type.strip():
|
|
26
|
+
msg = "scope_type must be a non-empty string"
|
|
27
|
+
raise ValueError(msg)
|
|
28
|
+
if not scope_id.strip():
|
|
29
|
+
msg = "scope_id must be a non-empty string"
|
|
30
|
+
raise ValueError(msg)
|
|
31
|
+
|
|
32
|
+
target_logger = logger or logging.getLogger(f"{app_name}.{scope_type}.{scope_id}")
|
|
33
|
+
target_logger.setLevel(_normalize_level(level))
|
|
34
|
+
target_logger.propagate = propagate
|
|
35
|
+
|
|
36
|
+
cleanup_logging(target_logger, kind=ManagedHandlerKind.SCOPE)
|
|
37
|
+
|
|
38
|
+
root_logs_path = resolve_logs_path(
|
|
39
|
+
app_name=app_name,
|
|
40
|
+
logs_path=logs_path,
|
|
41
|
+
default_convention=default_convention,
|
|
42
|
+
)
|
|
43
|
+
scope_logs_path = root_logs_path / "scopes" / _slugify(scope_type)
|
|
44
|
+
scope_logs_path.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
file_path = scope_logs_path / f"{_slugify(scope_id)}-{_utc_now_stamp()}.log"
|
|
46
|
+
|
|
47
|
+
handler = logging.FileHandler(file_path, encoding="utf-8")
|
|
48
|
+
handler.setLevel(_normalize_level(level))
|
|
49
|
+
handler.setFormatter(_build_formatter(log_format))
|
|
50
|
+
handler.addFilter(ContextFilter())
|
|
51
|
+
target_logger.addHandler(mark_handler(handler, kind=ManagedHandlerKind.SCOPE, file_path=file_path))
|
|
52
|
+
|
|
53
|
+
bind_context(scope_type=scope_type, scope_id=scope_id)
|
|
54
|
+
return target_logger, file_path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def end_scope_logging(logger: logging.Logger | None = None) -> None:
|
|
58
|
+
cleanup_logging(logger, kind=ManagedHandlerKind.SCOPE)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from utilityhub_logging.cleanup import cleanup_logging, mark_handler
|
|
9
|
+
from utilityhub_logging.context import ContextFilter, bind_context
|
|
10
|
+
from utilityhub_logging.formatters import JsonFormatter, PlainTextFormatter
|
|
11
|
+
from utilityhub_logging.paths import resolve_logs_path
|
|
12
|
+
from utilityhub_logging.types import LogFormat, LogPathConvention, ManagedHandlerKind
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def configure_app_logging(
|
|
16
|
+
app_name: str,
|
|
17
|
+
*,
|
|
18
|
+
level: int | str = "INFO",
|
|
19
|
+
logs_path: str | Path | None = None,
|
|
20
|
+
default_convention: LogPathConvention | str = LogPathConvention.PLATFORM,
|
|
21
|
+
console: bool = True,
|
|
22
|
+
log_format: LogFormat | str = LogFormat.PLAIN,
|
|
23
|
+
logger: logging.Logger | None = None,
|
|
24
|
+
propagate: bool = False,
|
|
25
|
+
) -> Path:
|
|
26
|
+
target_logger = logging.getLogger() if logger is None else logger
|
|
27
|
+
target_logger.setLevel(_normalize_level(level))
|
|
28
|
+
target_logger.propagate = propagate
|
|
29
|
+
|
|
30
|
+
cleanup_logging(target_logger, kind=ManagedHandlerKind.APP)
|
|
31
|
+
|
|
32
|
+
resolved_logs_path = resolve_logs_path(
|
|
33
|
+
app_name=app_name,
|
|
34
|
+
logs_path=logs_path,
|
|
35
|
+
default_convention=default_convention,
|
|
36
|
+
)
|
|
37
|
+
session_id = uuid.uuid4().hex[:8]
|
|
38
|
+
timestamp = _utc_now_stamp()
|
|
39
|
+
file_path = resolved_logs_path / f"{_slugify(app_name)}-{timestamp}-{session_id}.log"
|
|
40
|
+
|
|
41
|
+
formatter = _build_formatter(log_format)
|
|
42
|
+
file_handler = logging.FileHandler(file_path, encoding="utf-8")
|
|
43
|
+
file_handler.setLevel(_normalize_level(level))
|
|
44
|
+
file_handler.setFormatter(formatter)
|
|
45
|
+
file_handler.addFilter(ContextFilter())
|
|
46
|
+
target_logger.addHandler(mark_handler(file_handler, kind=ManagedHandlerKind.APP, file_path=file_path))
|
|
47
|
+
|
|
48
|
+
if console:
|
|
49
|
+
console_handler = logging.StreamHandler()
|
|
50
|
+
console_handler.setLevel(_normalize_level(level))
|
|
51
|
+
console_handler.setFormatter(formatter)
|
|
52
|
+
console_handler.addFilter(ContextFilter())
|
|
53
|
+
target_logger.addHandler(mark_handler(console_handler, kind=ManagedHandlerKind.APP))
|
|
54
|
+
|
|
55
|
+
bind_context(app_name=app_name, session_id=session_id)
|
|
56
|
+
return file_path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_formatter(log_format: LogFormat | str) -> logging.Formatter:
|
|
60
|
+
selected = LogFormat(log_format)
|
|
61
|
+
if selected is LogFormat.JSON:
|
|
62
|
+
return JsonFormatter()
|
|
63
|
+
return PlainTextFormatter()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _normalize_level(level: int | str) -> int:
|
|
67
|
+
if isinstance(level, int):
|
|
68
|
+
return level
|
|
69
|
+
normalized = logging.getLevelName(level.upper())
|
|
70
|
+
if isinstance(normalized, int):
|
|
71
|
+
return normalized
|
|
72
|
+
msg = f"Unsupported log level: {level!r}"
|
|
73
|
+
raise ValueError(msg)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _utc_now_stamp() -> str:
|
|
77
|
+
return datetime.now(tz=UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _slugify(value: str) -> str:
|
|
81
|
+
chars = [char.lower() if char.isalnum() else "-" for char in value.strip()]
|
|
82
|
+
slug = "".join(chars).strip("-")
|
|
83
|
+
while "--" in slug:
|
|
84
|
+
slug = slug.replace("--", "-")
|
|
85
|
+
return slug or "log"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LogFormat(StrEnum):
|
|
9
|
+
PLAIN = "plain"
|
|
10
|
+
JSON = "json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogPathConvention(StrEnum):
|
|
14
|
+
PLATFORM = "platform"
|
|
15
|
+
HOME_HIDDEN = "home_hidden"
|
|
16
|
+
CWD = "cwd"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ManagedHandlerKind(StrEnum):
|
|
20
|
+
APP = "app"
|
|
21
|
+
SCOPE = "scope"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ManagedHandlerRecord:
|
|
26
|
+
logger_name: str
|
|
27
|
+
handler_name: str
|
|
28
|
+
kind: ManagedHandlerKind
|
|
29
|
+
file_path: Path | None = None
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: utilityhub-logging
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Project-agnostic logging orchestration utilities built on Python's stdlib logging package
|
|
5
|
+
Author: Rajesh Das
|
|
6
|
+
Author-email: Rajesh Das <rajesh@hyperoot.dev>
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# utilityhub-logging
|
|
11
|
+
|
|
12
|
+
`utilityhub-logging` is a small, project-agnostic package that extends Python's
|
|
13
|
+
standard library `logging` module with deterministic setup and cleanup helpers.
|
|
14
|
+
|
|
15
|
+
It focuses on the repetitive parts teams tend to rebuild:
|
|
16
|
+
|
|
17
|
+
- resolving a safe logs directory
|
|
18
|
+
- configuring one log file per application run
|
|
19
|
+
- opening a scoped log for an operation
|
|
20
|
+
- switching between plain text and JSON output
|
|
21
|
+
- attaching async-safe contextual metadata
|
|
22
|
+
- preventing duplicate handlers and cleaning them up reliably
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add utilityhub-logging
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import logging
|
|
34
|
+
|
|
35
|
+
from utilityhub_logging import (
|
|
36
|
+
LogFormat,
|
|
37
|
+
begin_scope_logging,
|
|
38
|
+
bind_context,
|
|
39
|
+
cleanup_logging,
|
|
40
|
+
configure_app_logging,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
log_file = configure_app_logging(
|
|
44
|
+
app_name="demo-app",
|
|
45
|
+
level="INFO",
|
|
46
|
+
console=True,
|
|
47
|
+
log_format=LogFormat.PLAIN,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("demo")
|
|
51
|
+
with bind_context(environment="dev", subsystem="worker"):
|
|
52
|
+
logger.info("Application started")
|
|
53
|
+
|
|
54
|
+
scope_logger, scope_file = begin_scope_logging(
|
|
55
|
+
app_name="demo-app",
|
|
56
|
+
scope_type="job",
|
|
57
|
+
scope_id="job-42",
|
|
58
|
+
log_format=LogFormat.JSON,
|
|
59
|
+
)
|
|
60
|
+
scope_logger.info("Running scoped operation")
|
|
61
|
+
|
|
62
|
+
cleanup_logging()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Public API
|
|
66
|
+
|
|
67
|
+
- `resolve_logs_path(...)`
|
|
68
|
+
- `configure_app_logging(...)`
|
|
69
|
+
- `begin_scope_logging(...)`
|
|
70
|
+
- `end_scope_logging(...)`
|
|
71
|
+
- `bind_context(...)`
|
|
72
|
+
- `cleanup_logging(...)`
|
|
73
|
+
|
|
74
|
+
## Defaults
|
|
75
|
+
|
|
76
|
+
- UTF-8 file output
|
|
77
|
+
- timestamped log files in UTC
|
|
78
|
+
- platform-aware default log directories
|
|
79
|
+
- `~/.local/state/<app>/logs` on Linux and similar Unix systems
|
|
80
|
+
- `~/Library/Logs/<app>` on macOS
|
|
81
|
+
- `%LOCALAPPDATA%\<app>\Logs` on Windows
|
|
82
|
+
- no current-working-directory logging unless explicitly requested
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
utilityhub_logging/__init__.py,sha256=MyvBmIEtZrmwHT-7vmFeQLOtjIDABoPYGGMd0HliUJk,898
|
|
2
|
+
utilityhub_logging/cleanup.py,sha256=aIbAYMxKVbSSLBPo3eGRO8LnCn6v3OUCevdpPa2srNk,1756
|
|
3
|
+
utilityhub_logging/context.py,sha256=kWfZBLnpQ3dDIpHifztjhS8H8I1u361ZJ-eg1P61L8o,2100
|
|
4
|
+
utilityhub_logging/formatters.py,sha256=y6oBr9h2UGGOwGrYOaNrc-hO8uwWPJuoqI9sWWWqr6Y,1816
|
|
5
|
+
utilityhub_logging/paths.py,sha256=KNgyX1Bp1ddRudxqVD64XQcddqkOHNFbR81FozHRi_E,1453
|
|
6
|
+
utilityhub_logging/scopes.py,sha256=-kNExpQL3Znu5xAjwFKD2TLMHY24e21myCuMDOmAVb8,2215
|
|
7
|
+
utilityhub_logging/setup.py,sha256=8rGePfB9KG92zGamYA6KPJ2DPc_8nxEbAvCeeVe3WG0,2965
|
|
8
|
+
utilityhub_logging/types.py,sha256=jWwmSXwXHNs8B6Ko4CXLLaUsDrNt-ndTioA0wCrjnbg,541
|
|
9
|
+
utilityhub_logging-0.1.0.dist-info/WHEEL,sha256=0ksFqNncQTsCUyOQQNfyYN3tci86LvOLl92eMRFTR70,81
|
|
10
|
+
utilityhub_logging-0.1.0.dist-info/entry_points.txt,sha256=wbkXZVmVvR3CgxbiNMB1mYFHzEvmk_MwNF3UB9sJWC4,64
|
|
11
|
+
utilityhub_logging-0.1.0.dist-info/METADATA,sha256=gFQ5XTE5PWxuOoLRvAUjVkrxaBDg07-SJNMTgaiuUHk,2015
|
|
12
|
+
utilityhub_logging-0.1.0.dist-info/RECORD,,
|