libsh 0.0.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.0.0/PKG-INFO +13 -0
- libsh-0.0.0/pyproject.toml +50 -0
- libsh-0.0.0/src/libsh/__init__.py +27 -0
- libsh-0.0.0/src/libsh/format.py +45 -0
- libsh-0.0.0/src/libsh/logs.py +391 -0
- libsh-0.0.0/src/libsh/proc/__init__.py +7 -0
- libsh-0.0.0/src/libsh/proc/float.py +76 -0
- libsh-0.0.0/src/libsh/proc/logger_filter.py +47 -0
- libsh-0.0.0/src/libsh/py.typed +0 -0
libsh-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: libsh
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: shyndman
|
|
6
|
+
Author-email: shyndman <scotty.hyndman@gmail.com>
|
|
7
|
+
Requires-Dist: numpy<2
|
|
8
|
+
Requires-Dist: pydantic>=2
|
|
9
|
+
Requires-Dist: rich>=14.1.0
|
|
10
|
+
Requires-Dist: structlog>=25.4.0
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Project-URL: Bug Tracker, https://github.com/shyndman/libsh-python/issues
|
|
13
|
+
Project-URL: repository, https://github.com/shyndman/libsh-python
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "libsh"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
authors = [{name = "shyndman", email = "scotty.hyndman@gmail.com"}]
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = ["numpy<2", "pydantic>=2", "rich>=14.1.0", "structlog>=25.4.0"]
|
|
8
|
+
|
|
9
|
+
[build-system]
|
|
10
|
+
requires = ["uv_build>=0.9.5,<0.10.0"]
|
|
11
|
+
build-backend = "uv_build"
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
repository = "https://github.com/shyndman/libsh-python"
|
|
15
|
+
"Bug Tracker" = "https://github.com/shyndman/libsh-python/issues"
|
|
16
|
+
|
|
17
|
+
[tool.uv.build-backend]
|
|
18
|
+
module-name = "libsh"
|
|
19
|
+
module-root = "src"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[tool.pyright]
|
|
23
|
+
include = ["src", "tests"]
|
|
24
|
+
ignore = ["**/.venv"]
|
|
25
|
+
defineConstant = {DEBUG = true}
|
|
26
|
+
reportMissingImports = "error"
|
|
27
|
+
deprecateTypingAliases = true
|
|
28
|
+
reportDeprecated = "information"
|
|
29
|
+
pythonPlatform = "Linux"
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 100
|
|
33
|
+
target-version = "py312"
|
|
34
|
+
indent-width = 2
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
37
|
+
# Disallow all relative imports.
|
|
38
|
+
ban-relative-imports = "all"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "W"]
|
|
42
|
+
ignore = []
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.isort]
|
|
45
|
+
known-first-party = ["libsh"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.format]
|
|
48
|
+
quote-style = "double"
|
|
49
|
+
indent-style = "space"
|
|
50
|
+
line-ending = "auto"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scott's common package.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .format import (
|
|
6
|
+
Microseconds,
|
|
7
|
+
Milliseconds,
|
|
8
|
+
Pretty,
|
|
9
|
+
Range,
|
|
10
|
+
Samples,
|
|
11
|
+
Seconds,
|
|
12
|
+
Unit,
|
|
13
|
+
)
|
|
14
|
+
from .logs import get_logger, setup_logging, setup_logging_from_env
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"get_logger",
|
|
18
|
+
"setup_logging",
|
|
19
|
+
"setup_logging_from_env",
|
|
20
|
+
"Microseconds",
|
|
21
|
+
"Milliseconds",
|
|
22
|
+
"Pretty",
|
|
23
|
+
"Range",
|
|
24
|
+
"Samples",
|
|
25
|
+
"Seconds",
|
|
26
|
+
"Unit",
|
|
27
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import NamedTuple
|
|
2
|
+
|
|
3
|
+
from rich.pretty import pretty_repr
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Pretty(NamedTuple):
|
|
7
|
+
value: object
|
|
8
|
+
|
|
9
|
+
def __str__(self) -> str:
|
|
10
|
+
return pretty_repr(self.value)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Unit(NamedTuple):
|
|
14
|
+
value: float
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Seconds(Unit):
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
return f"{self.value:.3}s"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Milliseconds(Unit):
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return f"{self.value:.3}ms"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Microseconds(Unit):
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return f"{self.value:.3}μs"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Samples(Unit):
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
return f"{self.value:.3} samples"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_EN_DASH = "–"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Range(NamedTuple):
|
|
41
|
+
start: Unit
|
|
42
|
+
end: Unit
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
return f"{self.start}{_EN_DASH}{self.end}"
|
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
elapsed = time.time() - _PROGRAM_START_TIME
|
|
137
|
+
|
|
138
|
+
# Calculate hours, minutes, seconds
|
|
139
|
+
hours = int(elapsed // 3600)
|
|
140
|
+
minutes = int((elapsed % 3600) // 60)
|
|
141
|
+
seconds = elapsed % 60
|
|
142
|
+
|
|
143
|
+
# ANSI color codes: \x1b[2m for dim/gray, \x1b[90m for darker gray, \x1b[0m to reset
|
|
144
|
+
gray = "\x1b[2m" # Normal gray (like original timestamp)
|
|
145
|
+
dark_gray = "\x1b[90m" # Darker gray for zeros
|
|
146
|
+
reset = "\x1b[0m"
|
|
147
|
+
|
|
148
|
+
separator = f"{gray}:{reset}"
|
|
149
|
+
|
|
150
|
+
# Format hours with darker gray if zero, normal gray otherwise
|
|
151
|
+
hours_str = f"{gray}{hours:02d}{reset}{separator}" if hours != 0 else ""
|
|
152
|
+
|
|
153
|
+
# Format minutes with darker gray if zero, normal gray otherwise
|
|
154
|
+
minutes_str = f"{gray}{minutes:02d}{reset}{separator}" if minutes != 0 and hours != 0 else ""
|
|
155
|
+
|
|
156
|
+
# Always show seconds in normal gray
|
|
157
|
+
seconds_str = f"{gray}{seconds:06.3f}{reset}"
|
|
158
|
+
|
|
159
|
+
# Colons in normal gray too
|
|
160
|
+
time_str = f"{dark_gray}+{reset}{hours_str}{minutes_str}{seconds_str}"
|
|
161
|
+
|
|
162
|
+
event_dict["timestamp"] = time_str
|
|
163
|
+
return event_dict
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _compact_level_processor(
|
|
167
|
+
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
168
|
+
) -> EventDict:
|
|
169
|
+
"""Convert log levels to compact 4-character format with 24-bit colors and darker brackets."""
|
|
170
|
+
# 24-bit color codes: \x1b[38;2;R;G;Bm for foreground, \x1b[48;2;R;G;Bm for background
|
|
171
|
+
reset = "\x1b[0m"
|
|
172
|
+
|
|
173
|
+
# Original colors and their 10% darkened versions for brackets
|
|
174
|
+
level_mapping = {
|
|
175
|
+
"debug": {
|
|
176
|
+
"text": f"{hex_to_ansi_fg(0x908CAA)}dbug{reset}", # fg #908caa
|
|
177
|
+
"bracket": hex_to_ansi_fg(0x827E99), # 10% darker
|
|
178
|
+
},
|
|
179
|
+
"info": {
|
|
180
|
+
"text": f"{hex_to_ansi_fg(0x9CCFD8)}info{reset}", # fg #9ccfd8
|
|
181
|
+
"bracket": hex_to_ansi_fg(0x8CBAC2), # 10% darker
|
|
182
|
+
},
|
|
183
|
+
"warning": {
|
|
184
|
+
"text": f"{hex_to_ansi_fg(0xF6C177)}warn{reset}", # fg #f6c177
|
|
185
|
+
"bracket": hex_to_ansi_fg(0xDDAE6B), # 10% darker
|
|
186
|
+
},
|
|
187
|
+
"error": {
|
|
188
|
+
"text": f"{hex_to_ansi_fg(0xEB6F92)}eror{reset}", # fg #eb6f92
|
|
189
|
+
"bracket": hex_to_ansi_fg(0xD46483), # 10% darker
|
|
190
|
+
},
|
|
191
|
+
"exception": {
|
|
192
|
+
"text": f"{hex_to_ansi_fg(0xEB6F92)}exc!{reset}", # fg #eb6f92
|
|
193
|
+
"bracket": hex_to_ansi_fg(0xD46483), # 10% darker
|
|
194
|
+
},
|
|
195
|
+
"critical": {
|
|
196
|
+
"text": f"\x1b[48;2;235;111;146;38;2;33;32;46mcrit{reset}", # bg #eb6f92, fg #21202e
|
|
197
|
+
"bracket": hex_to_ansi_fg(0xD46483), # 10% darker
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if "level" in event_dict:
|
|
202
|
+
original_level = event_dict["level"]
|
|
203
|
+
if original_level in level_mapping:
|
|
204
|
+
colors = level_mapping[original_level]
|
|
205
|
+
event_dict["level"] = (
|
|
206
|
+
f"{colors['bracket']}[{reset}{colors['text']}{colors['bracket']}]{reset}"
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
event_dict["level"] = original_level
|
|
210
|
+
|
|
211
|
+
return event_dict
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _debug_event_colorer(
|
|
215
|
+
_logger: structlog.stdlib.BoundLogger, _method_name: str, event_dict: EventDict
|
|
216
|
+
) -> EventDict:
|
|
217
|
+
"""Color the event text purple for debug level messages."""
|
|
218
|
+
# Check both the original level and processed level
|
|
219
|
+
level = event_dict.get("level", "")
|
|
220
|
+
if ("debug" in str(level).lower() or level == "debug") and "event" in event_dict:
|
|
221
|
+
# Color debug event text with #908caa
|
|
222
|
+
debug_color = hex_to_ansi_fg(0x908CAA)
|
|
223
|
+
reset = "\x1b[0m"
|
|
224
|
+
event_dict["event"] = f"{debug_color}{event_dict['event']}{reset}"
|
|
225
|
+
|
|
226
|
+
return event_dict
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def setup_logging(
|
|
230
|
+
level: str = "INFO",
|
|
231
|
+
json_output: bool = False,
|
|
232
|
+
correlation_id: str | None = None,
|
|
233
|
+
filter_to_logger: str | None = None,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Configure structured logging for the application."""
|
|
236
|
+
filters = [structlog.stdlib.filter_by_level]
|
|
237
|
+
if filter_to_logger:
|
|
238
|
+
filters += [LoggerFilterProcessor(filter_to_logger)]
|
|
239
|
+
|
|
240
|
+
# Configure processors
|
|
241
|
+
shared_processors: list[Processor] = [
|
|
242
|
+
structlog.stdlib.add_logger_name,
|
|
243
|
+
structlog.stdlib.add_log_level,
|
|
244
|
+
*filters,
|
|
245
|
+
_debug_event_colorer, # Run before level processing
|
|
246
|
+
_compact_level_processor,
|
|
247
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
248
|
+
structlog.stdlib.ExtraAdder(),
|
|
249
|
+
FloatPrecisionProcessor(digits=3),
|
|
250
|
+
_relative_time_processor,
|
|
251
|
+
structlog.processors.StackInfoRenderer(),
|
|
252
|
+
# structlog.processors.format_exc_info,
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
# Add correlation ID if provided
|
|
256
|
+
if correlation_id:
|
|
257
|
+
shared_processors.insert(0, structlog.contextvars.merge_contextvars)
|
|
258
|
+
structlog.contextvars.bind_contextvars(correlation_id=correlation_id)
|
|
259
|
+
|
|
260
|
+
# Configure output format
|
|
261
|
+
if json_output:
|
|
262
|
+
log_renderer = structlog.processors.JSONRenderer()
|
|
263
|
+
else:
|
|
264
|
+
event_key: str = "event"
|
|
265
|
+
timestamp_key: str = "timestamp"
|
|
266
|
+
logger_name_formatter = KeyValueColumnFormatter(
|
|
267
|
+
key_style=None,
|
|
268
|
+
value_style=hex_to_ansi_fg(0x7D6B95),
|
|
269
|
+
reset_style=RESET_ALL,
|
|
270
|
+
value_repr=str,
|
|
271
|
+
prefix="[",
|
|
272
|
+
postfix="]",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
log_renderer = ConsoleRenderer(
|
|
276
|
+
colors=True,
|
|
277
|
+
columns=[
|
|
278
|
+
# Default formatter
|
|
279
|
+
Column(
|
|
280
|
+
"",
|
|
281
|
+
RegexValueColumnFormatter(
|
|
282
|
+
key_style=hex_to_ansi_fg(0x6E6A86),
|
|
283
|
+
value_style_map=[
|
|
284
|
+
(
|
|
285
|
+
# Integers (including negative) get bright green styling
|
|
286
|
+
r"^True|False$",
|
|
287
|
+
hex_to_ansi_fg(0x6E6A86),
|
|
288
|
+
),
|
|
289
|
+
(
|
|
290
|
+
# Integers (including negative) get bright green styling
|
|
291
|
+
r"^-?\d+$",
|
|
292
|
+
hex_to_ansi_fg(0xF6C177),
|
|
293
|
+
),
|
|
294
|
+
(
|
|
295
|
+
# Floats (including negative) get bright yellow styling
|
|
296
|
+
r"^-?\d*\.\d+$",
|
|
297
|
+
hex_to_ansi_fg(0xF6C177),
|
|
298
|
+
),
|
|
299
|
+
(
|
|
300
|
+
# Numeric durations (123ms, 45.67s, 12h, etc.)
|
|
301
|
+
r"^-?\d*\.?\d+(?:h|m|s|ms|us|µs)$",
|
|
302
|
+
hex_to_ansi_fg(0x9CCFD8),
|
|
303
|
+
),
|
|
304
|
+
],
|
|
305
|
+
default_value_style="",
|
|
306
|
+
reset_style=RESET_ALL,
|
|
307
|
+
value_repr=str,
|
|
308
|
+
),
|
|
309
|
+
),
|
|
310
|
+
Column(
|
|
311
|
+
timestamp_key,
|
|
312
|
+
KeyValueColumnFormatter(
|
|
313
|
+
key_style=None,
|
|
314
|
+
value_style=DIM,
|
|
315
|
+
reset_style=RESET_ALL,
|
|
316
|
+
value_repr=str,
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
Column(
|
|
320
|
+
"level",
|
|
321
|
+
KeyValueColumnFormatter(
|
|
322
|
+
key_style=None,
|
|
323
|
+
value_style="",
|
|
324
|
+
reset_style=RESET_ALL,
|
|
325
|
+
value_repr=str,
|
|
326
|
+
),
|
|
327
|
+
),
|
|
328
|
+
Column("logger_name", logger_name_formatter),
|
|
329
|
+
Column("logger", logger_name_formatter),
|
|
330
|
+
Column(
|
|
331
|
+
event_key,
|
|
332
|
+
KeyValueColumnFormatter(
|
|
333
|
+
key_style=None,
|
|
334
|
+
value_style=BRIGHT,
|
|
335
|
+
reset_style=RESET_ALL,
|
|
336
|
+
value_repr=str,
|
|
337
|
+
width=30,
|
|
338
|
+
),
|
|
339
|
+
),
|
|
340
|
+
],
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Configure structlog
|
|
344
|
+
structlog.configure(
|
|
345
|
+
processors=shared_processors + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
|
|
346
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
347
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
348
|
+
cache_logger_on_first_use=True,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
formatter = structlog.stdlib.ProcessorFormatter(
|
|
352
|
+
foreign_pre_chain=shared_processors,
|
|
353
|
+
processors=[
|
|
354
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
355
|
+
log_renderer,
|
|
356
|
+
],
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# setup logging
|
|
360
|
+
handler = logging.StreamHandler()
|
|
361
|
+
handler.setFormatter(formatter)
|
|
362
|
+
root_logger = logging.getLogger()
|
|
363
|
+
root_logger.addHandler(handler)
|
|
364
|
+
root_logger.setLevel(level)
|
|
365
|
+
|
|
366
|
+
# Propogate the logs of some libraries
|
|
367
|
+
for liblog in [logging.getLogger(_liblog) for _liblog in ["websockets"]]:
|
|
368
|
+
liblog.handlers.clear()
|
|
369
|
+
liblog.setLevel(logging.WARNING)
|
|
370
|
+
liblog.propagate = True
|
|
371
|
+
|
|
372
|
+
# And suppress the logs of others
|
|
373
|
+
# for liblog in [logging.getLogger(_liblog) for _liblog in ["websockets"]]:
|
|
374
|
+
# liblog.handlers.clear()
|
|
375
|
+
# liblog.propagate = False
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def get_logger(
|
|
379
|
+
name: str | None = None, *args: list[Any], **initial_values: Any
|
|
380
|
+
) -> structlog.stdlib.BoundLogger:
|
|
381
|
+
"""Get a structured logger instance."""
|
|
382
|
+
return structlog.get_logger(*([name] + list(args)), **initial_values)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def setup_logging_from_env() -> None:
|
|
386
|
+
"""Setup logging using environment variables."""
|
|
387
|
+
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
388
|
+
json_output = os.getenv("JSON_LOGS", "false").lower() in ("true", "1", "yes", "on")
|
|
389
|
+
correlation_id = os.getenv("CORRELATION_ID")
|
|
390
|
+
|
|
391
|
+
setup_logging(level=log_level, json_output=json_output, correlation_id=correlation_id)
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|