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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: jaylog
3
- Version: 0.2.6
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 colorida no console (`true`/`false`) |
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 colorida no console (`true`/`false`) |
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jaylog"
3
- version = "0.2.6"
3
+ version = "0.2.8"
4
4
  description = "Customized Python logging library with file rotation and HTTP forwarding"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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(show_service=show_service)
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