jaylog 0.2.8__tar.gz → 0.3.0a0__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.8 → jaylog-0.3.0a0}/PKG-INFO +46 -13
- {jaylog-0.2.8 → jaylog-0.3.0a0}/README.md +45 -12
- {jaylog-0.2.8 → jaylog-0.3.0a0}/pyproject.toml +19 -1
- jaylog-0.3.0a0/src/jaylog/__init__.py +37 -0
- jaylog-0.3.0a0/src/jaylog/_compat.py +29 -0
- jaylog-0.3.0a0/src/jaylog/_version.py +19 -0
- jaylog-0.3.0a0/src/jaylog/context.py +93 -0
- jaylog-0.3.0a0/src/jaylog/endpoints.py +26 -0
- jaylog-0.3.0a0/src/jaylog/formatters.py +33 -0
- {jaylog-0.2.8 → jaylog-0.3.0a0}/src/jaylog/handlers/console_handler.py +8 -6
- jaylog-0.3.0a0/src/jaylog/handlers/http_handler.py +102 -0
- jaylog-0.3.0a0/src/jaylog/host/__init__.py +6 -0
- jaylog-0.3.0a0/src/jaylog/host/_safe.py +56 -0
- jaylog-0.3.0a0/src/jaylog/host/collectors.py +130 -0
- jaylog-0.3.0a0/src/jaylog/host/detect_execution.py +199 -0
- jaylog-0.3.0a0/src/jaylog/host/detect_git.py +206 -0
- jaylog-0.3.0a0/src/jaylog/host/detect_python.py +28 -0
- jaylog-0.3.0a0/src/jaylog/host/detect_venv.py +79 -0
- jaylog-0.3.0a0/src/jaylog/host/identity.py +53 -0
- jaylog-0.3.0a0/src/jaylog/host/models.py +91 -0
- jaylog-0.3.0a0/src/jaylog/host/payload.py +29 -0
- jaylog-0.3.0a0/src/jaylog/host/reporter.py +295 -0
- jaylog-0.3.0a0/src/jaylog/host/win32.py +148 -0
- {jaylog-0.2.8 → jaylog-0.3.0a0}/src/jaylog/logger.py +128 -18
- jaylog-0.3.0a0/src/jaylog/runtime.py +21 -0
- jaylog-0.3.0a0/src/jaylog/screenshot.py +102 -0
- {jaylog-0.2.8 → jaylog-0.3.0a0}/src/jaylog/settings.py +70 -19
- jaylog-0.2.8/src/jaylog/__init__.py +0 -5
- jaylog-0.2.8/src/jaylog/formatters.py +0 -118
- jaylog-0.2.8/src/jaylog/handlers/http_handler.py +0 -78
- jaylog-0.2.8/src/jaylog/models.py +0 -17
- {jaylog-0.2.8 → jaylog-0.3.0a0}/src/jaylog/colors.py +0 -0
- {jaylog-0.2.8 → jaylog-0.3.0a0}/src/jaylog/filters.py +0 -0
- {jaylog-0.2.8 → jaylog-0.3.0a0}/src/jaylog/handlers/__init__.py +0 -0
- {jaylog-0.2.8 → jaylog-0.3.0a0}/src/jaylog/handlers/file_handler.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: jaylog
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0a0
|
|
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>
|
|
@@ -45,7 +45,16 @@ As variáveis usam o prefixo `JAYLOG_`. Podem ser definidas no ambiente do siste
|
|
|
45
45
|
| `JAYLOG_LOG_HTTP_ENDPOINT` | NÃO | `null` | URL do endpoint que receberá os logs |
|
|
46
46
|
| `JAYLOG_LOG_HTTP_API_KEY` | NÃO | `null` | Chave de autenticação enviada no header `x-api-key` |
|
|
47
47
|
| `JAYLOG_LOG_HTTP_PROXY` | NÃO | `null` | URL do proxy para o envio HTTP (ex: `http:\\user:password@server:port`) |
|
|
48
|
+
| `JAYLOG_LOG_HTTP_VERIFY` | NÃO | `false` | `true` ou caminho para o bundle de CA usado para validar TLS |
|
|
48
49
|
| `JAYLOG_LOG_SCREENSHOT_ENABLED` | NÃO | `false` | Captura screenshot no momento do log (`true`/`false`, apenas Windows) |
|
|
50
|
+
| `JAYLOG_HOST_REPORT_ENABLED` | NÃO | `true` | Envia uma fotografia do ambiente no startup |
|
|
51
|
+
| `JAYLOG_HOST_HTTP_ENDPOINT` | NÃO | derivado | Override para o endpoint de host; por padrão `/logs/add` vira `/logs/host` |
|
|
52
|
+
| `JAYLOG_HOST_REPORT_TIMEOUT` | NÃO | `2 × HTTP_TIMEOUT` | Timeout do POST de ambiente |
|
|
53
|
+
| `JAYLOG_HOST_GIT_ENABLED` | NÃO | `true` | Coleta metadados do repositório Git |
|
|
54
|
+
| `JAYLOG_HOST_GIT_DIRTY_ENABLED` | NÃO | `true` | Coleta se há arquivos alterados |
|
|
55
|
+
| `JAYLOG_HOST_GIT_REMOTE_ENABLED`| NÃO | `true` | Coleta a URL remota sem credenciais |
|
|
56
|
+
| `JAYLOG_HOST_GIT_TIMEOUT` | NÃO | `3.0` | Timeout, em segundos, de cada chamada ao Git |
|
|
57
|
+
| `JAYLOG_HOST_GIT_DIR` | NÃO | `null` | Diretório inicial para localizar o repositório |
|
|
49
58
|
|
|
50
59
|
|
|
51
60
|
## Como usar?
|
|
@@ -121,6 +130,7 @@ from jaylog import get_logger
|
|
|
121
130
|
|
|
122
131
|
logger = get_logger()
|
|
123
132
|
|
|
133
|
+
|
|
124
134
|
def parse_csv():
|
|
125
135
|
logger.info("Múltiplos Arquivos - parse.py")
|
|
126
136
|
```
|
|
@@ -138,12 +148,12 @@ __*main.py*__
|
|
|
138
148
|
```python
|
|
139
149
|
from jaylog import JaylogSettings, configure, get_logger
|
|
140
150
|
|
|
141
|
-
settings_order
|
|
151
|
+
settings_order = JaylogSettings(app_name="ORDER-PROCESSOR")
|
|
142
152
|
settings_billing = JaylogSettings(app_name="BILLING")
|
|
143
153
|
|
|
144
154
|
configure([settings_order, settings_billing])
|
|
145
155
|
|
|
146
|
-
logger
|
|
156
|
+
logger = get_logger("ORDER-PROCESSOR") # ou get_logger() — retorna o primeiro registrado
|
|
147
157
|
billing_logger = get_logger("BILLING")
|
|
148
158
|
|
|
149
159
|
logger.info("Pedido recebido")
|
|
@@ -191,7 +201,7 @@ __*main.py*__
|
|
|
191
201
|
```python
|
|
192
202
|
from jaylog import JaylogSettings, configure, get_logger
|
|
193
203
|
|
|
194
|
-
configure(JaylogSettings(_env_file=
|
|
204
|
+
configure(JaylogSettings(_env_file="development.env"))
|
|
195
205
|
logger = get_logger()
|
|
196
206
|
|
|
197
207
|
logger.info("Alterando Caminho padrão do .env")
|
|
@@ -200,6 +210,32 @@ logger.info("Alterando Caminho padrão do .env")
|
|
|
200
210
|
|
|
201
211
|
## Preparando para produção
|
|
202
212
|
|
|
213
|
+
## Registro do ambiente
|
|
214
|
+
|
|
215
|
+
Na versão 0.3, `configure()` inicia em segundo plano um único `POST` JSON para
|
|
216
|
+
o endpoint de host por serviço. O corpo dos logs continua compatível com a
|
|
217
|
+
linha 0.2.x; apenas os headers `x-jaylog-protocol: 2` e `x-jaylog-run-id` são
|
|
218
|
+
adicionados. O `run_id` permite ao backend ligar os logs à execução que os
|
|
219
|
+
produziu.
|
|
220
|
+
|
|
221
|
+
O endpoint é derivado automaticamente trocando o último segmento de
|
|
222
|
+
`JAYLOG_LOG_HTTP_ENDPOINT`: `https://api.example/logs/add` vira
|
|
223
|
+
`https://api.example/logs/host`. Use `JAYLOG_HOST_HTTP_ENDPOINT` somente quando
|
|
224
|
+
o backend publicar a rota em outro endereço.
|
|
225
|
+
|
|
226
|
+
O registro inclui sistema operacional, modo de execução, Python, virtualenv,
|
|
227
|
+
Git e diretórios de execução. A URL remota do Git tem sempre a credencial
|
|
228
|
+
embutida removida antes de sair da máquina. Falhas de coleta ou de rede não
|
|
229
|
+
interrompem a aplicação: o envio tenta novamente com backoff. Se o backend
|
|
230
|
+
aceitar um log mas devolver `x-jaylog-host-required: 1`, o jaylog reenvia o
|
|
231
|
+
registro de host com debounce de 30 segundos; o log já foi aceito e não é
|
|
232
|
+
reenviado.
|
|
233
|
+
|
|
234
|
+
`JAYLOG_LOG_HTTP_VERIFY=false` continua sendo o padrão desta versão para não
|
|
235
|
+
interromper instalações atrás de proxies corporativos. Para validar TLS, use
|
|
236
|
+
`JAYLOG_LOG_HTTP_VERIFY=true` ou informe o caminho do CA bundle corporativo.
|
|
237
|
+
O padrão passará a `true` na 0.4.0.
|
|
238
|
+
|
|
203
239
|
> [!IMPORTANT]
|
|
204
240
|
> **HTTP_ENDPOINT** e **HTTP_API_KEY** (opcionais) 📢
|
|
205
241
|
>
|
|
@@ -232,10 +268,7 @@ __*main.py*__
|
|
|
232
268
|
```python
|
|
233
269
|
from jaylog import JaylogSettings, configure, get_logger
|
|
234
270
|
|
|
235
|
-
configure(JaylogSettings(
|
|
236
|
-
_env_file='prodution.env',
|
|
237
|
-
_secrets_dir='/foo/bar/secrets/'
|
|
238
|
-
))
|
|
271
|
+
configure(JaylogSettings(_env_file="prodution.env", _secrets_dir="/foo/bar/secrets/"))
|
|
239
272
|
logger = get_logger()
|
|
240
273
|
|
|
241
274
|
logger.info("Mensagem de log")
|
|
@@ -256,7 +289,7 @@ from jaylog import JaylogSettings, configure, get_logger
|
|
|
256
289
|
|
|
257
290
|
# nesse caso é necessário usar a função de classe `reload_secrets`
|
|
258
291
|
# pois o diretorio dos secrets foi passado via variável de ambiente
|
|
259
|
-
configure(JaylogSettings(_env_file=
|
|
292
|
+
configure(JaylogSettings(_env_file="prodution.env").reload_secrets())
|
|
260
293
|
logger = get_logger()
|
|
261
294
|
|
|
262
295
|
logger.info("Mensagem de log")
|
|
@@ -265,7 +298,7 @@ logger.info("Mensagem de log")
|
|
|
265
298
|
`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
299
|
|
|
267
300
|
```python
|
|
268
|
-
settings = JaylogSettings(app_name=
|
|
301
|
+
settings = JaylogSettings(app_name="meu-bot", log_level="DEBUG").reload_secrets()
|
|
269
302
|
configure(settings) # app_name e log_level mantidos; endpoint/api_key vêm dos secrets
|
|
270
303
|
```
|
|
271
304
|
|
|
@@ -277,10 +310,10 @@ configure(settings) # app_name e log_level mantidos; endpoint/api_key vêm dos
|
|
|
277
310
|
`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
311
|
|
|
279
312
|
```python
|
|
280
|
-
base = JaylogSettings(app_name=
|
|
313
|
+
base = JaylogSettings(app_name="meu-bot", log_level="DEBUG")
|
|
281
314
|
|
|
282
|
-
homolog = base.reconfigure(_env_file=
|
|
283
|
-
prod
|
|
315
|
+
homolog = base.reconfigure(_env_file="homolog.env")
|
|
316
|
+
prod = base.reconfigure(_env_file="producao.env", log_level="WARNING")
|
|
284
317
|
# app_name preservado nos dois; só o que foi informado muda
|
|
285
318
|
```
|
|
286
319
|
|
|
@@ -26,7 +26,16 @@ As variáveis usam o prefixo `JAYLOG_`. Podem ser definidas no ambiente do siste
|
|
|
26
26
|
| `JAYLOG_LOG_HTTP_ENDPOINT` | NÃO | `null` | URL do endpoint que receberá os logs |
|
|
27
27
|
| `JAYLOG_LOG_HTTP_API_KEY` | NÃO | `null` | Chave de autenticação enviada no header `x-api-key` |
|
|
28
28
|
| `JAYLOG_LOG_HTTP_PROXY` | NÃO | `null` | URL do proxy para o envio HTTP (ex: `http:\\user:password@server:port`) |
|
|
29
|
+
| `JAYLOG_LOG_HTTP_VERIFY` | NÃO | `false` | `true` ou caminho para o bundle de CA usado para validar TLS |
|
|
29
30
|
| `JAYLOG_LOG_SCREENSHOT_ENABLED` | NÃO | `false` | Captura screenshot no momento do log (`true`/`false`, apenas Windows) |
|
|
31
|
+
| `JAYLOG_HOST_REPORT_ENABLED` | NÃO | `true` | Envia uma fotografia do ambiente no startup |
|
|
32
|
+
| `JAYLOG_HOST_HTTP_ENDPOINT` | NÃO | derivado | Override para o endpoint de host; por padrão `/logs/add` vira `/logs/host` |
|
|
33
|
+
| `JAYLOG_HOST_REPORT_TIMEOUT` | NÃO | `2 × HTTP_TIMEOUT` | Timeout do POST de ambiente |
|
|
34
|
+
| `JAYLOG_HOST_GIT_ENABLED` | NÃO | `true` | Coleta metadados do repositório Git |
|
|
35
|
+
| `JAYLOG_HOST_GIT_DIRTY_ENABLED` | NÃO | `true` | Coleta se há arquivos alterados |
|
|
36
|
+
| `JAYLOG_HOST_GIT_REMOTE_ENABLED`| NÃO | `true` | Coleta a URL remota sem credenciais |
|
|
37
|
+
| `JAYLOG_HOST_GIT_TIMEOUT` | NÃO | `3.0` | Timeout, em segundos, de cada chamada ao Git |
|
|
38
|
+
| `JAYLOG_HOST_GIT_DIR` | NÃO | `null` | Diretório inicial para localizar o repositório |
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
## Como usar?
|
|
@@ -102,6 +111,7 @@ from jaylog import get_logger
|
|
|
102
111
|
|
|
103
112
|
logger = get_logger()
|
|
104
113
|
|
|
114
|
+
|
|
105
115
|
def parse_csv():
|
|
106
116
|
logger.info("Múltiplos Arquivos - parse.py")
|
|
107
117
|
```
|
|
@@ -119,12 +129,12 @@ __*main.py*__
|
|
|
119
129
|
```python
|
|
120
130
|
from jaylog import JaylogSettings, configure, get_logger
|
|
121
131
|
|
|
122
|
-
settings_order
|
|
132
|
+
settings_order = JaylogSettings(app_name="ORDER-PROCESSOR")
|
|
123
133
|
settings_billing = JaylogSettings(app_name="BILLING")
|
|
124
134
|
|
|
125
135
|
configure([settings_order, settings_billing])
|
|
126
136
|
|
|
127
|
-
logger
|
|
137
|
+
logger = get_logger("ORDER-PROCESSOR") # ou get_logger() — retorna o primeiro registrado
|
|
128
138
|
billing_logger = get_logger("BILLING")
|
|
129
139
|
|
|
130
140
|
logger.info("Pedido recebido")
|
|
@@ -172,7 +182,7 @@ __*main.py*__
|
|
|
172
182
|
```python
|
|
173
183
|
from jaylog import JaylogSettings, configure, get_logger
|
|
174
184
|
|
|
175
|
-
configure(JaylogSettings(_env_file=
|
|
185
|
+
configure(JaylogSettings(_env_file="development.env"))
|
|
176
186
|
logger = get_logger()
|
|
177
187
|
|
|
178
188
|
logger.info("Alterando Caminho padrão do .env")
|
|
@@ -181,6 +191,32 @@ logger.info("Alterando Caminho padrão do .env")
|
|
|
181
191
|
|
|
182
192
|
## Preparando para produção
|
|
183
193
|
|
|
194
|
+
## Registro do ambiente
|
|
195
|
+
|
|
196
|
+
Na versão 0.3, `configure()` inicia em segundo plano um único `POST` JSON para
|
|
197
|
+
o endpoint de host por serviço. O corpo dos logs continua compatível com a
|
|
198
|
+
linha 0.2.x; apenas os headers `x-jaylog-protocol: 2` e `x-jaylog-run-id` são
|
|
199
|
+
adicionados. O `run_id` permite ao backend ligar os logs à execução que os
|
|
200
|
+
produziu.
|
|
201
|
+
|
|
202
|
+
O endpoint é derivado automaticamente trocando o último segmento de
|
|
203
|
+
`JAYLOG_LOG_HTTP_ENDPOINT`: `https://api.example/logs/add` vira
|
|
204
|
+
`https://api.example/logs/host`. Use `JAYLOG_HOST_HTTP_ENDPOINT` somente quando
|
|
205
|
+
o backend publicar a rota em outro endereço.
|
|
206
|
+
|
|
207
|
+
O registro inclui sistema operacional, modo de execução, Python, virtualenv,
|
|
208
|
+
Git e diretórios de execução. A URL remota do Git tem sempre a credencial
|
|
209
|
+
embutida removida antes de sair da máquina. Falhas de coleta ou de rede não
|
|
210
|
+
interrompem a aplicação: o envio tenta novamente com backoff. Se o backend
|
|
211
|
+
aceitar um log mas devolver `x-jaylog-host-required: 1`, o jaylog reenvia o
|
|
212
|
+
registro de host com debounce de 30 segundos; o log já foi aceito e não é
|
|
213
|
+
reenviado.
|
|
214
|
+
|
|
215
|
+
`JAYLOG_LOG_HTTP_VERIFY=false` continua sendo o padrão desta versão para não
|
|
216
|
+
interromper instalações atrás de proxies corporativos. Para validar TLS, use
|
|
217
|
+
`JAYLOG_LOG_HTTP_VERIFY=true` ou informe o caminho do CA bundle corporativo.
|
|
218
|
+
O padrão passará a `true` na 0.4.0.
|
|
219
|
+
|
|
184
220
|
> [!IMPORTANT]
|
|
185
221
|
> **HTTP_ENDPOINT** e **HTTP_API_KEY** (opcionais) 📢
|
|
186
222
|
>
|
|
@@ -213,10 +249,7 @@ __*main.py*__
|
|
|
213
249
|
```python
|
|
214
250
|
from jaylog import JaylogSettings, configure, get_logger
|
|
215
251
|
|
|
216
|
-
configure(JaylogSettings(
|
|
217
|
-
_env_file='prodution.env',
|
|
218
|
-
_secrets_dir='/foo/bar/secrets/'
|
|
219
|
-
))
|
|
252
|
+
configure(JaylogSettings(_env_file="prodution.env", _secrets_dir="/foo/bar/secrets/"))
|
|
220
253
|
logger = get_logger()
|
|
221
254
|
|
|
222
255
|
logger.info("Mensagem de log")
|
|
@@ -237,7 +270,7 @@ from jaylog import JaylogSettings, configure, get_logger
|
|
|
237
270
|
|
|
238
271
|
# nesse caso é necessário usar a função de classe `reload_secrets`
|
|
239
272
|
# pois o diretorio dos secrets foi passado via variável de ambiente
|
|
240
|
-
configure(JaylogSettings(_env_file=
|
|
273
|
+
configure(JaylogSettings(_env_file="prodution.env").reload_secrets())
|
|
241
274
|
logger = get_logger()
|
|
242
275
|
|
|
243
276
|
logger.info("Mensagem de log")
|
|
@@ -246,7 +279,7 @@ logger.info("Mensagem de log")
|
|
|
246
279
|
`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
280
|
|
|
248
281
|
```python
|
|
249
|
-
settings = JaylogSettings(app_name=
|
|
282
|
+
settings = JaylogSettings(app_name="meu-bot", log_level="DEBUG").reload_secrets()
|
|
250
283
|
configure(settings) # app_name e log_level mantidos; endpoint/api_key vêm dos secrets
|
|
251
284
|
```
|
|
252
285
|
|
|
@@ -258,10 +291,10 @@ configure(settings) # app_name e log_level mantidos; endpoint/api_key vêm dos
|
|
|
258
291
|
`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
292
|
|
|
260
293
|
```python
|
|
261
|
-
base = JaylogSettings(app_name=
|
|
294
|
+
base = JaylogSettings(app_name="meu-bot", log_level="DEBUG")
|
|
262
295
|
|
|
263
|
-
homolog = base.reconfigure(_env_file=
|
|
264
|
-
prod
|
|
296
|
+
homolog = base.reconfigure(_env_file="homolog.env")
|
|
297
|
+
prod = base.reconfigure(_env_file="producao.env", log_level="WARNING")
|
|
265
298
|
# app_name preservado nos dois; só o que foi informado muda
|
|
266
299
|
```
|
|
267
300
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "jaylog"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0a0"
|
|
4
4
|
description = "Customized Python logging library with file rotation and HTTP forwarding"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -26,6 +26,24 @@ dependencies = [
|
|
|
26
26
|
repository = "https://github.com/Gpocas/jaylog"
|
|
27
27
|
"Bug Tracker" = "https://github.com/Gpocas/jaylog/issues"
|
|
28
28
|
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8,<9",
|
|
32
|
+
"ruff>=0.6",
|
|
33
|
+
]
|
|
34
|
+
|
|
29
35
|
[build-system]
|
|
30
36
|
requires = ["uv_build>=0.10.8,<0.11.0"]
|
|
31
37
|
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
addopts = "-q"
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
|
45
|
+
src = ["src", "tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
49
|
+
ignore = ["E501"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from jaylog._version import PROTOCOL_VERSION, __version__
|
|
2
|
+
from jaylog.logger import configure, get_logger, shutdown
|
|
3
|
+
from jaylog.settings import JaylogSettings
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"configure",
|
|
7
|
+
"get_logger",
|
|
8
|
+
"shutdown",
|
|
9
|
+
"JaylogSettings",
|
|
10
|
+
"__version__",
|
|
11
|
+
"PROTOCOL_VERSION",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
_DEPRECATED = {"LogEntry"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def __getattr__(name: str):
|
|
18
|
+
# `jaylog.models.LogEntry` era código morto — nunca foi construído, e o tipo
|
|
19
|
+
# declarado nem batia com o que o handler HTTP envia. Fica acessível por um
|
|
20
|
+
# release para não quebrar `from jaylog import LogEntry` em código de bot.
|
|
21
|
+
if name in _DEPRECATED:
|
|
22
|
+
import warnings
|
|
23
|
+
|
|
24
|
+
warnings.warn(
|
|
25
|
+
f"jaylog.{name} está depreciado e será removido na 0.4.0; "
|
|
26
|
+
"o modelo nunca foi usado pela biblioteca.",
|
|
27
|
+
DeprecationWarning,
|
|
28
|
+
stacklevel=2,
|
|
29
|
+
)
|
|
30
|
+
from jaylog import _compat
|
|
31
|
+
|
|
32
|
+
return getattr(_compat, name)
|
|
33
|
+
raise AttributeError(f"module 'jaylog' has no attribute '{name}'")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def __dir__() -> list[str]:
|
|
37
|
+
return sorted([*__all__, *_DEPRECATED])
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shim de depreciação de ``jaylog.models``.
|
|
3
|
+
|
|
4
|
+
``LogEntry`` era código morto: nunca foi construído em lugar nenhum, e o tipo
|
|
5
|
+
declarado (``log_img: Optional[str]``) nem batia com os ``bytes`` que o handler
|
|
6
|
+
HTTP envia. Fica acessível por mais um release via ``jaylog.LogEntry``, que
|
|
7
|
+
emite ``DeprecationWarning``. Remover na 0.4.0.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LogEntry(BaseModel):
|
|
16
|
+
log_timestamp: datetime
|
|
17
|
+
log_level: str
|
|
18
|
+
is_exception: bool
|
|
19
|
+
log_message: str
|
|
20
|
+
service: str
|
|
21
|
+
username: str
|
|
22
|
+
hostname: str
|
|
23
|
+
ipv4: str
|
|
24
|
+
service_path: str
|
|
25
|
+
line_number: int
|
|
26
|
+
log_img: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ["LogEntry"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Versão do pacote e versão do protocolo de ingestão.
|
|
3
|
+
|
|
4
|
+
Lookup único de ``importlib.metadata``: o valor era recalculado em
|
|
5
|
+
``handlers/http_handler.py`` e voltaria a ser recalculado no reporter de host.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
#: Versão do contrato cliente <-> backend. Vai no header ``x-jaylog-protocol``
|
|
11
|
+
#: e no payload de host. Mede o rollout; não dirige lógica no backend.
|
|
12
|
+
PROTOCOL_VERSION = 2
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
__version__ = version("jaylog")
|
|
16
|
+
except PackageNotFoundError: # rodando do fonte, sem instalação
|
|
17
|
+
__version__ = "unknown"
|
|
18
|
+
|
|
19
|
+
__all__ = ["__version__", "PROTOCOL_VERSION"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enriquecimento do ``LogRecord``, memoizado **no próprio record**.
|
|
3
|
+
|
|
4
|
+
O problema que isto resolve: ``build_log_entry_dict()`` é chamado uma vez por
|
|
5
|
+
handler downstream (arquivo, console, HTTP) e mais uma vez dentro do
|
|
6
|
+
``PlainTextFormatter``. Com screenshot ligado, o mesmo erro capturava a tela até
|
|
7
|
+
três vezes — três ``ImageGrab.grab()`` sequenciais, cada um na casa das centenas
|
|
8
|
+
de milissegundos, para produzir três imagens quase idênticas das quais duas eram
|
|
9
|
+
descartadas. Memoizar no record resolve porque o record é exatamente o escopo do
|
|
10
|
+
"uma vez por evento de log".
|
|
11
|
+
|
|
12
|
+
O screenshot saiu do caminho compartilhado: ``record_screenshot()`` é chamado só
|
|
13
|
+
pelo ``JaylogHttpHandler``, que é o único com para onde mandá-lo.
|
|
14
|
+
|
|
15
|
+
.. warning::
|
|
16
|
+
Este enriquecimento tem que continuar **downstream da fila**.
|
|
17
|
+
``QueueHandler.prepare()`` já faz ``record.msg = self.format(record)`` e zera
|
|
18
|
+
``exc_info``; por isso, aqui embaixo, ``record.exc_info`` é sempre ``None`` e
|
|
19
|
+
o traceback sobrevive apenas por já estar embutido em ``getMessage()``. Mover
|
|
20
|
+
o enriquecimento para antes da fila (um ``Filter`` no ``QueueHandler``, por
|
|
21
|
+
exemplo) duplicaria o traceback — e ainda rodaria na thread do chamador.
|
|
22
|
+
Há um teste de regressão travando isso.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import traceback
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
|
|
29
|
+
from jaylog.host import identity
|
|
30
|
+
from jaylog.screenshot import capture_screenshot
|
|
31
|
+
|
|
32
|
+
_ENTRY_ATTR = "_jaylog_entry"
|
|
33
|
+
_SCREENSHOT_ATTR = "_jaylog_screenshot"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_log_entry_dict(record: logging.LogRecord) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Campos do log, sem ``log_img``.
|
|
39
|
+
|
|
40
|
+
``hostname``/``username``/``ipv4`` continuam no corpo de todo log — a
|
|
41
|
+
tabela ``logs`` os mantém desnormalizados de forma permanente, então não há
|
|
42
|
+
o que migrar.
|
|
43
|
+
"""
|
|
44
|
+
is_exception = getattr(record, "is_exception", False)
|
|
45
|
+
|
|
46
|
+
log_message = record.getMessage()
|
|
47
|
+
if is_exception and record.exc_info:
|
|
48
|
+
tb = "".join(traceback.format_exception(*record.exc_info)).strip()
|
|
49
|
+
log_message = f"{log_message}\n{tb}"
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
"log_timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
|
|
53
|
+
"log_level": "EXCEPTION" if is_exception else record.levelname,
|
|
54
|
+
"is_exception": is_exception,
|
|
55
|
+
"log_message": log_message,
|
|
56
|
+
"service": record.name,
|
|
57
|
+
"username": identity.username(),
|
|
58
|
+
"hostname": identity.hostname(),
|
|
59
|
+
"ipv4": identity.ipv4(),
|
|
60
|
+
"service_path": record.pathname,
|
|
61
|
+
"line_number": record.lineno,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def record_entry(record: logging.LogRecord) -> dict:
|
|
66
|
+
"""``build_log_entry_dict`` com o resultado guardado no record."""
|
|
67
|
+
entry = getattr(record, _ENTRY_ATTR, None)
|
|
68
|
+
if entry is None:
|
|
69
|
+
entry = build_log_entry_dict(record)
|
|
70
|
+
setattr(record, _ENTRY_ATTR, entry)
|
|
71
|
+
return entry
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def wants_screenshot(record: logging.LogRecord) -> bool:
|
|
75
|
+
return record.levelno >= logging.ERROR or bool(getattr(record, "is_exception", False))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def record_screenshot(record: logging.LogRecord) -> bytes | None:
|
|
79
|
+
"""
|
|
80
|
+
Screenshot do evento, capturado no máximo uma vez por record.
|
|
81
|
+
|
|
82
|
+
O sentinela guardado no record distingue "ainda não tentamos" de "tentamos e
|
|
83
|
+
não veio nada" — sem ele, um ``None`` legítimo faria a próxima chamada
|
|
84
|
+
capturar de novo.
|
|
85
|
+
"""
|
|
86
|
+
if _SCREENSHOT_ATTR in record.__dict__:
|
|
87
|
+
return record.__dict__[_SCREENSHOT_ATTR]
|
|
88
|
+
image = capture_screenshot() if wants_screenshot(record) else None
|
|
89
|
+
record.__dict__[_SCREENSHOT_ATTR] = image
|
|
90
|
+
return image
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = ["build_log_entry_dict", "record_entry", "record_screenshot", "wants_screenshot"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Derivação da URL do endpoint de host a partir da URL do endpoint de log.
|
|
3
|
+
|
|
4
|
+
Deploys existentes já configuram ``JAYLOG_LOG_HTTP_ENDPOINT``; trocar o último
|
|
5
|
+
segmento do path (``/logs/add`` -> ``/logs/host``) evita exigir uma variável
|
|
6
|
+
nova de todo mundo. Quem precisar de outro caminho usa
|
|
7
|
+
``JAYLOG_HOST_HTTP_ENDPOINT``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def derive_host_endpoint(log_endpoint: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
``https://api/logs/add`` -> ``https://api/logs/host``.
|
|
16
|
+
|
|
17
|
+
Query string e fragmento são preservados; barra final é ignorada. Uma URL
|
|
18
|
+
sem path (``https://api``) vira ``https://api/host``.
|
|
19
|
+
"""
|
|
20
|
+
parts = urlsplit(log_endpoint)
|
|
21
|
+
head, sep, _last = parts.path.rstrip("/").rpartition("/")
|
|
22
|
+
path = f"{head}/host" if sep else "/host"
|
|
23
|
+
return urlunsplit((parts.scheme, parts.netloc, path, parts.query, parts.fragment))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["derive_host_endpoint"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Formatação legível para arquivo.
|
|
3
|
+
|
|
4
|
+
O módulo encolheu: identidade da máquina foi para ``host/identity.py`` (lazy),
|
|
5
|
+
screenshot para ``screenshot.py`` e a montagem do dicionário de log para
|
|
6
|
+
``context.py``. ``build_log_entry_dict`` continua reexportado aqui por compat —
|
|
7
|
+
remover na 0.4.0.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from jaylog.context import build_log_entry_dict, record_entry
|
|
14
|
+
from jaylog.screenshot import configure_screenshot
|
|
15
|
+
|
|
16
|
+
__all__ = ["PlainTextFormatter", "build_log_entry_dict", "configure_screenshot"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PlainTextFormatter(logging.Formatter):
|
|
20
|
+
"""Human-readable single-line formatter for .log files."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, show_service: bool = True) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.show_service = show_service
|
|
25
|
+
|
|
26
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
27
|
+
entry = record_entry(record)
|
|
28
|
+
log_timestamp = (
|
|
29
|
+
datetime.fromisoformat(entry["log_timestamp"]).astimezone().strftime("%d/%m/%Y %X")
|
|
30
|
+
)
|
|
31
|
+
log_level = f"[{entry['log_level']}]"
|
|
32
|
+
service_segment = f"[{entry['service']}] | " if self.show_service else ""
|
|
33
|
+
return f"{log_timestamp} {log_level.ljust(11)} | {service_segment}{entry['log_message']}"
|
|
@@ -4,7 +4,7 @@ from datetime import datetime
|
|
|
4
4
|
from typing import IO
|
|
5
5
|
|
|
6
6
|
from jaylog.colors import BLUE, GREEN, PURPLE, RED, RESET, YELLOW, supports_color
|
|
7
|
-
from jaylog.
|
|
7
|
+
from jaylog.context import record_entry
|
|
8
8
|
|
|
9
9
|
_LEVEL_COLORS = {
|
|
10
10
|
"DEBUG": PURPLE,
|
|
@@ -37,12 +37,14 @@ class ConsoleFormatter(logging.Formatter):
|
|
|
37
37
|
return f"{color}{text}{RESET}"
|
|
38
38
|
|
|
39
39
|
def format(self, record: logging.LogRecord) -> str:
|
|
40
|
-
entry =
|
|
41
|
-
log_timestamp =
|
|
42
|
-
|
|
40
|
+
entry = record_entry(record)
|
|
41
|
+
log_timestamp = (
|
|
42
|
+
datetime.fromisoformat(entry["log_timestamp"]).astimezone().strftime("%d/%m/%Y %X")
|
|
43
|
+
)
|
|
44
|
+
log_level = f"[{entry['log_level']}]"
|
|
43
45
|
colored_timestamp = self._paint(log_timestamp, GREEN)
|
|
44
|
-
colored_level = self._paint(log_level.ljust(11), _level_color(entry[
|
|
45
|
-
service_segment = f
|
|
46
|
+
colored_level = self._paint(log_level.ljust(11), _level_color(entry["log_level"]))
|
|
47
|
+
service_segment = f"[{entry['service']}] | " if self.show_service else ""
|
|
46
48
|
return f"{colored_timestamp} {colored_level} | {service_segment}{entry['log_message']}"
|
|
47
49
|
|
|
48
50
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
import urllib3
|
|
8
|
+
|
|
9
|
+
from jaylog._version import PROTOCOL_VERSION, __version__
|
|
10
|
+
from jaylog.context import record_entry, record_screenshot
|
|
11
|
+
from jaylog.host import reporter
|
|
12
|
+
from jaylog.runtime import RUN_ID
|
|
13
|
+
|
|
14
|
+
#: O backend sinaliza por header que não conhece este `run_id` — normalmente
|
|
15
|
+
#: porque o log correu na frente do POST de host. Ver `reporter.request_resend`.
|
|
16
|
+
HOST_REQUIRED_HEADER = "x-jaylog-host-required"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _to_multipart(fields: dict) -> dict:
|
|
20
|
+
result = {}
|
|
21
|
+
for key, value in fields.items():
|
|
22
|
+
if value is None:
|
|
23
|
+
continue
|
|
24
|
+
if isinstance(value, bytes):
|
|
25
|
+
result[key] = ("screenshot.jpg", value, "image/jpeg")
|
|
26
|
+
elif isinstance(value, bool):
|
|
27
|
+
result[key] = (None, "true" if value else "false")
|
|
28
|
+
elif not isinstance(value, str):
|
|
29
|
+
result[key] = (None, json.dumps(value))
|
|
30
|
+
else:
|
|
31
|
+
result[key] = (None, value)
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JaylogHttpHandler(logging.Handler):
|
|
36
|
+
"""
|
|
37
|
+
HTTP handler that POSTs log records as multipart/form-data to a remote endpoint.
|
|
38
|
+
|
|
39
|
+
Non-blocking behaviour is guaranteed by the QueueListener that drives this
|
|
40
|
+
handler — emit() runs in the listener's background thread.
|
|
41
|
+
|
|
42
|
+
O **corpo** do POST é byte-a-byte o mesmo da 0.2.x: ``logs`` mantém
|
|
43
|
+
``hostname``/``username``/``ipv4`` desnormalizados de forma permanente, então
|
|
44
|
+
não há campo a remover. O que muda são dois headers novos
|
|
45
|
+
(``x-jaylog-protocol`` e ``x-jaylog-run-id``); um backend antigo simplesmente
|
|
46
|
+
os ignora.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
endpoint: str,
|
|
52
|
+
api_key: str,
|
|
53
|
+
timeout: float = 5.0,
|
|
54
|
+
proxy: str | None = None,
|
|
55
|
+
verify: bool | str = False,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.endpoint = endpoint
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
self.verify = verify
|
|
61
|
+
self.proxy = {"http": proxy, "https": proxy} if proxy else None
|
|
62
|
+
self._session = requests.Session()
|
|
63
|
+
self._session.headers["x-api-key"] = api_key
|
|
64
|
+
self._session.headers["x-jaylog-version"] = __version__
|
|
65
|
+
self._session.headers["x-jaylog-protocol"] = str(PROTOCOL_VERSION)
|
|
66
|
+
self._session.headers["x-jaylog-run-id"] = RUN_ID
|
|
67
|
+
|
|
68
|
+
def mapLogRecord(self, record: logging.LogRecord) -> dict:
|
|
69
|
+
# Único ponto do programa que pede screenshot: é o único com para onde
|
|
70
|
+
# mandá-la. Console e arquivo não capturam mais nada.
|
|
71
|
+
entry = dict(record_entry(record))
|
|
72
|
+
entry["log_img"] = record_screenshot(record)
|
|
73
|
+
return entry
|
|
74
|
+
|
|
75
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
76
|
+
try:
|
|
77
|
+
if self.proxy:
|
|
78
|
+
self._session.proxies.update(self.proxy)
|
|
79
|
+
|
|
80
|
+
with warnings.catch_warnings():
|
|
81
|
+
warnings.simplefilter("ignore", urllib3.exceptions.InsecureRequestWarning)
|
|
82
|
+
response = self._session.post(
|
|
83
|
+
self.endpoint,
|
|
84
|
+
files=_to_multipart(self.mapLogRecord(record)),
|
|
85
|
+
timeout=self.timeout,
|
|
86
|
+
verify=self.verify,
|
|
87
|
+
)
|
|
88
|
+
if os.getenv("JAYLOG_HTTP_DEBUG") == "1":
|
|
89
|
+
print(response.status_code)
|
|
90
|
+
print(response.content)
|
|
91
|
+
|
|
92
|
+
if response.headers.get(HOST_REQUIRED_HEADER) == "1":
|
|
93
|
+
# O log foi aceito (202) — não há nada a reenviar aqui. O que
|
|
94
|
+
# falta é o registro de ambiente, que o reporter reenvia (com
|
|
95
|
+
# debounce, porque o header vem em toda linha afetada).
|
|
96
|
+
reporter.request_resend(record.name)
|
|
97
|
+
except Exception:
|
|
98
|
+
# `handleError` respeita `logging.raiseExceptions`: barulho em dev,
|
|
99
|
+
# silêncio em produção. O `except: pass` anterior era um buraco
|
|
100
|
+
# negro — era por causa dele que ninguém descobria que o caminho
|
|
101
|
+
# HTTP estava falhando.
|
|
102
|
+
self.handleError(record)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Intencionalmente vazio.
|
|
2
|
+
#
|
|
3
|
+
# `settings.py` precisa de `host.identity` sem arrastar `requests` (via
|
|
4
|
+
# `reporter`) nem `subprocess` (via `detect_git`). Re-exportar os submódulos
|
|
5
|
+
# aqui recriaria esse acoplamento no import. Importe sempre o submódulo
|
|
6
|
+
# explícito: `from jaylog.host import identity`.
|