jaylog 0.2.1__tar.gz → 0.2.4__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.
- {jaylog-0.2.1 → jaylog-0.2.4}/PKG-INFO +1 -1
- {jaylog-0.2.1 → jaylog-0.2.4}/pyproject.toml +1 -1
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/formatters.py +12 -5
- jaylog-0.2.4/src/jaylog/handlers/__init__.py +5 -0
- jaylog-0.2.4/src/jaylog/handlers/console_handler.py +43 -0
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/handlers/file_handler.py +2 -1
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/logger.py +68 -23
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/settings.py +24 -7
- jaylog-0.2.1/src/jaylog/handlers/__init__.py +0 -4
- {jaylog-0.2.1 → jaylog-0.2.4}/README.md +0 -0
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/__init__.py +0 -0
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/filters.py +0 -0
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/handlers/http_handler.py +0 -0
- {jaylog-0.2.1 → jaylog-0.2.4}/src/jaylog/models.py +0 -0
|
@@ -100,11 +100,18 @@ def build_log_entry_dict(record: logging.LogRecord) -> dict:
|
|
|
100
100
|
class PlainTextFormatter(logging.Formatter):
|
|
101
101
|
"""Human-readable single-line formatter for .log files."""
|
|
102
102
|
|
|
103
|
+
def __init__(self, show_service: bool = True) -> None:
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.show_service = show_service
|
|
106
|
+
|
|
103
107
|
def format(self, record: logging.LogRecord) -> str:
|
|
104
108
|
entry = build_log_entry_dict(record)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
log_timestamp = datetime.fromisoformat(entry['log_timestamp']).strftime('%d/%m/%Y %X')
|
|
110
|
+
log_level = f'[{entry["log_level"]}]'
|
|
111
|
+
service_segment = f'[{entry["service"]}] | ' if self.show_service else ''
|
|
112
|
+
return (
|
|
113
|
+
f'{log_timestamp} '
|
|
114
|
+
f'{log_level.ljust(11)} | '
|
|
115
|
+
f'{service_segment}'
|
|
116
|
+
f"{entry['log_message']}"
|
|
109
117
|
)
|
|
110
|
-
return line
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from jaylog.formatters import build_log_entry_dict
|
|
6
|
+
|
|
7
|
+
_GREEN = "\033[32m"
|
|
8
|
+
_BLUE = "\033[34m"
|
|
9
|
+
_PURPLE = "\033[35m"
|
|
10
|
+
_YELLOW = "\033[33m"
|
|
11
|
+
_RED = "\033[31m"
|
|
12
|
+
_RESET = "\033[0m"
|
|
13
|
+
|
|
14
|
+
_LEVEL_COLORS = {
|
|
15
|
+
"DEBUG": _PURPLE,
|
|
16
|
+
"INFO": _BLUE,
|
|
17
|
+
"WARNING": _YELLOW,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _level_color(level: str) -> str:
|
|
22
|
+
return _LEVEL_COLORS.get(level, _RED)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConsoleFormatter(logging.Formatter):
|
|
26
|
+
def __init__(self, show_service: bool = True) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.show_service = show_service
|
|
29
|
+
|
|
30
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
31
|
+
entry = build_log_entry_dict(record)
|
|
32
|
+
log_timestamp = datetime.fromisoformat(entry["log_timestamp"]).strftime("%d/%m/%Y %X")
|
|
33
|
+
log_level = f'[{entry["log_level"]}]'
|
|
34
|
+
colored_timestamp = f"{_GREEN}{log_timestamp}{_RESET}"
|
|
35
|
+
colored_level = f"{_level_color(entry['log_level'])}{log_level.ljust(11)}{_RESET}"
|
|
36
|
+
service_segment = f'[{entry["service"]}] | ' if self.show_service else ''
|
|
37
|
+
return f"{colored_timestamp} {colored_level} | {service_segment}{entry['log_message']}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class JaylogConsoleHandler(logging.StreamHandler):
|
|
41
|
+
def __init__(self, show_service: bool = True) -> None:
|
|
42
|
+
super().__init__(stream=sys.stdout)
|
|
43
|
+
self.setFormatter(ConsoleFormatter(show_service=show_service))
|
|
@@ -19,6 +19,7 @@ class JaylogFileHandler(RotatingFileHandler):
|
|
|
19
19
|
backup_count: int = 20,
|
|
20
20
|
retention_days: int = 7,
|
|
21
21
|
encoding: str = "utf-8",
|
|
22
|
+
show_service: bool = True,
|
|
22
23
|
) -> None:
|
|
23
24
|
filename.parent.mkdir(parents=True, exist_ok=True)
|
|
24
25
|
super().__init__(
|
|
@@ -28,7 +29,7 @@ class JaylogFileHandler(RotatingFileHandler):
|
|
|
28
29
|
encoding=encoding,
|
|
29
30
|
)
|
|
30
31
|
self.retention_days = retention_days
|
|
31
|
-
self.setFormatter(PlainTextFormatter())
|
|
32
|
+
self.setFormatter(PlainTextFormatter(show_service=show_service))
|
|
32
33
|
|
|
33
34
|
def doRollover(self) -> None:
|
|
34
35
|
super().doRollover()
|
|
@@ -6,6 +6,7 @@ from queue import Queue
|
|
|
6
6
|
|
|
7
7
|
from jaylog.filters import ExceptionFlagFilter
|
|
8
8
|
from jaylog.formatters import configure_screenshot
|
|
9
|
+
from jaylog.handlers.console_handler import JaylogConsoleHandler
|
|
9
10
|
from jaylog.handlers.file_handler import JaylogFileHandler
|
|
10
11
|
from jaylog.handlers.http_handler import JaylogHttpHandler
|
|
11
12
|
from jaylog.settings import JaylogSettings
|
|
@@ -13,14 +14,39 @@ from jaylog.settings import JaylogSettings
|
|
|
13
14
|
# Registry: name -> (logger, listener) so callers can shut down cleanly
|
|
14
15
|
_registry: dict[str, tuple[logging.Logger, QueueListener]] = {}
|
|
15
16
|
_shutdown_registered = False
|
|
16
|
-
|
|
17
|
+
# name (== app_name) -> settings registrada via configure()
|
|
18
|
+
_settings_registry: dict[str, JaylogSettings] = {}
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
def configure(settings: JaylogSettings) -> None:
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
def configure(settings: JaylogSettings | list[JaylogSettings]) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Registra uma ou mais configurações de logger.
|
|
24
|
+
|
|
25
|
+
Aceita uma única ``JaylogSettings`` ou uma lista delas. Cada configuração é
|
|
26
|
+
registrada sob o seu próprio ``app_name``, permitindo vários loggers com
|
|
27
|
+
configurações diferentes no mesmo projeto::
|
|
28
|
+
|
|
29
|
+
configure(settings_order)
|
|
30
|
+
configure([settings_order, settings_billing])
|
|
31
|
+
|
|
32
|
+
Cada chamada é a "fonte da verdade" do conjunto de loggers: derruba os
|
|
33
|
+
loggers registrados anteriormente e registra apenas os informados aqui.
|
|
34
|
+
Levanta ``ValueError`` se a lista contiver ``app_name`` duplicado.
|
|
35
|
+
"""
|
|
36
|
+
items = [settings] if isinstance(settings, JaylogSettings) else list(settings)
|
|
37
|
+
|
|
38
|
+
seen: set[str] = set()
|
|
39
|
+
for item in items:
|
|
40
|
+
if item.app_name in seen:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"app_name duplicado em configure(): '{item.app_name}'"
|
|
43
|
+
)
|
|
44
|
+
seen.add(item.app_name)
|
|
45
|
+
|
|
23
46
|
shutdown()
|
|
47
|
+
_settings_registry.clear()
|
|
48
|
+
for item in items:
|
|
49
|
+
_settings_registry[item.app_name] = item
|
|
24
50
|
|
|
25
51
|
|
|
26
52
|
def _register_shutdown_hooks() -> None:
|
|
@@ -39,13 +65,14 @@ def _register_shutdown_hooks() -> None:
|
|
|
39
65
|
signal.signal(signal.SIGTERM, _sigterm_handler)
|
|
40
66
|
|
|
41
67
|
|
|
42
|
-
def get_logger() -> logging.Logger:
|
|
68
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
43
69
|
"""
|
|
44
70
|
Return a configured logger.
|
|
45
71
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
O ``name`` deve ser exatamente o ``app_name`` de uma configuração registrada
|
|
73
|
+
via ``configure()``. Quando omitido, retorna o logger da **primeira**
|
|
74
|
+
configuração registrada. Chamadas repetidas com o mesmo nome retornam o
|
|
75
|
+
**mesmo** logger, sem re-anexar handlers.
|
|
49
76
|
|
|
50
77
|
Architecture:
|
|
51
78
|
logger → QueueHandler → Queue → QueueListener → [FileHandler, HttpHandler?]
|
|
@@ -53,12 +80,22 @@ def get_logger() -> logging.Logger:
|
|
|
53
80
|
The QueueListener runs in a background thread so `emit()` never blocks the
|
|
54
81
|
calling thread.
|
|
55
82
|
"""
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
if not _settings_registry:
|
|
84
|
+
raise Exception(
|
|
85
|
+
'Não é possivel retornar uma instancia de logger sem configuração\n'
|
|
86
|
+
'use jaylog.configure() antes de jaylog.get_logger()'
|
|
87
|
+
)
|
|
60
88
|
|
|
61
|
-
name
|
|
89
|
+
if name is None:
|
|
90
|
+
name = next(iter(_settings_registry))
|
|
91
|
+
elif name not in _settings_registry:
|
|
92
|
+
disponiveis = ', '.join(_settings_registry) or '(nenhum)'
|
|
93
|
+
raise KeyError(
|
|
94
|
+
f"Nenhuma configuração registrada para '{name}'. "
|
|
95
|
+
f"Nomes disponíveis: {disponiveis}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
settings = _settings_registry[name]
|
|
62
99
|
|
|
63
100
|
configure_screenshot(settings.log_screenshot_enabled)
|
|
64
101
|
|
|
@@ -69,16 +106,24 @@ def get_logger() -> logging.Logger:
|
|
|
69
106
|
# Build the actual (downstream) handlers
|
|
70
107
|
# ------------------------------------------------------------------
|
|
71
108
|
downstream: list[logging.Handler] = []
|
|
109
|
+
show_service = len(_settings_registry) > 1
|
|
110
|
+
|
|
111
|
+
if settings.log_dir is not None:
|
|
112
|
+
log_path = settings.log_dir / settings.log_filename
|
|
113
|
+
file_handler = JaylogFileHandler(
|
|
114
|
+
filename=log_path,
|
|
115
|
+
max_bytes=settings.log_max_bytes,
|
|
116
|
+
backup_count=settings.log_backup_count,
|
|
117
|
+
retention_days=settings.log_retention_days,
|
|
118
|
+
show_service=show_service,
|
|
119
|
+
)
|
|
120
|
+
file_handler.setLevel(settings.log_level)
|
|
121
|
+
downstream.append(file_handler)
|
|
72
122
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
backup_count=settings.log_backup_count,
|
|
78
|
-
retention_days=settings.log_retention_days,
|
|
79
|
-
)
|
|
80
|
-
file_handler.setLevel(settings.log_level)
|
|
81
|
-
downstream.append(file_handler)
|
|
123
|
+
if settings.log_console_enabled:
|
|
124
|
+
console_handler = JaylogConsoleHandler(show_service=show_service)
|
|
125
|
+
console_handler.setLevel(settings.log_level)
|
|
126
|
+
downstream.append(console_handler)
|
|
82
127
|
|
|
83
128
|
if settings.log_http_endpoint and settings.log_http_api_key:
|
|
84
129
|
http_handler = JaylogHttpHandler(
|
|
@@ -17,15 +17,21 @@ class JaylogSettings(BaseSettings):
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
_env_file: str | tuple | None = None
|
|
20
|
+
_secrets_dir: str | None = None
|
|
20
21
|
|
|
21
|
-
def __init__(
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
_env_file=model_config.get('env_file'),
|
|
25
|
+
_secrets_dir=model_config.get('secrets_dir'),
|
|
26
|
+
**data
|
|
27
|
+
):
|
|
28
|
+
super().__init__(_env_file=_env_file, _secrets_dir=_secrets_dir, **data)
|
|
24
29
|
object.__setattr__(self, '_env_file', _env_file)
|
|
30
|
+
object.__setattr__(self, '_secrets_dir', _secrets_dir)
|
|
25
31
|
|
|
26
32
|
# App identity
|
|
27
33
|
app_name: str
|
|
28
|
-
log_dir: Path
|
|
34
|
+
log_dir: Path | None = None
|
|
29
35
|
|
|
30
36
|
secrets_dir: Path | None = None
|
|
31
37
|
|
|
@@ -41,25 +47,36 @@ class JaylogSettings(BaseSettings):
|
|
|
41
47
|
log_http_timeout: float = 5.0
|
|
42
48
|
log_http_proxy: Optional[str] = None
|
|
43
49
|
|
|
50
|
+
# Console handler — desativar com JAYLOG_LOG_CONSOLE_ENABLED=false
|
|
51
|
+
log_console_enabled: bool = True
|
|
52
|
+
|
|
44
53
|
# Screenshot (log_img field) — desativar com JAYLOG_LOG_SCREENSHOT_ENABLED=false
|
|
45
54
|
log_screenshot_enabled: bool = False
|
|
46
55
|
|
|
47
56
|
@field_validator("log_dir", mode="after")
|
|
48
57
|
@classmethod
|
|
49
|
-
def validate_log_dir(cls, v: Path) -> Path:
|
|
50
|
-
if v.exists() and not v.is_dir():
|
|
58
|
+
def validate_log_dir(cls, v: Path | None) -> Path | None:
|
|
59
|
+
if v is not None and v.exists() and not v.is_dir():
|
|
51
60
|
raise ValueError(f"JAYLOG_LOG_DIR '{v}' exists but is not a directory")
|
|
52
61
|
return v
|
|
53
62
|
|
|
54
63
|
@computed_field
|
|
55
64
|
@property
|
|
56
|
-
def log_filename(self) -> Path:
|
|
65
|
+
def log_filename(self) -> Path | None:
|
|
66
|
+
if self.log_dir is None:
|
|
67
|
+
return None
|
|
57
68
|
return Path(f"{self.app_name}_{_HOSTNAME}_{_HOST_USERNAME}.log")
|
|
58
69
|
|
|
59
70
|
def reload_secrets(self):
|
|
60
71
|
if self.secrets_dir:
|
|
61
72
|
if not self.secrets_dir.exists() or not self.secrets_dir.is_dir:
|
|
62
73
|
raise ValueError('SECRETS_DIR is not valid directory')
|
|
74
|
+
|
|
75
|
+
elif self._secrets_dir != self.model_config.get('secrets_dir'):
|
|
76
|
+
raise SyntaxError(
|
|
77
|
+
'Não é possivel usar `reload_secrets` caso o valor de _secrets_dir foi sobrescrito'
|
|
78
|
+
)
|
|
79
|
+
|
|
63
80
|
|
|
64
81
|
|
|
65
82
|
return JaylogSettings(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|