jaylog 0.2.6__tar.gz → 0.2.8__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.6 → jaylog-0.2.8}/PKG-INFO +55 -2
- {jaylog-0.2.6 → jaylog-0.2.8}/README.md +54 -1
- {jaylog-0.2.6 → jaylog-0.2.8}/pyproject.toml +1 -1
- jaylog-0.2.8/src/jaylog/colors.py +137 -0
- jaylog-0.2.8/src/jaylog/handlers/console_handler.py +71 -0
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/logger.py +4 -1
- jaylog-0.2.8/src/jaylog/settings.py +154 -0
- jaylog-0.2.6/src/jaylog/handlers/console_handler.py +0 -43
- jaylog-0.2.6/src/jaylog/settings.py +0 -84
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/__init__.py +0 -0
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/filters.py +0 -0
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/formatters.py +0 -0
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/handlers/__init__.py +0 -0
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/handlers/file_handler.py +0 -0
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/handlers/http_handler.py +0 -0
- {jaylog-0.2.6 → jaylog-0.2.8}/src/jaylog/models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: jaylog
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Customized Python logging library with file rotation and HTTP forwarding
|
|
5
5
|
Author: Gpocas
|
|
6
6
|
Author-email: Gpocas <gpocas01@gmail.com>
|
|
@@ -39,7 +39,8 @@ As variáveis usam o prefixo `JAYLOG_`. Podem ser definidas no ambiente do siste
|
|
|
39
39
|
| `JAYLOG_LOG_MAX_BYTES` | NÃO | `5242880` | Tamanho máximo do arquivo de log antes de rotacionar (bytes) |
|
|
40
40
|
| `JAYLOG_LOG_BACKUP_COUNT` | NÃO | `5` | Quantidade de arquivos de backup mantidos após rotação |
|
|
41
41
|
| `JAYLOG_LOG_RETENTION_DAYS` | NÃO | `7` | Dias para manter arquivos de log antigos |
|
|
42
|
-
| `JAYLOG_LOG_CONSOLE_ENABLED` | NÃO | `true` | Habilita saída
|
|
42
|
+
| `JAYLOG_LOG_CONSOLE_ENABLED` | NÃO | `true` | Habilita a saída de log no console (`true`/`false`) |
|
|
43
|
+
| `JAYLOG_LOG_CONSOLE_COLOR` | NÃO | `null` | Força (`true`) ou desliga (`false`) as cores no console. Se omitido, detecta automaticamente o suporte do terminal |
|
|
43
44
|
| `JAYLOG_LOG_HTTP_TIMEOUT` | NÃO | `5.0` | Timeout em segundos para o envio HTTP |
|
|
44
45
|
| `JAYLOG_LOG_HTTP_ENDPOINT` | NÃO | `null` | URL do endpoint que receberá os logs |
|
|
45
46
|
| `JAYLOG_LOG_HTTP_API_KEY` | NÃO | `null` | Chave de autenticação enviada no header `x-api-key` |
|
|
@@ -151,6 +152,33 @@ billing_logger.info("Fatura emitida")
|
|
|
151
152
|
|
|
152
153
|
|
|
153
154
|
|
|
155
|
+
## Cores no console
|
|
156
|
+
|
|
157
|
+
As cores são ligadas automaticamente quando o terminal suporta ANSI. A detecção cobre:
|
|
158
|
+
|
|
159
|
+
- **Windows**: o modo *virtual terminal* do console é habilitado em tempo de execução, o que faz as cores funcionarem também no `cmd.exe`/PowerShell rodando no console legado (`conhost`) do Windows 10 — antes só saía colorido no Windows Terminal.
|
|
160
|
+
- **Saída redirecionada** (`python main.py > saida.txt`, pipes, serviços sem console): as cores são desligadas, para o arquivo não ficar com lixo do tipo `←[32m`.
|
|
161
|
+
- **Consoles antigos** que não suportam ANSI de jeito nenhum: o log sai em texto limpo, com o mesmo alinhamento.
|
|
162
|
+
- As convenções `NO_COLOR` e `FORCE_COLOR` são respeitadas.
|
|
163
|
+
|
|
164
|
+
Para forçar um comportamento, use `JAYLOG_LOG_CONSOLE_COLOR`:
|
|
165
|
+
|
|
166
|
+
__*.env.logging*__
|
|
167
|
+
```env
|
|
168
|
+
JAYLOG_APP_NAME=meu-bot
|
|
169
|
+
JAYLOG_LOG_CONSOLE_COLOR=false
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Ou direto no código:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
configure(JaylogSettings(app_name="meu-bot", log_console_color=False))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
> [!TIP]
|
|
179
|
+
> `JAYLOG_LOG_CONSOLE_COLOR=true` força as cores mesmo com a saída redirecionada — útil quando o log é consumido por uma ferramenta que entende ANSI (ex: `... | less -R`).
|
|
180
|
+
|
|
181
|
+
|
|
154
182
|
## Alterando Caminho padrão do .env
|
|
155
183
|
|
|
156
184
|
__*development.env*__
|
|
@@ -232,3 +260,28 @@ configure(JaylogSettings(_env_file='prodution.env').reload_secrets())
|
|
|
232
260
|
logger = get_logger()
|
|
233
261
|
|
|
234
262
|
logger.info("Mensagem de log")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`reload_secrets()` devolve uma **nova** instância preservando tudo que foi passado explicitamente no construtor, então dá para combinar configuração em código com secrets em disco:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
settings = JaylogSettings(app_name='meu-bot', log_level='DEBUG').reload_secrets()
|
|
269
|
+
configure(settings) # app_name e log_level mantidos; endpoint/api_key vêm dos secrets
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
> [!NOTE]
|
|
273
|
+
> Valores passados no construtor têm prioridade sobre os secrets: um `log_http_api_key='...'` definido em código **não** é sobrescrito pelo arquivo em `secrets/`.
|
|
274
|
+
|
|
275
|
+
## Reconfigurando sem repetir argumentos
|
|
276
|
+
|
|
277
|
+
`reconfigure()` recria a configuração com os mesmos argumentos do construtor, aplicando apenas os overrides informados. Vale tanto para campos quanto para os argumentos de configuração do pydantic-settings (`_env_file`, `_secrets_dir`, `_case_sensitive`, `_env_prefix`, ...):
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
base = JaylogSettings(app_name='meu-bot', log_level='DEBUG')
|
|
281
|
+
|
|
282
|
+
homolog = base.reconfigure(_env_file='homolog.env')
|
|
283
|
+
prod = base.reconfigure(_env_file='producao.env', log_level='WARNING')
|
|
284
|
+
# app_name preservado nos dois; só o que foi informado muda
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
É sobre esse mecanismo que o `reload_secrets()` é construído — ele é só um `reconfigure(_secrets_dir=...)` com validação.
|
|
@@ -20,7 +20,8 @@ As variáveis usam o prefixo `JAYLOG_`. Podem ser definidas no ambiente do siste
|
|
|
20
20
|
| `JAYLOG_LOG_MAX_BYTES` | NÃO | `5242880` | Tamanho máximo do arquivo de log antes de rotacionar (bytes) |
|
|
21
21
|
| `JAYLOG_LOG_BACKUP_COUNT` | NÃO | `5` | Quantidade de arquivos de backup mantidos após rotação |
|
|
22
22
|
| `JAYLOG_LOG_RETENTION_DAYS` | NÃO | `7` | Dias para manter arquivos de log antigos |
|
|
23
|
-
| `JAYLOG_LOG_CONSOLE_ENABLED` | NÃO | `true` | Habilita saída
|
|
23
|
+
| `JAYLOG_LOG_CONSOLE_ENABLED` | NÃO | `true` | Habilita a saída de log no console (`true`/`false`) |
|
|
24
|
+
| `JAYLOG_LOG_CONSOLE_COLOR` | NÃO | `null` | Força (`true`) ou desliga (`false`) as cores no console. Se omitido, detecta automaticamente o suporte do terminal |
|
|
24
25
|
| `JAYLOG_LOG_HTTP_TIMEOUT` | NÃO | `5.0` | Timeout em segundos para o envio HTTP |
|
|
25
26
|
| `JAYLOG_LOG_HTTP_ENDPOINT` | NÃO | `null` | URL do endpoint que receberá os logs |
|
|
26
27
|
| `JAYLOG_LOG_HTTP_API_KEY` | NÃO | `null` | Chave de autenticação enviada no header `x-api-key` |
|
|
@@ -132,6 +133,33 @@ billing_logger.info("Fatura emitida")
|
|
|
132
133
|
|
|
133
134
|
|
|
134
135
|
|
|
136
|
+
## Cores no console
|
|
137
|
+
|
|
138
|
+
As cores são ligadas automaticamente quando o terminal suporta ANSI. A detecção cobre:
|
|
139
|
+
|
|
140
|
+
- **Windows**: o modo *virtual terminal* do console é habilitado em tempo de execução, o que faz as cores funcionarem também no `cmd.exe`/PowerShell rodando no console legado (`conhost`) do Windows 10 — antes só saía colorido no Windows Terminal.
|
|
141
|
+
- **Saída redirecionada** (`python main.py > saida.txt`, pipes, serviços sem console): as cores são desligadas, para o arquivo não ficar com lixo do tipo `←[32m`.
|
|
142
|
+
- **Consoles antigos** que não suportam ANSI de jeito nenhum: o log sai em texto limpo, com o mesmo alinhamento.
|
|
143
|
+
- As convenções `NO_COLOR` e `FORCE_COLOR` são respeitadas.
|
|
144
|
+
|
|
145
|
+
Para forçar um comportamento, use `JAYLOG_LOG_CONSOLE_COLOR`:
|
|
146
|
+
|
|
147
|
+
__*.env.logging*__
|
|
148
|
+
```env
|
|
149
|
+
JAYLOG_APP_NAME=meu-bot
|
|
150
|
+
JAYLOG_LOG_CONSOLE_COLOR=false
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Ou direto no código:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
configure(JaylogSettings(app_name="meu-bot", log_console_color=False))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> [!TIP]
|
|
160
|
+
> `JAYLOG_LOG_CONSOLE_COLOR=true` força as cores mesmo com a saída redirecionada — útil quando o log é consumido por uma ferramenta que entende ANSI (ex: `... | less -R`).
|
|
161
|
+
|
|
162
|
+
|
|
135
163
|
## Alterando Caminho padrão do .env
|
|
136
164
|
|
|
137
165
|
__*development.env*__
|
|
@@ -213,3 +241,28 @@ configure(JaylogSettings(_env_file='prodution.env').reload_secrets())
|
|
|
213
241
|
logger = get_logger()
|
|
214
242
|
|
|
215
243
|
logger.info("Mensagem de log")
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`reload_secrets()` devolve uma **nova** instância preservando tudo que foi passado explicitamente no construtor, então dá para combinar configuração em código com secrets em disco:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
settings = JaylogSettings(app_name='meu-bot', log_level='DEBUG').reload_secrets()
|
|
250
|
+
configure(settings) # app_name e log_level mantidos; endpoint/api_key vêm dos secrets
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
> [!NOTE]
|
|
254
|
+
> Valores passados no construtor têm prioridade sobre os secrets: um `log_http_api_key='...'` definido em código **não** é sobrescrito pelo arquivo em `secrets/`.
|
|
255
|
+
|
|
256
|
+
## Reconfigurando sem repetir argumentos
|
|
257
|
+
|
|
258
|
+
`reconfigure()` recria a configuração com os mesmos argumentos do construtor, aplicando apenas os overrides informados. Vale tanto para campos quanto para os argumentos de configuração do pydantic-settings (`_env_file`, `_secrets_dir`, `_case_sensitive`, `_env_prefix`, ...):
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
base = JaylogSettings(app_name='meu-bot', log_level='DEBUG')
|
|
262
|
+
|
|
263
|
+
homolog = base.reconfigure(_env_file='homolog.env')
|
|
264
|
+
prod = base.reconfigure(_env_file='producao.env', log_level='WARNING')
|
|
265
|
+
# app_name preservado nos dois; só o que foi informado muda
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
É sobre esse mecanismo que o `reload_secrets()` é construído — ele é só um `reconfigure(_secrets_dir=...)` com validação.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Detecção e habilitação de cores ANSI no console.
|
|
3
|
+
|
|
4
|
+
O console legado do Windows (``conhost.exe``, usado pelo `cmd.exe` do Windows 10
|
|
5
|
+
quando não está hospedado no Windows Terminal) não interpreta sequências ANSI
|
|
6
|
+
por padrão: o flag ``ENABLE_VIRTUAL_TERMINAL_PROCESSING`` existe desde o build
|
|
7
|
+
10586, mas vem desligado. Por isso o mesmo log que sai colorido no Windows 11 +
|
|
8
|
+
Windows Terminal aparece com lixo (``←[32m``) — ou sem cor — no Windows 10 + cmd.
|
|
9
|
+
|
|
10
|
+
Este módulo resolve os dois lados do problema:
|
|
11
|
+
|
|
12
|
+
* liga o modo VT no handle do console quando o Windows suporta;
|
|
13
|
+
* quando não suporta (ou a saída foi redirecionada para arquivo/pipe),
|
|
14
|
+
desabilita as cores para que o log saia em texto limpo, sem escape codes.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from typing import IO
|
|
20
|
+
|
|
21
|
+
GREEN = "\033[32m"
|
|
22
|
+
BLUE = "\033[34m"
|
|
23
|
+
PURPLE = "\033[35m"
|
|
24
|
+
YELLOW = "\033[33m"
|
|
25
|
+
RED = "\033[31m"
|
|
26
|
+
RESET = "\033[0m"
|
|
27
|
+
|
|
28
|
+
_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
29
|
+
_STD_HANDLE_BY_FILENO = {1: -11, 2: -12} # STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
|
|
30
|
+
|
|
31
|
+
# handles já processados, para não chamar SetConsoleMode a cada logger criado
|
|
32
|
+
_vt_cache: dict[int, bool] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _env_flag(name: str) -> bool | None:
|
|
36
|
+
"""Lê uma env var no padrão NO_COLOR/FORCE_COLOR: ausente/vazia = indefinido."""
|
|
37
|
+
value = os.environ.get(name)
|
|
38
|
+
if value is None or value.strip() == "":
|
|
39
|
+
return None
|
|
40
|
+
return value.strip().lower() not in {"0", "false", "no", "off"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _enable_windows_vt(stream: IO[str]) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Liga ``ENABLE_VIRTUAL_TERMINAL_PROCESSING`` no console do Windows.
|
|
46
|
+
|
|
47
|
+
Retorna ``True`` se o stream passa a interpretar ANSI (ou já interpretava).
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
fileno = stream.fileno()
|
|
51
|
+
except (AttributeError, OSError, ValueError):
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
handle_id = _STD_HANDLE_BY_FILENO.get(fileno)
|
|
55
|
+
if handle_id is None:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if fileno in _vt_cache:
|
|
59
|
+
return _vt_cache[fileno]
|
|
60
|
+
|
|
61
|
+
enabled = False
|
|
62
|
+
try:
|
|
63
|
+
import ctypes
|
|
64
|
+
from ctypes import wintypes
|
|
65
|
+
|
|
66
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
67
|
+
|
|
68
|
+
kernel32.GetStdHandle.argtypes = [wintypes.DWORD]
|
|
69
|
+
kernel32.GetStdHandle.restype = wintypes.HANDLE
|
|
70
|
+
kernel32.GetConsoleMode.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
|
|
71
|
+
kernel32.GetConsoleMode.restype = wintypes.BOOL
|
|
72
|
+
kernel32.SetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
73
|
+
kernel32.SetConsoleMode.restype = wintypes.BOOL
|
|
74
|
+
|
|
75
|
+
handle = kernel32.GetStdHandle(wintypes.DWORD(handle_id & 0xFFFFFFFF))
|
|
76
|
+
invalid_handle = ctypes.c_void_p(-1).value
|
|
77
|
+
if not handle or handle == invalid_handle:
|
|
78
|
+
enabled = False
|
|
79
|
+
else:
|
|
80
|
+
mode = wintypes.DWORD()
|
|
81
|
+
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
82
|
+
# não é um console de verdade (pipe, arquivo, serviço sem console)
|
|
83
|
+
enabled = False
|
|
84
|
+
elif mode.value & _ENABLE_VIRTUAL_TERMINAL_PROCESSING:
|
|
85
|
+
enabled = True
|
|
86
|
+
else:
|
|
87
|
+
enabled = bool(
|
|
88
|
+
kernel32.SetConsoleMode(
|
|
89
|
+
handle, mode.value | _ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
except Exception:
|
|
93
|
+
# build antigo do Windows, ctypes indisponível, etc.
|
|
94
|
+
enabled = False
|
|
95
|
+
|
|
96
|
+
_vt_cache[fileno] = enabled
|
|
97
|
+
return enabled
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def supports_color(stream: IO[str] | None = None, force: bool | None = None) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Decide se ``stream`` deve receber sequências ANSI.
|
|
103
|
+
|
|
104
|
+
``force=True``/``False`` sobrepõe a detecção automática (vem de
|
|
105
|
+
``JAYLOG_LOG_CONSOLE_COLOR``); mesmo com ``force=True`` o modo VT do Windows
|
|
106
|
+
ainda é ligado, senão o "forçar" só produziria lixo na tela.
|
|
107
|
+
|
|
108
|
+
Sem override, respeita as convenções ``NO_COLOR`` e ``FORCE_COLOR``, exige
|
|
109
|
+
um TTY e, no Windows, exige que o modo VT tenha sido habilitado com sucesso.
|
|
110
|
+
"""
|
|
111
|
+
stream = stream if stream is not None else sys.stdout
|
|
112
|
+
|
|
113
|
+
if sys.platform == "win32":
|
|
114
|
+
vt_enabled = _enable_windows_vt(stream)
|
|
115
|
+
else:
|
|
116
|
+
vt_enabled = True
|
|
117
|
+
|
|
118
|
+
if force is not None:
|
|
119
|
+
return force
|
|
120
|
+
|
|
121
|
+
if _env_flag("NO_COLOR"):
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
forced_by_env = _env_flag("FORCE_COLOR")
|
|
125
|
+
if forced_by_env is not None:
|
|
126
|
+
return forced_by_env
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if not stream.isatty():
|
|
130
|
+
return False
|
|
131
|
+
except (AttributeError, ValueError):
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
if os.environ.get("TERM", "").strip().lower() in {"dumb", "unknown"}:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
return vt_enabled
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import IO
|
|
5
|
+
|
|
6
|
+
from jaylog.colors import BLUE, GREEN, PURPLE, RED, RESET, YELLOW, supports_color
|
|
7
|
+
from jaylog.formatters import build_log_entry_dict
|
|
8
|
+
|
|
9
|
+
_LEVEL_COLORS = {
|
|
10
|
+
"DEBUG": PURPLE,
|
|
11
|
+
"INFO": BLUE,
|
|
12
|
+
"WARNING": YELLOW,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _level_color(level: str) -> str:
|
|
17
|
+
return _LEVEL_COLORS.get(level, RED)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConsoleFormatter(logging.Formatter):
|
|
21
|
+
"""
|
|
22
|
+
Formata o log para o console.
|
|
23
|
+
|
|
24
|
+
Quando ``use_color`` é falso (console legado do Windows sem suporte a ANSI,
|
|
25
|
+
saída redirecionada para arquivo/pipe, ``NO_COLOR`` etc.) a mesma linha é
|
|
26
|
+
emitida sem nenhum escape code, em vez de sujar a tela com ``←[32m``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, show_service: bool = True, use_color: bool = True) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.show_service = show_service
|
|
32
|
+
self.use_color = use_color
|
|
33
|
+
|
|
34
|
+
def _paint(self, text: str, color: str) -> str:
|
|
35
|
+
if not self.use_color:
|
|
36
|
+
return text
|
|
37
|
+
return f"{color}{text}{RESET}"
|
|
38
|
+
|
|
39
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
40
|
+
entry = build_log_entry_dict(record)
|
|
41
|
+
log_timestamp = datetime.fromisoformat(entry["log_timestamp"]).astimezone().strftime("%d/%m/%Y %X")
|
|
42
|
+
log_level = f'[{entry["log_level"]}]'
|
|
43
|
+
colored_timestamp = self._paint(log_timestamp, GREEN)
|
|
44
|
+
colored_level = self._paint(log_level.ljust(11), _level_color(entry['log_level']))
|
|
45
|
+
service_segment = f'[{entry["service"]}] | ' if self.show_service else ''
|
|
46
|
+
return f"{colored_timestamp} {colored_level} | {service_segment}{entry['log_message']}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class JaylogConsoleHandler(logging.StreamHandler):
|
|
50
|
+
"""
|
|
51
|
+
Handler de console.
|
|
52
|
+
|
|
53
|
+
``color=None`` (padrão) detecta automaticamente o suporte a ANSI do stream —
|
|
54
|
+
ligando o modo VT do console do Windows quando disponível. ``True``/``False``
|
|
55
|
+
forçam o comportamento.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
show_service: bool = True,
|
|
61
|
+
color: bool | None = None,
|
|
62
|
+
stream: IO[str] | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
stream = stream if stream is not None else sys.stdout
|
|
65
|
+
super().__init__(stream=stream)
|
|
66
|
+
self.setFormatter(
|
|
67
|
+
ConsoleFormatter(
|
|
68
|
+
show_service=show_service,
|
|
69
|
+
use_color=supports_color(stream, force=color),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
@@ -137,7 +137,10 @@ def _build_logger(name: str | None) -> logging.Logger:
|
|
|
137
137
|
downstream.append(file_handler)
|
|
138
138
|
|
|
139
139
|
if settings.log_console_enabled:
|
|
140
|
-
console_handler = JaylogConsoleHandler(
|
|
140
|
+
console_handler = JaylogConsoleHandler(
|
|
141
|
+
show_service=show_service,
|
|
142
|
+
color=settings.log_console_color,
|
|
143
|
+
)
|
|
141
144
|
console_handler.setLevel(settings.log_level)
|
|
142
145
|
downstream.append(console_handler)
|
|
143
146
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import computed_field, field_validator
|
|
4
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
5
|
+
|
|
6
|
+
from jaylog.formatters import _HOST_USERNAME, _HOSTNAME
|
|
7
|
+
|
|
8
|
+
# Compat: na assinatura anterior de `JaylogSettings.__init__` estes eram os dois
|
|
9
|
+
# primeiros parâmetros posicionais. Pode ser removido quando não houver mais
|
|
10
|
+
# chamadas posicionais em uso.
|
|
11
|
+
_LEGACY_POSITIONAL_ARGS = ('_env_file', '_secrets_dir')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JaylogSettings(BaseSettings):
|
|
15
|
+
model_config = SettingsConfigDict(
|
|
16
|
+
env_prefix="JAYLOG_",
|
|
17
|
+
env_file=('.env.logging', '.env'),
|
|
18
|
+
env_file_encoding='utf-8',
|
|
19
|
+
secrets_dir='secrets',
|
|
20
|
+
extra="ignore"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Tudo que foi recebido no construtor: os campos normais (`app_name=...`) e
|
|
24
|
+
# os argumentos de configuração do pydantic-settings (`_env_file`,
|
|
25
|
+
# `_secrets_dir`, `_case_sensitive`, ... — são 28 e variam por versão).
|
|
26
|
+
# Guardado inteiro para que `reconfigure()` recrie a instância sem perder
|
|
27
|
+
# nada, sem precisar conhecer os nomes um a um.
|
|
28
|
+
_init_values: dict = {}
|
|
29
|
+
|
|
30
|
+
def __init__(self, *args, **values) -> None:
|
|
31
|
+
if len(args) > len(_LEGACY_POSITIONAL_ARGS):
|
|
32
|
+
raise TypeError(
|
|
33
|
+
f'{type(self).__name__}() aceita no máximo '
|
|
34
|
+
f'{len(_LEGACY_POSITIONAL_ARGS)} argumentos posicionais'
|
|
35
|
+
)
|
|
36
|
+
for name, value in zip(_LEGACY_POSITIONAL_ARGS, args):
|
|
37
|
+
if name in values:
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f'{type(self).__name__}() recebeu dois valores para {name!r}'
|
|
40
|
+
)
|
|
41
|
+
values[name] = value
|
|
42
|
+
|
|
43
|
+
super().__init__(**values)
|
|
44
|
+
object.__setattr__(self, '_init_values', dict(values))
|
|
45
|
+
|
|
46
|
+
def _settings_arg(self, name: str):
|
|
47
|
+
"""
|
|
48
|
+
Valor efetivo de um argumento ``_``-prefixado do pydantic-settings:
|
|
49
|
+
o que foi passado no construtor ou, na ausência dele, o que está no
|
|
50
|
+
``model_config`` (``_env_file`` -> ``env_file``).
|
|
51
|
+
"""
|
|
52
|
+
if name in self._init_values:
|
|
53
|
+
return self._init_values[name]
|
|
54
|
+
return self.model_config.get(name.lstrip('_'))
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def _env_file(self):
|
|
58
|
+
return self._settings_arg('_env_file')
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def _secrets_dir(self):
|
|
62
|
+
return self._settings_arg('_secrets_dir')
|
|
63
|
+
|
|
64
|
+
def reconfigure(self, **overrides) -> "JaylogSettings":
|
|
65
|
+
"""
|
|
66
|
+
Recria a instância com os mesmos argumentos do construtor, aplicando
|
|
67
|
+
``overrides`` por cima.
|
|
68
|
+
|
|
69
|
+
Serve para reler a configuração mudando um detalhe sem repetir (nem
|
|
70
|
+
perder) o resto::
|
|
71
|
+
|
|
72
|
+
settings = JaylogSettings(app_name='meu-bot', log_level='DEBUG')
|
|
73
|
+
outra = settings.reconfigure(_env_file='producao.env')
|
|
74
|
+
# app_name e log_level preservados, .env trocado
|
|
75
|
+
|
|
76
|
+
Vale para qualquer argumento aceito por ``BaseSettings`` — inclusive os
|
|
77
|
+
que esta classe não conhece explicitamente.
|
|
78
|
+
"""
|
|
79
|
+
return type(self)(**{**self._init_values, **overrides})
|
|
80
|
+
|
|
81
|
+
# App identity
|
|
82
|
+
app_name: str
|
|
83
|
+
log_dir: Path | None = None
|
|
84
|
+
|
|
85
|
+
secrets_dir: Path | None = None
|
|
86
|
+
|
|
87
|
+
# File handler
|
|
88
|
+
log_level: str = "INFO"
|
|
89
|
+
log_max_bytes: int = 5 * 1024 * 1024 # 5 MB
|
|
90
|
+
log_backup_count: int = 5
|
|
91
|
+
log_retention_days: int = 7
|
|
92
|
+
|
|
93
|
+
# HTTP handler
|
|
94
|
+
log_http_endpoint: str | None = None
|
|
95
|
+
log_http_api_key: str | None = None
|
|
96
|
+
log_http_timeout: float = 5.0
|
|
97
|
+
log_http_proxy: str | None = None
|
|
98
|
+
|
|
99
|
+
# Console handler — desativar com JAYLOG_LOG_CONSOLE_ENABLED=false
|
|
100
|
+
log_console_enabled: bool = True
|
|
101
|
+
|
|
102
|
+
# Cores ANSI no console. `None` (padrão) detecta o suporte do terminal;
|
|
103
|
+
# true/false forçam. Ver JAYLOG_LOG_CONSOLE_COLOR no README.
|
|
104
|
+
log_console_color: bool | None = None
|
|
105
|
+
|
|
106
|
+
# Screenshot (log_img field) — desativar com JAYLOG_LOG_SCREENSHOT_ENABLED=false
|
|
107
|
+
log_screenshot_enabled: bool = False
|
|
108
|
+
|
|
109
|
+
@field_validator("log_dir", mode="after")
|
|
110
|
+
@classmethod
|
|
111
|
+
def validate_log_dir(cls, v: Path | None) -> Path | None:
|
|
112
|
+
if v is not None and v.exists() and not v.is_dir():
|
|
113
|
+
raise ValueError(f"JAYLOG_LOG_DIR '{v}' exists but is not a directory")
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
@computed_field
|
|
117
|
+
@property
|
|
118
|
+
def log_filename(self) -> Path | None:
|
|
119
|
+
if self.log_dir is None:
|
|
120
|
+
return None
|
|
121
|
+
return Path(f"{self.app_name}_{_HOSTNAME}_{_HOST_USERNAME}.log")
|
|
122
|
+
|
|
123
|
+
def reload_secrets(self) -> "JaylogSettings":
|
|
124
|
+
"""
|
|
125
|
+
Recarrega a configuração usando o diretório apontado por
|
|
126
|
+
``JAYLOG_SECRETS_DIR``.
|
|
127
|
+
|
|
128
|
+
Necessário porque o diretório de secrets só é conhecido depois que o
|
|
129
|
+
``.env`` é lido — ou seja, tarde demais para o pydantic-settings usá-lo
|
|
130
|
+
como fonte na primeira instanciação.
|
|
131
|
+
|
|
132
|
+
Retorna uma **nova** instância preservando tudo que foi passado
|
|
133
|
+
explicitamente no construtor, para que isto continue funcionando::
|
|
134
|
+
|
|
135
|
+
JaylogSettings(app_name='meu-bot').reload_secrets()
|
|
136
|
+
|
|
137
|
+
Os valores explícitos continuam tendo prioridade sobre os secrets: um
|
|
138
|
+
``log_http_api_key`` passado no código não é sobrescrito pelo arquivo.
|
|
139
|
+
"""
|
|
140
|
+
if self.secrets_dir is not None:
|
|
141
|
+
if not self.secrets_dir.is_dir():
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"JAYLOG_SECRETS_DIR '{self.secrets_dir}' não é um diretório válido"
|
|
144
|
+
)
|
|
145
|
+
return self.reconfigure(_secrets_dir=self.secrets_dir)
|
|
146
|
+
|
|
147
|
+
if '_secrets_dir' in self._init_values:
|
|
148
|
+
raise SyntaxError(
|
|
149
|
+
'Não é possivel usar `reload_secrets` caso o valor de _secrets_dir foi sobrescrito'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# nenhum JAYLOG_SECRETS_DIR definido: relê mantendo o diretório atual,
|
|
153
|
+
# em vez de desligar a leitura de secrets
|
|
154
|
+
return self.reconfigure()
|
|
@@ -1,43 +0,0 @@
|
|
|
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"]).astimezone().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))
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
from pydantic import computed_field, field_validator
|
|
5
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
-
|
|
7
|
-
from jaylog.formatters import _HOSTNAME, _HOST_USERNAME
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class JaylogSettings(BaseSettings):
|
|
11
|
-
model_config = SettingsConfigDict(
|
|
12
|
-
env_prefix="JAYLOG_",
|
|
13
|
-
env_file=('.env.logging', '.env'),
|
|
14
|
-
env_file_encoding='utf-8',
|
|
15
|
-
secrets_dir='secrets',
|
|
16
|
-
extra="ignore"
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
_env_file: str | tuple | None = None
|
|
20
|
-
_secrets_dir: str | None = None
|
|
21
|
-
|
|
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)
|
|
29
|
-
object.__setattr__(self, '_env_file', _env_file)
|
|
30
|
-
object.__setattr__(self, '_secrets_dir', _secrets_dir)
|
|
31
|
-
|
|
32
|
-
# App identity
|
|
33
|
-
app_name: str
|
|
34
|
-
log_dir: Path | None = None
|
|
35
|
-
|
|
36
|
-
secrets_dir: Path | None = None
|
|
37
|
-
|
|
38
|
-
# File handler
|
|
39
|
-
log_level: str = "INFO"
|
|
40
|
-
log_max_bytes: int = 5 * 1024 * 1024 # 5 MB
|
|
41
|
-
log_backup_count: int = 5
|
|
42
|
-
log_retention_days: int = 7
|
|
43
|
-
|
|
44
|
-
# HTTP handler
|
|
45
|
-
log_http_endpoint: Optional[str] = None
|
|
46
|
-
log_http_api_key: Optional[str] = None
|
|
47
|
-
log_http_timeout: float = 5.0
|
|
48
|
-
log_http_proxy: Optional[str] = None
|
|
49
|
-
|
|
50
|
-
# Console handler — desativar com JAYLOG_LOG_CONSOLE_ENABLED=false
|
|
51
|
-
log_console_enabled: bool = True
|
|
52
|
-
|
|
53
|
-
# Screenshot (log_img field) — desativar com JAYLOG_LOG_SCREENSHOT_ENABLED=false
|
|
54
|
-
log_screenshot_enabled: bool = False
|
|
55
|
-
|
|
56
|
-
@field_validator("log_dir", mode="after")
|
|
57
|
-
@classmethod
|
|
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():
|
|
60
|
-
raise ValueError(f"JAYLOG_LOG_DIR '{v}' exists but is not a directory")
|
|
61
|
-
return v
|
|
62
|
-
|
|
63
|
-
@computed_field
|
|
64
|
-
@property
|
|
65
|
-
def log_filename(self) -> Path | None:
|
|
66
|
-
if self.log_dir is None:
|
|
67
|
-
return None
|
|
68
|
-
return Path(f"{self.app_name}_{_HOSTNAME}_{_HOST_USERNAME}.log")
|
|
69
|
-
|
|
70
|
-
def reload_secrets(self):
|
|
71
|
-
if self.secrets_dir:
|
|
72
|
-
if not self.secrets_dir.exists() or not self.secrets_dir.is_dir:
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return JaylogSettings(
|
|
83
|
-
_env_file=self._env_file,
|
|
84
|
-
_secrets_dir=self.secrets_dir)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|