libsh 0.2.0__tar.gz → 0.3.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.
- {libsh-0.2.0 → libsh-0.3.0}/PKG-INFO +2 -2
- {libsh-0.2.0 → libsh-0.3.0}/pyproject.toml +7 -1
- {libsh-0.2.0 → libsh-0.3.0}/src/libsh/__init__.py +1 -1
- libsh-0.3.0/src/libsh/logging/__init__.py +5 -0
- libsh-0.3.0/src/libsh/logging/config.py +190 -0
- libsh-0.3.0/src/libsh/logging/processors.py +191 -0
- libsh-0.3.0/src/libsh/logging/render.py +158 -0
- libsh-0.3.0/src/libsh/logs.py +5 -0
- libsh-0.3.0/src/libsh/proc/__init__.py +6 -0
- libsh-0.3.0/src/libsh/proc/float.py +5 -0
- libsh-0.3.0/src/libsh/proc/logger_filter.py +5 -0
- libsh-0.2.0/src/libsh/logs.py +0 -430
- libsh-0.2.0/src/libsh/proc/__init__.py +0 -7
- libsh-0.2.0/src/libsh/proc/float.py +0 -76
- libsh-0.2.0/src/libsh/proc/logger_filter.py +0 -47
- {libsh-0.2.0 → libsh-0.3.0}/src/libsh/format.py +0 -0
- {libsh-0.2.0 → libsh-0.3.0}/src/libsh/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: libsh
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Scott Hyndman's reusable common package for Python projects. Provides opinionated structured logging with custom rendering (24-bit colors, relative timestamps, compact levels) and type-safe value formatting utilities (time units, ranges, pretty-printing).
|
|
5
5
|
Author: shyndman
|
|
6
6
|
Author-email: shyndman <scotty.hyndman@gmail.com>
|
|
@@ -9,5 +9,5 @@ Requires-Dist: pydantic>=2
|
|
|
9
9
|
Requires-Dist: rich>=14
|
|
10
10
|
Requires-Dist: structlog>=25
|
|
11
11
|
Requires-Python: >=3.12
|
|
12
|
-
Project-URL: Bug Tracker, https://github.com/shyndman/libsh-python/issues
|
|
13
12
|
Project-URL: repository, https://github.com/shyndman/libsh-python
|
|
13
|
+
Project-URL: Bug Tracker, https://github.com/shyndman/libsh-python/issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "libsh"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Scott Hyndman's reusable common package for Python projects. Provides opinionated structured logging with custom rendering (24-bit colors, relative timestamps, compact levels) and type-safe value formatting utilities (time units, ranges, pretty-printing)."
|
|
5
5
|
authors = [{name = "shyndman", email = "scotty.hyndman@gmail.com"}]
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -48,3 +48,9 @@ known-first-party = ["libsh"]
|
|
|
48
48
|
quote-style = "double"
|
|
49
49
|
indent-style = "space"
|
|
50
50
|
line-ending = "auto"
|
|
51
|
+
|
|
52
|
+
[dependency-groups]
|
|
53
|
+
dev = [
|
|
54
|
+
"basedpyright>=1.39.6",
|
|
55
|
+
"pytest>=9.0.3",
|
|
56
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Final, TextIO, cast
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
from structlog.typing import Processor
|
|
8
|
+
|
|
9
|
+
from .processors import (
|
|
10
|
+
FloatPrecisionProcessor,
|
|
11
|
+
LoggerFilterProcessor,
|
|
12
|
+
compact_level_processor,
|
|
13
|
+
debug_event_colorer,
|
|
14
|
+
relative_time_processor,
|
|
15
|
+
relative_time_processor_plain,
|
|
16
|
+
)
|
|
17
|
+
from .render import build_console_renderer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _ManagedStreamHandler(logging.StreamHandler[TextIO]):
|
|
21
|
+
"""Root console handler installed by libsh; identified by type so repeated
|
|
22
|
+
setup_logging calls replace prior libsh handlers without touching foreign ones."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _ManagedFileHandler(logging.FileHandler):
|
|
26
|
+
"""Root JSON file handler installed by libsh; identified by type like its
|
|
27
|
+
stream sibling so setup_logging stays idempotent across calls."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_MANAGED_ROOT_HANDLER_TYPES: Final = (_ManagedStreamHandler, _ManagedFileHandler)
|
|
31
|
+
_JSON_ENV_TRUTHY: Final = frozenset({"true", "1", "yes", "on"})
|
|
32
|
+
_PROPAGATING_LIBRARY_LOGGERS: Final = ("websockets",)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def setup_logging(
|
|
36
|
+
level: str = "INFO",
|
|
37
|
+
json_output: bool = False,
|
|
38
|
+
correlation_id: str | None = None,
|
|
39
|
+
filter_to_logger: str | None = None,
|
|
40
|
+
log_file: Path | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Configure structured logging for the application."""
|
|
43
|
+
processors: list[Processor] = []
|
|
44
|
+
_ = structlog.contextvars.unbind_contextvars("correlation_id")
|
|
45
|
+
if correlation_id:
|
|
46
|
+
processors.append(structlog.contextvars.merge_contextvars)
|
|
47
|
+
_ = structlog.contextvars.bind_contextvars(correlation_id=correlation_id)
|
|
48
|
+
|
|
49
|
+
processors.append(structlog.stdlib.filter_by_level)
|
|
50
|
+
if filter_to_logger:
|
|
51
|
+
processors.append(LoggerFilterProcessor(filter_to_logger))
|
|
52
|
+
|
|
53
|
+
processors.extend(
|
|
54
|
+
[
|
|
55
|
+
structlog.stdlib.add_logger_name,
|
|
56
|
+
structlog.stdlib.add_log_level,
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if json_output:
|
|
61
|
+
processors.extend(
|
|
62
|
+
[
|
|
63
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
64
|
+
structlog.stdlib.ExtraAdder(),
|
|
65
|
+
FloatPrecisionProcessor(digits=3),
|
|
66
|
+
relative_time_processor_plain,
|
|
67
|
+
structlog.processors.StackInfoRenderer(),
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
processors.extend(
|
|
72
|
+
[
|
|
73
|
+
debug_event_colorer,
|
|
74
|
+
compact_level_processor,
|
|
75
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
76
|
+
structlog.stdlib.ExtraAdder(),
|
|
77
|
+
FloatPrecisionProcessor(digits=3),
|
|
78
|
+
relative_time_processor,
|
|
79
|
+
structlog.processors.StackInfoRenderer(),
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
log_renderer: Processor
|
|
84
|
+
if json_output:
|
|
85
|
+
log_renderer = structlog.processors.JSONRenderer()
|
|
86
|
+
else:
|
|
87
|
+
log_renderer = build_console_renderer()
|
|
88
|
+
|
|
89
|
+
formatter_pre_chain: list[Processor] = [
|
|
90
|
+
proc
|
|
91
|
+
for proc in processors
|
|
92
|
+
if proc is not structlog.stdlib.filter_by_level and not isinstance(proc, LoggerFilterProcessor)
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
structlog.configure(
|
|
96
|
+
processors=processors + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
|
|
97
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
98
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
99
|
+
cache_logger_on_first_use=True,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
formatter = structlog.stdlib.ProcessorFormatter(
|
|
103
|
+
foreign_pre_chain=formatter_pre_chain,
|
|
104
|
+
processors=[
|
|
105
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
106
|
+
log_renderer,
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# A file sink always renders JSON regardless of console format, so persisted
|
|
111
|
+
# logs stay machine-parseable. It shares the same pre-chain, so structured
|
|
112
|
+
# fields (including any ANSI baked into the console pipeline) are serialized
|
|
113
|
+
# verbatim and JSON-escaped.
|
|
114
|
+
file_formatter: logging.Formatter | None = None
|
|
115
|
+
if log_file is not None:
|
|
116
|
+
file_formatter = structlog.stdlib.ProcessorFormatter(
|
|
117
|
+
foreign_pre_chain=formatter_pre_chain,
|
|
118
|
+
processors=[
|
|
119
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
120
|
+
structlog.processors.JSONRenderer(),
|
|
121
|
+
],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
root_logger = logging.getLogger()
|
|
125
|
+
_replace_managed_root_handler(
|
|
126
|
+
root_logger, formatter, level, filter_to_logger, log_file, file_formatter
|
|
127
|
+
)
|
|
128
|
+
_configure_library_loggers()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_logger(
|
|
132
|
+
name: str | None = None, *args: object, **initial_values: object
|
|
133
|
+
) -> structlog.stdlib.BoundLogger:
|
|
134
|
+
"""Get a structured logger instance."""
|
|
135
|
+
if name is None:
|
|
136
|
+
return cast(structlog.stdlib.BoundLogger, structlog.get_logger(*args, **initial_values))
|
|
137
|
+
|
|
138
|
+
return cast(
|
|
139
|
+
structlog.stdlib.BoundLogger,
|
|
140
|
+
structlog.get_logger(name, *args, **initial_values),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def setup_logging_from_env() -> None:
|
|
145
|
+
"""Setup logging using environment variables."""
|
|
146
|
+
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
147
|
+
json_output = os.getenv("JSON_LOGS", "false").lower() in _JSON_ENV_TRUTHY
|
|
148
|
+
correlation_id = os.getenv("CORRELATION_ID")
|
|
149
|
+
|
|
150
|
+
setup_logging(level=log_level, json_output=json_output, correlation_id=correlation_id)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _replace_managed_root_handler(
|
|
154
|
+
root_logger: logging.Logger,
|
|
155
|
+
formatter: logging.Formatter,
|
|
156
|
+
level: str,
|
|
157
|
+
filter_to_logger: str | None,
|
|
158
|
+
log_file: Path | None,
|
|
159
|
+
file_formatter: logging.Formatter | None,
|
|
160
|
+
) -> None:
|
|
161
|
+
managed_handlers = [
|
|
162
|
+
handler for handler in root_logger.handlers if isinstance(handler, _MANAGED_ROOT_HANDLER_TYPES)
|
|
163
|
+
]
|
|
164
|
+
for handler in managed_handlers:
|
|
165
|
+
root_logger.removeHandler(handler)
|
|
166
|
+
handler.close()
|
|
167
|
+
|
|
168
|
+
stream_handler = _ManagedStreamHandler()
|
|
169
|
+
stream_handler.setFormatter(formatter)
|
|
170
|
+
if filter_to_logger:
|
|
171
|
+
stream_handler.addFilter(logging.Filter(filter_to_logger))
|
|
172
|
+
root_logger.addHandler(stream_handler)
|
|
173
|
+
|
|
174
|
+
if log_file is not None and file_formatter is not None:
|
|
175
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
file_handler = _ManagedFileHandler(log_file, encoding="utf-8")
|
|
177
|
+
file_handler.setFormatter(file_formatter)
|
|
178
|
+
if filter_to_logger:
|
|
179
|
+
file_handler.addFilter(logging.Filter(filter_to_logger))
|
|
180
|
+
root_logger.addHandler(file_handler)
|
|
181
|
+
|
|
182
|
+
root_logger.setLevel(level)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _configure_library_loggers() -> None:
|
|
186
|
+
for liblog_name in _PROPAGATING_LIBRARY_LOGGERS:
|
|
187
|
+
liblog = logging.getLogger(liblog_name)
|
|
188
|
+
liblog.handlers.clear()
|
|
189
|
+
liblog.setLevel(logging.WARNING)
|
|
190
|
+
liblog.propagate = True
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Final, Protocol, runtime_checkable
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import structlog
|
|
6
|
+
from structlog import DropEvent
|
|
7
|
+
from structlog.typing import EventDict, WrappedLogger
|
|
8
|
+
|
|
9
|
+
from .render import hex_to_ansi_fg
|
|
10
|
+
|
|
11
|
+
_PROGRAM_START_TIME = time.time()
|
|
12
|
+
_LEVEL_STYLES: Final = {
|
|
13
|
+
"debug": (0x908CAA, 0x827E99, "dbug"),
|
|
14
|
+
"info": (0x9CCFD8, 0x8CBAC2, "info"),
|
|
15
|
+
"warning": (0xF6C177, 0xDDAE6B, "warn"),
|
|
16
|
+
"error": (0xEB6F92, 0xD46483, "eror"),
|
|
17
|
+
"exception": (0xEB6F92, 0xD46483, "exc!"),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"FloatPrecisionProcessor",
|
|
22
|
+
"LoggerFilterProcessor",
|
|
23
|
+
"compact_level_processor",
|
|
24
|
+
"debug_event_colorer",
|
|
25
|
+
"relative_time_processor",
|
|
26
|
+
"relative_time_processor_plain",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FloatPrecisionProcessor:
|
|
31
|
+
digits: int
|
|
32
|
+
np_array_to_list: bool
|
|
33
|
+
only_fields: set[str]
|
|
34
|
+
not_fields: set[str]
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
digits: int = 3,
|
|
39
|
+
only_fields: set[str] | None = None,
|
|
40
|
+
not_fields: set[str] | None = None,
|
|
41
|
+
np_array_to_list: bool = True,
|
|
42
|
+
):
|
|
43
|
+
self.digits = digits
|
|
44
|
+
self.np_array_to_list = np_array_to_list
|
|
45
|
+
self.only_fields = set[str]() if only_fields is None else only_fields
|
|
46
|
+
self.not_fields = set[str]() if not_fields is None else not_fields
|
|
47
|
+
|
|
48
|
+
def _round(self, value: Any) -> Any:
|
|
49
|
+
if isinstance(value, float):
|
|
50
|
+
return round(value, self.digits)
|
|
51
|
+
if self.np_array_to_list and isinstance(value, np.ndarray):
|
|
52
|
+
return self._round(list(value))
|
|
53
|
+
if isinstance(value, list):
|
|
54
|
+
for idx, item in enumerate(value):
|
|
55
|
+
value[idx] = self._round(item)
|
|
56
|
+
return value
|
|
57
|
+
if isinstance(value, dict):
|
|
58
|
+
for key, nested_value in value.items():
|
|
59
|
+
value[key] = self._round(nested_value)
|
|
60
|
+
return value
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
def __call__(self, _logger: WrappedLogger, _method_name: str, event_dict: EventDict) -> EventDict:
|
|
64
|
+
for key, value in event_dict.items():
|
|
65
|
+
if self.only_fields and key not in self.only_fields:
|
|
66
|
+
continue
|
|
67
|
+
if self.not_fields and key in self.not_fields:
|
|
68
|
+
continue
|
|
69
|
+
if isinstance(value, bool):
|
|
70
|
+
continue
|
|
71
|
+
event_dict[key] = self._round(value)
|
|
72
|
+
return event_dict
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@runtime_checkable
|
|
76
|
+
class NamedLogger(Protocol):
|
|
77
|
+
name: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _logger_name_from_event(logger: object, event_dict: EventDict) -> str:
|
|
81
|
+
if isinstance(logger, NamedLogger):
|
|
82
|
+
return logger.name
|
|
83
|
+
|
|
84
|
+
record = event_dict.get("_record")
|
|
85
|
+
if isinstance(record, NamedLogger):
|
|
86
|
+
return record.name
|
|
87
|
+
|
|
88
|
+
event_logger_name = event_dict.get("logger_name") or event_dict.get("logger")
|
|
89
|
+
return event_logger_name if isinstance(event_logger_name, str) else ""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class LoggerFilterProcessor:
|
|
93
|
+
logger_name: str
|
|
94
|
+
_child_logger_prefix: str
|
|
95
|
+
|
|
96
|
+
def __init__(self, logger_name: str):
|
|
97
|
+
self.logger_name = logger_name
|
|
98
|
+
self._child_logger_prefix = f"{logger_name}."
|
|
99
|
+
|
|
100
|
+
def __call__(self, logger: object, _method_name: str, event_dict: EventDict) -> EventDict:
|
|
101
|
+
logger_name = _logger_name_from_event(logger, event_dict)
|
|
102
|
+
if logger_name == self.logger_name or logger_name.startswith(self._child_logger_prefix):
|
|
103
|
+
return event_dict
|
|
104
|
+
raise DropEvent
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def relative_time_processor(
|
|
108
|
+
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
109
|
+
) -> EventDict:
|
|
110
|
+
"""Add relative timestamp since program start with hours:minutes:seconds.milliseconds format."""
|
|
111
|
+
hours, minutes, seconds = _relative_time_parts()
|
|
112
|
+
|
|
113
|
+
gray = "\x1b[2m"
|
|
114
|
+
dark_gray = "\x1b[90m"
|
|
115
|
+
reset = "\x1b[0m"
|
|
116
|
+
|
|
117
|
+
separator = f"{gray}:{reset}"
|
|
118
|
+
hours_str = f"{gray}{hours:02d}{reset}{separator}" if hours != 0 else ""
|
|
119
|
+
minutes_str = f"{gray}{minutes:02d}{reset}{separator}" if minutes != 0 and hours != 0 else ""
|
|
120
|
+
seconds_str = f"{gray}{seconds:06.3f}{reset}"
|
|
121
|
+
|
|
122
|
+
event_dict["timestamp"] = f"{dark_gray}+{reset}{hours_str}{minutes_str}{seconds_str}"
|
|
123
|
+
return event_dict
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def relative_time_processor_plain(
|
|
127
|
+
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
128
|
+
) -> EventDict:
|
|
129
|
+
"""Add relative timestamp without ANSI styling for machine-consumable outputs."""
|
|
130
|
+
hours, minutes, seconds = _relative_time_parts()
|
|
131
|
+
|
|
132
|
+
separator = ":"
|
|
133
|
+
hours_str = f"{hours:02d}{separator}" if hours != 0 else ""
|
|
134
|
+
minutes_str = f"{minutes:02d}{separator}" if minutes != 0 and hours != 0 else ""
|
|
135
|
+
seconds_str = f"{seconds:06.3f}"
|
|
136
|
+
|
|
137
|
+
event_dict["timestamp"] = f"+{hours_str}{minutes_str}{seconds_str}"
|
|
138
|
+
return event_dict
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def compact_level_processor(
|
|
142
|
+
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
143
|
+
) -> EventDict:
|
|
144
|
+
"""Convert log levels to compact 4-character format with 24-bit colors and darker brackets."""
|
|
145
|
+
reset = "\x1b[0m"
|
|
146
|
+
|
|
147
|
+
original_level = event_dict.get("level")
|
|
148
|
+
if not isinstance(original_level, str):
|
|
149
|
+
return event_dict
|
|
150
|
+
|
|
151
|
+
if original_level == "critical":
|
|
152
|
+
event_dict["level"] = (
|
|
153
|
+
f"{hex_to_ansi_fg(0xD46483)}[{reset}"
|
|
154
|
+
f"\x1b[48;2;235;111;146;38;2;33;32;46mcrit{reset}"
|
|
155
|
+
f"{hex_to_ansi_fg(0xD46483)}]{reset}"
|
|
156
|
+
)
|
|
157
|
+
return event_dict
|
|
158
|
+
|
|
159
|
+
style = _LEVEL_STYLES.get(original_level)
|
|
160
|
+
if style is None:
|
|
161
|
+
return event_dict
|
|
162
|
+
|
|
163
|
+
text_color, bracket_color, text = style
|
|
164
|
+
event_dict["level"] = (
|
|
165
|
+
f"{hex_to_ansi_fg(bracket_color)}[{reset}"
|
|
166
|
+
f"{hex_to_ansi_fg(text_color)}{text}{reset}"
|
|
167
|
+
f"{hex_to_ansi_fg(bracket_color)}]{reset}"
|
|
168
|
+
)
|
|
169
|
+
return event_dict
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def debug_event_colorer(
|
|
173
|
+
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
174
|
+
) -> EventDict:
|
|
175
|
+
"""Color the event text purple for debug level messages."""
|
|
176
|
+
level = event_dict.get("level")
|
|
177
|
+
if isinstance(level, str) and "debug" in level.lower() and "event" in event_dict:
|
|
178
|
+
debug_color = hex_to_ansi_fg(0x908CAA)
|
|
179
|
+
event = event_dict.get("event")
|
|
180
|
+
event_dict["event"] = f"{debug_color}{event}\x1b[0m"
|
|
181
|
+
|
|
182
|
+
return event_dict
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _relative_time_parts() -> tuple[int, int, float]:
|
|
186
|
+
"""Return elapsed (hours, minutes, seconds) since program start."""
|
|
187
|
+
elapsed = time.time() - _PROGRAM_START_TIME
|
|
188
|
+
hours = int(elapsed // 3600)
|
|
189
|
+
minutes = int((elapsed % 3600) // 60)
|
|
190
|
+
seconds = elapsed % 60
|
|
191
|
+
return hours, minutes, seconds
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from io import StringIO
|
|
5
|
+
|
|
6
|
+
from structlog.dev import BRIGHT, DIM, RESET_ALL, Column, ConsoleRenderer, KeyValueColumnFormatter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _write(sio: StringIO, value: str) -> None:
|
|
10
|
+
_ = sio.write(value)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _pad_value(value: str, width: int) -> str:
|
|
14
|
+
if width <= 0:
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
return value.ljust(width)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def hex_to_ansi_fg(hex_color: int) -> str:
|
|
21
|
+
"""Convert hex color (e.g., 0xad8a89) to ANSI 24-bit foreground escape code."""
|
|
22
|
+
r = (hex_color >> 16) & 0xFF
|
|
23
|
+
g = (hex_color >> 8) & 0xFF
|
|
24
|
+
b = hex_color & 0xFF
|
|
25
|
+
return f"\x1b[38;2;{r};{g};{b}m"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RegexValueColumnFormatter:
|
|
30
|
+
"""
|
|
31
|
+
Format a key-value pair with regex-based value styling.
|
|
32
|
+
|
|
33
|
+
Like KeyValueColumnFormatter, but allows mapping values to styles based on
|
|
34
|
+
regular expression patterns.
|
|
35
|
+
|
|
36
|
+
:param key_style: The style to apply to the key. If None, the key is omitted.
|
|
37
|
+
:param value_style_map: A list of (regex_pattern, style) tuples. The first
|
|
38
|
+
pattern that matches the value will determine its style.
|
|
39
|
+
:param default_value_style: The style to use if no regex patterns match.
|
|
40
|
+
:param reset_style: The style to apply whenever a style is no longer needed.
|
|
41
|
+
:param value_repr: A callable that returns the string representation of the value.
|
|
42
|
+
:param width: The width to pad the value to. If 0, no padding is done.
|
|
43
|
+
:param prefix: A string to prepend to the formatted key-value pair. May contain
|
|
44
|
+
styles.
|
|
45
|
+
:param postfix: A string to append to the formatted key-value pair. May contain
|
|
46
|
+
styles.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
key_style: str | None
|
|
50
|
+
value_style_map: list[tuple[str, str]]
|
|
51
|
+
default_value_style: str
|
|
52
|
+
reset_style: str
|
|
53
|
+
value_repr: Callable[[object], str]
|
|
54
|
+
width: int = 0
|
|
55
|
+
prefix: str = ""
|
|
56
|
+
postfix: str = ""
|
|
57
|
+
_compiled_patterns: list[tuple[re.Pattern[str], str]] = field(init=False, repr=False)
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
"""Compile regex patterns for efficiency."""
|
|
61
|
+
self._compiled_patterns = [
|
|
62
|
+
(re.compile(pattern), style) for pattern, style in self.value_style_map
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def __call__(self, key: str, value: object) -> str:
|
|
66
|
+
sio = StringIO()
|
|
67
|
+
|
|
68
|
+
if self.prefix:
|
|
69
|
+
_write(sio, self.prefix)
|
|
70
|
+
_write(sio, self.reset_style)
|
|
71
|
+
|
|
72
|
+
if self.key_style is not None:
|
|
73
|
+
_write(sio, self.key_style)
|
|
74
|
+
_write(sio, key)
|
|
75
|
+
_write(sio, self.reset_style)
|
|
76
|
+
_write(sio, "=")
|
|
77
|
+
|
|
78
|
+
value_str = self.value_repr(value)
|
|
79
|
+
value_style = self.default_value_style
|
|
80
|
+
|
|
81
|
+
for pattern, style in self._compiled_patterns:
|
|
82
|
+
if pattern.search(value_str):
|
|
83
|
+
value_style = style
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
_write(sio, value_style)
|
|
87
|
+
_write(sio, _pad_value(value_str, self.width))
|
|
88
|
+
_write(sio, self.reset_style)
|
|
89
|
+
|
|
90
|
+
if self.postfix:
|
|
91
|
+
_write(sio, self.postfix)
|
|
92
|
+
_write(sio, self.reset_style)
|
|
93
|
+
|
|
94
|
+
return sio.getvalue()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_console_renderer() -> ConsoleRenderer:
|
|
98
|
+
event_key = "event"
|
|
99
|
+
timestamp_key = "timestamp"
|
|
100
|
+
logger_name_formatter = KeyValueColumnFormatter(
|
|
101
|
+
key_style=None,
|
|
102
|
+
value_style=hex_to_ansi_fg(0x7D6B95),
|
|
103
|
+
reset_style=RESET_ALL,
|
|
104
|
+
value_repr=str,
|
|
105
|
+
prefix="[",
|
|
106
|
+
postfix="]",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return ConsoleRenderer(
|
|
110
|
+
colors=True,
|
|
111
|
+
columns=[
|
|
112
|
+
Column(
|
|
113
|
+
"",
|
|
114
|
+
RegexValueColumnFormatter(
|
|
115
|
+
key_style=hex_to_ansi_fg(0x6E6A86),
|
|
116
|
+
value_style_map=[
|
|
117
|
+
(r"^(True|False)$", hex_to_ansi_fg(0x6E6A86)),
|
|
118
|
+
(r"^-?\d+$", hex_to_ansi_fg(0xF6C177)),
|
|
119
|
+
(r"^-?\d*\.\d+$", hex_to_ansi_fg(0xF6C177)),
|
|
120
|
+
(r"^-?\d*\.?\d+(?:h|m|s|ms|us|µs)$", hex_to_ansi_fg(0x9CCFD8)),
|
|
121
|
+
],
|
|
122
|
+
default_value_style="",
|
|
123
|
+
reset_style=RESET_ALL,
|
|
124
|
+
value_repr=str,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
Column(
|
|
128
|
+
timestamp_key,
|
|
129
|
+
KeyValueColumnFormatter(
|
|
130
|
+
key_style=None,
|
|
131
|
+
value_style=DIM,
|
|
132
|
+
reset_style=RESET_ALL,
|
|
133
|
+
value_repr=str,
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
Column(
|
|
137
|
+
"level",
|
|
138
|
+
KeyValueColumnFormatter(
|
|
139
|
+
key_style=None,
|
|
140
|
+
value_style="",
|
|
141
|
+
reset_style=RESET_ALL,
|
|
142
|
+
value_repr=str,
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
Column("logger_name", logger_name_formatter),
|
|
146
|
+
Column("logger", logger_name_formatter),
|
|
147
|
+
Column(
|
|
148
|
+
event_key,
|
|
149
|
+
KeyValueColumnFormatter(
|
|
150
|
+
key_style=None,
|
|
151
|
+
value_style=BRIGHT,
|
|
152
|
+
reset_style=RESET_ALL,
|
|
153
|
+
value_repr=str,
|
|
154
|
+
width=30,
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
],
|
|
158
|
+
)
|
libsh-0.2.0/src/libsh/logs.py
DELETED
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
"""Centralized logging configuration using structlog."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
import time
|
|
7
|
-
from collections.abc import Callable
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from io import StringIO
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
import structlog
|
|
13
|
-
from structlog.dev import (
|
|
14
|
-
BLUE,
|
|
15
|
-
BRIGHT,
|
|
16
|
-
CYAN,
|
|
17
|
-
DIM,
|
|
18
|
-
GREEN,
|
|
19
|
-
MAGENTA,
|
|
20
|
-
RED,
|
|
21
|
-
RED_BACK,
|
|
22
|
-
RESET_ALL,
|
|
23
|
-
YELLOW,
|
|
24
|
-
Column,
|
|
25
|
-
ConsoleRenderer,
|
|
26
|
-
KeyValueColumnFormatter,
|
|
27
|
-
_pad,
|
|
28
|
-
)
|
|
29
|
-
from structlog.typing import EventDict, Processor
|
|
30
|
-
|
|
31
|
-
from .proc import FloatPrecisionProcessor, LoggerFilterProcessor
|
|
32
|
-
|
|
33
|
-
# Store program start time for relative timestamps
|
|
34
|
-
_PROGRAM_START_TIME = time.time()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def hex_to_ansi_fg(hex_color: int) -> str:
|
|
38
|
-
"""Convert hex color (e.g., 0xad8a89) to ANSI 24-bit foreground escape code."""
|
|
39
|
-
r = (hex_color >> 16) & 0xFF
|
|
40
|
-
g = (hex_color >> 8) & 0xFF
|
|
41
|
-
b = hex_color & 0xFF
|
|
42
|
-
return f"\x1b[38;2;{r};{g};{b}m"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@dataclass
|
|
46
|
-
class RegexValueColumnFormatter:
|
|
47
|
-
"""
|
|
48
|
-
Format a key-value pair with regex-based value styling.
|
|
49
|
-
|
|
50
|
-
Like KeyValueColumnFormatter, but allows mapping values to styles based on
|
|
51
|
-
regular expression patterns.
|
|
52
|
-
|
|
53
|
-
:param key_style: The style to apply to the key. If None, the key is omitted.
|
|
54
|
-
:param value_style_map: A list of (regex_pattern, style) tuples. The first
|
|
55
|
-
pattern that matches the value will determine its style.
|
|
56
|
-
:param default_value_style: The style to use if no regex patterns match.
|
|
57
|
-
:param reset_style: The style to apply whenever a style is no longer needed.
|
|
58
|
-
:param value_repr: A callable that returns the string representation of the value.
|
|
59
|
-
:param width: The width to pad the value to. If 0, no padding is done.
|
|
60
|
-
:param prefix: A string to prepend to the formatted key-value pair. May contain
|
|
61
|
-
styles.
|
|
62
|
-
:param postfix: A string to append to the formatted key-value pair. May contain
|
|
63
|
-
styles.
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
key_style: str | None
|
|
67
|
-
value_style_map: list[tuple[str, str]]
|
|
68
|
-
default_value_style: str
|
|
69
|
-
reset_style: str
|
|
70
|
-
value_repr: Callable[[object], str]
|
|
71
|
-
width: int = 0
|
|
72
|
-
prefix: str = ""
|
|
73
|
-
postfix: str = ""
|
|
74
|
-
|
|
75
|
-
def __post_init__(self) -> None:
|
|
76
|
-
"""Compile regex patterns for efficiency."""
|
|
77
|
-
self._compiled_patterns = [
|
|
78
|
-
(re.compile(pattern), style) for pattern, style in self.value_style_map
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
def __call__(self, key: str, value: object) -> str:
|
|
82
|
-
sio = StringIO()
|
|
83
|
-
|
|
84
|
-
if self.prefix:
|
|
85
|
-
sio.write(self.prefix)
|
|
86
|
-
sio.write(self.reset_style)
|
|
87
|
-
|
|
88
|
-
if self.key_style is not None:
|
|
89
|
-
sio.write(self.key_style)
|
|
90
|
-
sio.write(key)
|
|
91
|
-
sio.write(self.reset_style)
|
|
92
|
-
sio.write("=")
|
|
93
|
-
|
|
94
|
-
# Determine value style based on regex matching
|
|
95
|
-
value_str = self.value_repr(value)
|
|
96
|
-
value_style = self.default_value_style
|
|
97
|
-
|
|
98
|
-
for pattern, style in self._compiled_patterns:
|
|
99
|
-
if pattern.search(value_str):
|
|
100
|
-
value_style = style
|
|
101
|
-
break
|
|
102
|
-
|
|
103
|
-
sio.write(value_style)
|
|
104
|
-
sio.write(_pad(value_str, self.width))
|
|
105
|
-
sio.write(self.reset_style)
|
|
106
|
-
|
|
107
|
-
if self.postfix:
|
|
108
|
-
sio.write(self.postfix)
|
|
109
|
-
sio.write(self.reset_style)
|
|
110
|
-
|
|
111
|
-
return sio.getvalue()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class NerdStyles:
|
|
115
|
-
reset = RESET_ALL
|
|
116
|
-
bright = BRIGHT
|
|
117
|
-
|
|
118
|
-
level_critical = RED
|
|
119
|
-
level_exception = RED
|
|
120
|
-
level_error = RED
|
|
121
|
-
level_warn = YELLOW
|
|
122
|
-
level_info = GREEN
|
|
123
|
-
level_debug = GREEN
|
|
124
|
-
level_notset = RED_BACK
|
|
125
|
-
|
|
126
|
-
timestamp = DIM
|
|
127
|
-
logger_name = BLUE
|
|
128
|
-
kv_key = CYAN
|
|
129
|
-
kv_value = MAGENTA
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _relative_time_processor(
|
|
133
|
-
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
134
|
-
) -> EventDict:
|
|
135
|
-
"""Add relative timestamp since program start with hours:minutes:seconds.milliseconds format."""
|
|
136
|
-
hours, minutes, seconds = _relative_time_parts()
|
|
137
|
-
|
|
138
|
-
# ANSI color codes: \x1b[2m for dim/gray, \x1b[90m for darker gray, \x1b[0m to reset
|
|
139
|
-
gray = "\x1b[2m" # Normal gray (like original timestamp)
|
|
140
|
-
dark_gray = "\x1b[90m" # Darker gray for zeros
|
|
141
|
-
reset = "\x1b[0m"
|
|
142
|
-
|
|
143
|
-
separator = f"{gray}:{reset}"
|
|
144
|
-
|
|
145
|
-
# Format hours with darker gray if zero, normal gray otherwise
|
|
146
|
-
hours_str = f"{gray}{hours:02d}{reset}{separator}" if hours != 0 else ""
|
|
147
|
-
|
|
148
|
-
# Format minutes with darker gray if zero, normal gray otherwise
|
|
149
|
-
minutes_str = f"{gray}{minutes:02d}{reset}{separator}" if minutes != 0 and hours != 0 else ""
|
|
150
|
-
|
|
151
|
-
# Always show seconds in normal gray
|
|
152
|
-
seconds_str = f"{gray}{seconds:06.3f}{reset}"
|
|
153
|
-
|
|
154
|
-
# Colons in normal gray too
|
|
155
|
-
time_str = f"{dark_gray}+{reset}{hours_str}{minutes_str}{seconds_str}"
|
|
156
|
-
|
|
157
|
-
event_dict["timestamp"] = time_str
|
|
158
|
-
return event_dict
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _relative_time_processor_plain(
|
|
162
|
-
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
163
|
-
) -> EventDict:
|
|
164
|
-
"""Add relative timestamp without ANSI styling for machine-consumable outputs."""
|
|
165
|
-
hours, minutes, seconds = _relative_time_parts()
|
|
166
|
-
|
|
167
|
-
separator = ":"
|
|
168
|
-
hours_str = f"{hours:02d}{separator}" if hours != 0 else ""
|
|
169
|
-
minutes_str = f"{minutes:02d}{separator}" if minutes != 0 and hours != 0 else ""
|
|
170
|
-
seconds_str = f"{seconds:06.3f}"
|
|
171
|
-
|
|
172
|
-
event_dict["timestamp"] = f"+{hours_str}{minutes_str}{seconds_str}"
|
|
173
|
-
return event_dict
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _relative_time_parts() -> tuple[int, int, float]:
|
|
177
|
-
"""Return elapsed (hours, minutes, seconds) since program start."""
|
|
178
|
-
elapsed = time.time() - _PROGRAM_START_TIME
|
|
179
|
-
hours = int(elapsed // 3600)
|
|
180
|
-
minutes = int((elapsed % 3600) // 60)
|
|
181
|
-
seconds = elapsed % 60
|
|
182
|
-
return hours, minutes, seconds
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _compact_level_processor(
|
|
186
|
-
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
187
|
-
) -> EventDict:
|
|
188
|
-
"""Convert log levels to compact 4-character format with 24-bit colors and darker brackets."""
|
|
189
|
-
# 24-bit color codes: \x1b[38;2;R;G;Bm for foreground, \x1b[48;2;R;G;Bm for background
|
|
190
|
-
reset = "\x1b[0m"
|
|
191
|
-
|
|
192
|
-
# Original colors and their 10% darkened versions for brackets
|
|
193
|
-
level_mapping = {
|
|
194
|
-
"debug": {
|
|
195
|
-
"text": f"{hex_to_ansi_fg(0x908CAA)}dbug{reset}", # fg #908caa
|
|
196
|
-
"bracket": hex_to_ansi_fg(0x827E99), # 10% darker
|
|
197
|
-
},
|
|
198
|
-
"info": {
|
|
199
|
-
"text": f"{hex_to_ansi_fg(0x9CCFD8)}info{reset}", # fg #9ccfd8
|
|
200
|
-
"bracket": hex_to_ansi_fg(0x8CBAC2), # 10% darker
|
|
201
|
-
},
|
|
202
|
-
"warning": {
|
|
203
|
-
"text": f"{hex_to_ansi_fg(0xF6C177)}warn{reset}", # fg #f6c177
|
|
204
|
-
"bracket": hex_to_ansi_fg(0xDDAE6B), # 10% darker
|
|
205
|
-
},
|
|
206
|
-
"error": {
|
|
207
|
-
"text": f"{hex_to_ansi_fg(0xEB6F92)}eror{reset}", # fg #eb6f92
|
|
208
|
-
"bracket": hex_to_ansi_fg(0xD46483), # 10% darker
|
|
209
|
-
},
|
|
210
|
-
"exception": {
|
|
211
|
-
"text": f"{hex_to_ansi_fg(0xEB6F92)}exc!{reset}", # fg #eb6f92
|
|
212
|
-
"bracket": hex_to_ansi_fg(0xD46483), # 10% darker
|
|
213
|
-
},
|
|
214
|
-
"critical": {
|
|
215
|
-
"text": f"\x1b[48;2;235;111;146;38;2;33;32;46mcrit{reset}", # bg #eb6f92, fg #21202e
|
|
216
|
-
"bracket": hex_to_ansi_fg(0xD46483), # 10% darker
|
|
217
|
-
},
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if "level" in event_dict:
|
|
221
|
-
original_level = event_dict["level"]
|
|
222
|
-
if original_level in level_mapping:
|
|
223
|
-
colors = level_mapping[original_level]
|
|
224
|
-
event_dict["level"] = (
|
|
225
|
-
f"{colors['bracket']}[{reset}{colors['text']}{colors['bracket']}]{reset}"
|
|
226
|
-
)
|
|
227
|
-
else:
|
|
228
|
-
event_dict["level"] = original_level
|
|
229
|
-
|
|
230
|
-
return event_dict
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _debug_event_colorer(
|
|
234
|
-
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
235
|
-
) -> EventDict:
|
|
236
|
-
"""Color the event text purple for debug level messages."""
|
|
237
|
-
# Check both the original level and processed level
|
|
238
|
-
level = event_dict.get("level", "")
|
|
239
|
-
if ("debug" in str(level).lower() or level == "debug") and "event" in event_dict:
|
|
240
|
-
# Color debug event text with #908caa
|
|
241
|
-
debug_color = hex_to_ansi_fg(0x908CAA)
|
|
242
|
-
reset = "\x1b[0m"
|
|
243
|
-
event_dict["event"] = f"{debug_color}{event_dict['event']}{reset}"
|
|
244
|
-
|
|
245
|
-
return event_dict
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def setup_logging(
|
|
249
|
-
level: str = "INFO",
|
|
250
|
-
json_output: bool = False,
|
|
251
|
-
correlation_id: str | None = None,
|
|
252
|
-
filter_to_logger: str | None = None,
|
|
253
|
-
) -> None:
|
|
254
|
-
"""Configure structured logging for the application."""
|
|
255
|
-
filters = [structlog.stdlib.filter_by_level]
|
|
256
|
-
if filter_to_logger:
|
|
257
|
-
filters += [LoggerFilterProcessor(filter_to_logger)]
|
|
258
|
-
|
|
259
|
-
# Configure processors
|
|
260
|
-
processors: list[Processor] = []
|
|
261
|
-
|
|
262
|
-
# Add correlation ID if provided
|
|
263
|
-
if correlation_id:
|
|
264
|
-
processors.append(structlog.contextvars.merge_contextvars)
|
|
265
|
-
structlog.contextvars.bind_contextvars(correlation_id=correlation_id)
|
|
266
|
-
|
|
267
|
-
# Level filtering should happen before any expensive processors run
|
|
268
|
-
processors.extend(filters)
|
|
269
|
-
|
|
270
|
-
processors += [
|
|
271
|
-
structlog.stdlib.add_logger_name,
|
|
272
|
-
structlog.stdlib.add_log_level,
|
|
273
|
-
]
|
|
274
|
-
|
|
275
|
-
if json_output:
|
|
276
|
-
processors += [
|
|
277
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
278
|
-
structlog.stdlib.ExtraAdder(),
|
|
279
|
-
FloatPrecisionProcessor(digits=3),
|
|
280
|
-
_relative_time_processor_plain,
|
|
281
|
-
structlog.processors.StackInfoRenderer(),
|
|
282
|
-
# structlog.processors.format_exc_info,
|
|
283
|
-
]
|
|
284
|
-
else:
|
|
285
|
-
processors += [
|
|
286
|
-
_debug_event_colorer, # Run before level processing
|
|
287
|
-
_compact_level_processor,
|
|
288
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
289
|
-
structlog.stdlib.ExtraAdder(),
|
|
290
|
-
FloatPrecisionProcessor(digits=3),
|
|
291
|
-
_relative_time_processor,
|
|
292
|
-
structlog.processors.StackInfoRenderer(),
|
|
293
|
-
# structlog.processors.format_exc_info,
|
|
294
|
-
]
|
|
295
|
-
|
|
296
|
-
# Configure output format
|
|
297
|
-
if json_output:
|
|
298
|
-
log_renderer = structlog.processors.JSONRenderer()
|
|
299
|
-
else:
|
|
300
|
-
event_key: str = "event"
|
|
301
|
-
timestamp_key: str = "timestamp"
|
|
302
|
-
logger_name_formatter = KeyValueColumnFormatter(
|
|
303
|
-
key_style=None,
|
|
304
|
-
value_style=hex_to_ansi_fg(0x7D6B95),
|
|
305
|
-
reset_style=RESET_ALL,
|
|
306
|
-
value_repr=str,
|
|
307
|
-
prefix="[",
|
|
308
|
-
postfix="]",
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
log_renderer = ConsoleRenderer(
|
|
312
|
-
colors=True,
|
|
313
|
-
columns=[
|
|
314
|
-
# Default formatter
|
|
315
|
-
Column(
|
|
316
|
-
"",
|
|
317
|
-
RegexValueColumnFormatter(
|
|
318
|
-
key_style=hex_to_ansi_fg(0x6E6A86),
|
|
319
|
-
value_style_map=[
|
|
320
|
-
(
|
|
321
|
-
# Integers (including negative) get bright green styling
|
|
322
|
-
r"^(True|False)$",
|
|
323
|
-
hex_to_ansi_fg(0x6E6A86),
|
|
324
|
-
),
|
|
325
|
-
(
|
|
326
|
-
# Integers (including negative) get bright green styling
|
|
327
|
-
r"^-?\d+$",
|
|
328
|
-
hex_to_ansi_fg(0xF6C177),
|
|
329
|
-
),
|
|
330
|
-
(
|
|
331
|
-
# Floats (including negative) get bright yellow styling
|
|
332
|
-
r"^-?\d*\.\d+$",
|
|
333
|
-
hex_to_ansi_fg(0xF6C177),
|
|
334
|
-
),
|
|
335
|
-
(
|
|
336
|
-
# Numeric durations (123ms, 45.67s, 12h, etc.)
|
|
337
|
-
r"^-?\d*\.?\d+(?:h|m|s|ms|us|µs)$",
|
|
338
|
-
hex_to_ansi_fg(0x9CCFD8),
|
|
339
|
-
),
|
|
340
|
-
],
|
|
341
|
-
default_value_style="",
|
|
342
|
-
reset_style=RESET_ALL,
|
|
343
|
-
value_repr=str,
|
|
344
|
-
),
|
|
345
|
-
),
|
|
346
|
-
Column(
|
|
347
|
-
timestamp_key,
|
|
348
|
-
KeyValueColumnFormatter(
|
|
349
|
-
key_style=None,
|
|
350
|
-
value_style=DIM,
|
|
351
|
-
reset_style=RESET_ALL,
|
|
352
|
-
value_repr=str,
|
|
353
|
-
),
|
|
354
|
-
),
|
|
355
|
-
Column(
|
|
356
|
-
"level",
|
|
357
|
-
KeyValueColumnFormatter(
|
|
358
|
-
key_style=None,
|
|
359
|
-
value_style="",
|
|
360
|
-
reset_style=RESET_ALL,
|
|
361
|
-
value_repr=str,
|
|
362
|
-
),
|
|
363
|
-
),
|
|
364
|
-
Column("logger_name", logger_name_formatter),
|
|
365
|
-
Column("logger", logger_name_formatter),
|
|
366
|
-
Column(
|
|
367
|
-
event_key,
|
|
368
|
-
KeyValueColumnFormatter(
|
|
369
|
-
key_style=None,
|
|
370
|
-
value_style=BRIGHT,
|
|
371
|
-
reset_style=RESET_ALL,
|
|
372
|
-
value_repr=str,
|
|
373
|
-
width=30,
|
|
374
|
-
),
|
|
375
|
-
),
|
|
376
|
-
],
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
# Omit level filters when processing stdlib log records to avoid double filtering
|
|
380
|
-
formatter_pre_chain: list[Processor] = [proc for proc in processors if proc not in filters]
|
|
381
|
-
|
|
382
|
-
# Configure structlog
|
|
383
|
-
structlog.configure(
|
|
384
|
-
processors=processors + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
|
|
385
|
-
wrapper_class=structlog.stdlib.BoundLogger,
|
|
386
|
-
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
387
|
-
cache_logger_on_first_use=True,
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
formatter = structlog.stdlib.ProcessorFormatter(
|
|
391
|
-
foreign_pre_chain=formatter_pre_chain,
|
|
392
|
-
processors=[
|
|
393
|
-
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
394
|
-
log_renderer,
|
|
395
|
-
],
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
# setup logging
|
|
399
|
-
handler = logging.StreamHandler()
|
|
400
|
-
handler.setFormatter(formatter)
|
|
401
|
-
root_logger = logging.getLogger()
|
|
402
|
-
root_logger.addHandler(handler)
|
|
403
|
-
root_logger.setLevel(level)
|
|
404
|
-
|
|
405
|
-
# Propogate the logs of some libraries
|
|
406
|
-
for liblog in [logging.getLogger(_liblog) for _liblog in ["websockets"]]:
|
|
407
|
-
liblog.handlers.clear()
|
|
408
|
-
liblog.setLevel(logging.WARNING)
|
|
409
|
-
liblog.propagate = True
|
|
410
|
-
|
|
411
|
-
# And suppress the logs of others
|
|
412
|
-
# for liblog in [logging.getLogger(_liblog) for _liblog in ["websockets"]]:
|
|
413
|
-
# liblog.handlers.clear()
|
|
414
|
-
# liblog.propagate = False
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def get_logger(
|
|
418
|
-
name: str | None = None, *args: list[Any], **initial_values: Any
|
|
419
|
-
) -> structlog.stdlib.BoundLogger:
|
|
420
|
-
"""Get a structured logger instance."""
|
|
421
|
-
return structlog.get_logger(*([name] + list(args)), **initial_values)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
def setup_logging_from_env() -> None:
|
|
425
|
-
"""Setup logging using environment variables."""
|
|
426
|
-
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
427
|
-
json_output = os.getenv("JSON_LOGS", "false").lower() in ("true", "1", "yes", "on")
|
|
428
|
-
correlation_id = os.getenv("CORRELATION_ID")
|
|
429
|
-
|
|
430
|
-
setup_logging(level=log_level, json_output=json_output, correlation_id=correlation_id)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from structlog.typing import EventDict, WrappedLogger
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class FloatPrecisionProcessor:
|
|
8
|
-
"""
|
|
9
|
-
A structlog processor for rounding floats. Both as single numbers or in data structures like
|
|
10
|
-
(nested) lists, dicts, or numpy arrays.
|
|
11
|
-
|
|
12
|
-
Inspired by https://github.com/underyx/structlog-pretty/blob/master/structlog_pretty/processors.py
|
|
13
|
-
|
|
14
|
-
NOTE: It seems that if a processor logs internally, like for debugging purposes, it will be
|
|
15
|
-
detected and removed from the processing stack.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
def __init__(
|
|
19
|
-
self,
|
|
20
|
-
digits: int = 3,
|
|
21
|
-
only_fields: set[str] = set(),
|
|
22
|
-
not_fields: set[str] = set(),
|
|
23
|
-
np_array_to_list: bool = True,
|
|
24
|
-
):
|
|
25
|
-
"""
|
|
26
|
-
Create a FloatRounder processor. That rounds floats to the given number of digits.
|
|
27
|
-
|
|
28
|
-
:param digits: The number of digits to round to
|
|
29
|
-
:param only_fields: A set specifying the fields to round (None = round all fields except
|
|
30
|
-
not_fields)
|
|
31
|
-
:param not_fields: A set specifying fields not to round
|
|
32
|
-
:param np_array_to_list: Whether to cast np.array to list for nicer printing
|
|
33
|
-
"""
|
|
34
|
-
self.digits = digits
|
|
35
|
-
self.np_array_to_list = np_array_to_list
|
|
36
|
-
self.only_fields = only_fields
|
|
37
|
-
self.not_fields = not_fields
|
|
38
|
-
|
|
39
|
-
def _round(self, value: Any):
|
|
40
|
-
"""
|
|
41
|
-
Round floats, unpack lists, convert np.arrays to lists
|
|
42
|
-
|
|
43
|
-
:param value: The value/data structure to round
|
|
44
|
-
:returns: The rounded value
|
|
45
|
-
"""
|
|
46
|
-
# round floats
|
|
47
|
-
if isinstance(value, float):
|
|
48
|
-
return round(value, self.digits)
|
|
49
|
-
# convert np.array to list
|
|
50
|
-
if self.np_array_to_list:
|
|
51
|
-
if isinstance(value, np.ndarray):
|
|
52
|
-
return self._round(list(value))
|
|
53
|
-
# round values in lists recursively (to handle lists of lists)
|
|
54
|
-
if isinstance(value, list):
|
|
55
|
-
for idx, item in enumerate(value):
|
|
56
|
-
value[idx] = self._round(item)
|
|
57
|
-
return value
|
|
58
|
-
# similarly, round values in dicts recursively
|
|
59
|
-
if isinstance(value, dict):
|
|
60
|
-
for k, v in value.items():
|
|
61
|
-
value[k] = self._round(v)
|
|
62
|
-
return value
|
|
63
|
-
# return any other values as they are
|
|
64
|
-
return value
|
|
65
|
-
|
|
66
|
-
def __call__(self, _: WrappedLogger, __: str, event_dict: EventDict):
|
|
67
|
-
for key, value in event_dict.items():
|
|
68
|
-
if len(self.only_fields) > 0 and key not in self.only_fields:
|
|
69
|
-
continue
|
|
70
|
-
if len(self.not_fields) > 0 and key in self.not_fields:
|
|
71
|
-
continue
|
|
72
|
-
if isinstance(value, bool):
|
|
73
|
-
continue # don't convert True to 1.0
|
|
74
|
-
|
|
75
|
-
event_dict[key] = self._round(value)
|
|
76
|
-
return event_dict
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from structlog import DropEvent
|
|
2
|
-
from structlog.typing import EventDict, WrappedLogger
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class LoggerFilterProcessor:
|
|
6
|
-
"""
|
|
7
|
-
A structlog processor that filters log events based on the logger name.
|
|
8
|
-
|
|
9
|
-
This processor allows you to specify a logger name, and only log events
|
|
10
|
-
originating from that logger (or its children) will be processed. All other
|
|
11
|
-
log events will be ignored.
|
|
12
|
-
|
|
13
|
-
Example usage:
|
|
14
|
-
import structlog
|
|
15
|
-
|
|
16
|
-
structlog.configure(
|
|
17
|
-
processors=[
|
|
18
|
-
LoggerFilterProcessor("my_logger"),
|
|
19
|
-
structlog.processors.JSONRenderer()
|
|
20
|
-
]
|
|
21
|
-
)
|
|
22
|
-
logger = structlog.get_logger("my_logger")
|
|
23
|
-
logger.info("This will be logged")
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def __init__(self, logger_name: str):
|
|
27
|
-
"""
|
|
28
|
-
Initialize the LoggerFilterProcessor.
|
|
29
|
-
|
|
30
|
-
:param logger_name: The name of the logger to filter on. Only log events
|
|
31
|
-
from this logger or its children will be processed.
|
|
32
|
-
"""
|
|
33
|
-
self.logger_name = logger_name
|
|
34
|
-
|
|
35
|
-
def __call__(self, logger: WrappedLogger, __: str, event_dict: EventDict):
|
|
36
|
-
"""
|
|
37
|
-
Process a log event.
|
|
38
|
-
|
|
39
|
-
:param logger: The logger instance that generated the event.
|
|
40
|
-
:param method_name: The logging method name (e.g., "info", "error").
|
|
41
|
-
:param event_dict: The log event dictionary.
|
|
42
|
-
:return: The event_dict if the event should be processed, or None to ignore it.
|
|
43
|
-
"""
|
|
44
|
-
if logger.name == self.logger_name or logger.name.startswith(f"{self.logger_name}."):
|
|
45
|
-
return event_dict
|
|
46
|
-
else:
|
|
47
|
-
raise DropEvent(f"Filtered out by LoggerFilterProcessor, logger={logger.name}")
|
|
File without changes
|
|
File without changes
|