jaylog 0.1.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.1.4/PKG-INFO ADDED
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.3
2
+ Name: jaylog
3
+ Version: 0.1.4
4
+ Summary: Customized Python logging library with file rotation and HTTP forwarding
5
+ Author: Gpocas
6
+ Author-email: Gpocas <gpocas01@gmail.com>
7
+ Classifier: Topic :: Software Development :: Build Tools
8
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
9
+ Classifier: Natural Language :: Portuguese (Brazilian)
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Requires-Dist: pydantic>=2.0,<3.0
12
+ Requires-Dist: pydantic-settings>=2.0,<3.0
13
+ Requires-Dist: requests>=2.32,<3.0
14
+ Requires-Dist: pillow>=11.0,<13.0
15
+ Requires-Python: >=3.10, <3.14
16
+ Project-URL: repository, https://github.com/Gpocas/jaylog
17
+ Project-URL: Bug Tracker, https://github.com/Gpocas/jaylog/issues
18
+ Description-Content-Type: text/markdown
19
+
20
+ # jaylog
21
+
22
+ Biblioteca de logging para Python com rotação de arquivos e envio HTTP para um endpoint remoto.
23
+
24
+ ## Instalação
25
+
26
+ ```bash
27
+ pip install -U --no-cache-dir git+https://github.com/Gpocas/jaylog.git
28
+ ```
29
+
30
+ ## Variáveis de ambiente
31
+
32
+ As variáveis usam o prefixo `JAYLOG_`. Podem ser definidas no ambiente do sistema ou em um arquivo `.env` / `.env.logging` na raiz do projeto.
33
+
34
+ | Variável | obrigatorio? | Padrão | Descrição |
35
+ | ------------------------------- | ------------ | --------- | ----------------------------------------------------------------------- |
36
+ | `JAYLOG_APP_NAME` | SIM | `null` | Nome do serviço/bot (usado no nome do arquivo de log) |
37
+ | `JAYLOG_LOG_DIR` | SIM | `null` | Caminho do diretório onde os arquivos de log serão salvos |
38
+ | `JAYLOG_LOG_LEVEL` | NÃO | `INFO` | Nível mínimo de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`) |
39
+ | `JAYLOG_LOG_MAX_BYTES` | NÃO | `5242880` | Tamanho máximo do arquivo de log antes de rotacionar (bytes) |
40
+ | `JAYLOG_LOG_BACKUP_COUNT` | NÃO | `5` | Quantidade de arquivos de backup mantidos após rotação |
41
+ | `JAYLOG_LOG_RETENTION_DAYS` | NÃO | `7` | Dias para manter arquivos de log antigos |
42
+ | `JAYLOG_LOG_HTTP_TIMEOUT` | NÃO | `5.0` | Timeout em segundos para o envio HTTP |
43
+ | `JAYLOG_LOG_HTTP_ENDPOINT` | NÃO | `null` | URL do endpoint que receberá os logs |
44
+ | `JAYLOG_LOG_HTTP_API_KEY` | NÃO | `null` | Chave de autenticação enviada no header `x-api-key` |
45
+ | `JAYLOG_LOG_HTTP_PROXY` | NÃO | `null` | URL do proxy para o envio HTTP (ex: `http:\\user:password@server:port`) |
46
+ | `JAYLOG_LOG_SCREENSHOT_ENABLED` | NÃO | `false` | Captura screenshot no momento do log (`true`/`false`, apenas Windows) |
47
+
48
+
49
+ ## Uso Simples
50
+
51
+ __*.env.logging*__
52
+ ```env
53
+ JAYLOG_APP_NAME=meu-bot
54
+ JAYLOG_LOG_DIR=C:\logs
55
+ ```
56
+ __*main.py*__
57
+ ```python
58
+ from jaylog import JaylogSettings, get_logger
59
+
60
+ logger = get_logger(JaylogSettings())
61
+
62
+ logger.info("Mensagem de log")
63
+ logger.error("Erro ao processar")
64
+ ```
65
+
66
+
67
+ ## Alterando Caminho padrão do .env
68
+
69
+ __*development.env*__
70
+ ```env
71
+ JAYLOG_APP_NAME=meu-bot
72
+ JAYLOG_LOG_DIR=C:\logs
73
+ ```
74
+
75
+ __*main.py*__
76
+ ```python
77
+ from jaylog import JaylogSettings, get_logger
78
+
79
+
80
+ settings = JaylogSettings(_env_file='development.env')
81
+ logger = get_logger(settings)
82
+
83
+ logger.info("Mensagem de log")
84
+ logger.error("Erro ao processar")
85
+ ```
86
+
87
+
88
+ ## Preparando para produção
89
+
90
+
91
+ > [!IMPORTANT]
92
+ > **HTTP_ENDPOINT** e **HTTP_API_KEY** (opcionais) 📢
93
+ >
94
+ > - A configuração **HTTP_ENDPOINT** e **HTTP_API_KEY** não precisa ser feita em ambiente local ou de desenvolvimento
95
+ > - Se apenas uma das duas variaveis **HTTP_ENDPOINT** ou **HTTP_API_KEYS** for definida, o envio HTTP é ignorado.
96
+ > - Caso a aplicação execute em um ambiente que usa um proxy ntlm, defina `JAYLOG_LOG_HTTP_PROXY`
97
+
98
+
99
+ __*prodution.env*__
100
+ ```env
101
+ JAYLOG_APP_NAME=mlleu-bot
102
+ JAYLOG_LOG_DIR=C:\logs
103
+ JAYLOG_LOG_HTTP_ENDPOINT=https://meu-backend.com/logs/add
104
+ JAYLOG_LOG_HTTP_API_KEY=minha-chave
105
+ JAYLOG_LOG_HTTP_PROXY=http://username:password@proxy.com:8080
106
+ ```
107
+
108
+ __*main.py*__
109
+ ```python
110
+ from jaylog import JaylogSettings, get_logger
111
+
112
+
113
+ settings = JaylogSettings(_env_file='prodution.env', _secrets_dir='/caminho/secrets/')
114
+ logger = get_logger(settings)
115
+
116
+ logger.info("Mensagem de log")
117
+ logger.error("Erro ao processar")
118
+ ```
119
+
120
+
jaylog-0.1.4/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # jaylog
2
+
3
+ Biblioteca de logging para Python com rotação de arquivos e envio HTTP para um endpoint remoto.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ pip install -U --no-cache-dir git+https://github.com/Gpocas/jaylog.git
9
+ ```
10
+
11
+ ## Variáveis de ambiente
12
+
13
+ As variáveis usam o prefixo `JAYLOG_`. Podem ser definidas no ambiente do sistema ou em um arquivo `.env` / `.env.logging` na raiz do projeto.
14
+
15
+ | Variável | obrigatorio? | Padrão | Descrição |
16
+ | ------------------------------- | ------------ | --------- | ----------------------------------------------------------------------- |
17
+ | `JAYLOG_APP_NAME` | SIM | `null` | Nome do serviço/bot (usado no nome do arquivo de log) |
18
+ | `JAYLOG_LOG_DIR` | SIM | `null` | Caminho do diretório onde os arquivos de log serão salvos |
19
+ | `JAYLOG_LOG_LEVEL` | NÃO | `INFO` | Nível mínimo de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`) |
20
+ | `JAYLOG_LOG_MAX_BYTES` | NÃO | `5242880` | Tamanho máximo do arquivo de log antes de rotacionar (bytes) |
21
+ | `JAYLOG_LOG_BACKUP_COUNT` | NÃO | `5` | Quantidade de arquivos de backup mantidos após rotação |
22
+ | `JAYLOG_LOG_RETENTION_DAYS` | NÃO | `7` | Dias para manter arquivos de log antigos |
23
+ | `JAYLOG_LOG_HTTP_TIMEOUT` | NÃO | `5.0` | Timeout em segundos para o envio HTTP |
24
+ | `JAYLOG_LOG_HTTP_ENDPOINT` | NÃO | `null` | URL do endpoint que receberá os logs |
25
+ | `JAYLOG_LOG_HTTP_API_KEY` | NÃO | `null` | Chave de autenticação enviada no header `x-api-key` |
26
+ | `JAYLOG_LOG_HTTP_PROXY` | NÃO | `null` | URL do proxy para o envio HTTP (ex: `http:\\user:password@server:port`) |
27
+ | `JAYLOG_LOG_SCREENSHOT_ENABLED` | NÃO | `false` | Captura screenshot no momento do log (`true`/`false`, apenas Windows) |
28
+
29
+
30
+ ## Uso Simples
31
+
32
+ __*.env.logging*__
33
+ ```env
34
+ JAYLOG_APP_NAME=meu-bot
35
+ JAYLOG_LOG_DIR=C:\logs
36
+ ```
37
+ __*main.py*__
38
+ ```python
39
+ from jaylog import JaylogSettings, get_logger
40
+
41
+ logger = get_logger(JaylogSettings())
42
+
43
+ logger.info("Mensagem de log")
44
+ logger.error("Erro ao processar")
45
+ ```
46
+
47
+
48
+ ## Alterando Caminho padrão do .env
49
+
50
+ __*development.env*__
51
+ ```env
52
+ JAYLOG_APP_NAME=meu-bot
53
+ JAYLOG_LOG_DIR=C:\logs
54
+ ```
55
+
56
+ __*main.py*__
57
+ ```python
58
+ from jaylog import JaylogSettings, get_logger
59
+
60
+
61
+ settings = JaylogSettings(_env_file='development.env')
62
+ logger = get_logger(settings)
63
+
64
+ logger.info("Mensagem de log")
65
+ logger.error("Erro ao processar")
66
+ ```
67
+
68
+
69
+ ## Preparando para produção
70
+
71
+
72
+ > [!IMPORTANT]
73
+ > **HTTP_ENDPOINT** e **HTTP_API_KEY** (opcionais) 📢
74
+ >
75
+ > - A configuração **HTTP_ENDPOINT** e **HTTP_API_KEY** não precisa ser feita em ambiente local ou de desenvolvimento
76
+ > - Se apenas uma das duas variaveis **HTTP_ENDPOINT** ou **HTTP_API_KEYS** for definida, o envio HTTP é ignorado.
77
+ > - Caso a aplicação execute em um ambiente que usa um proxy ntlm, defina `JAYLOG_LOG_HTTP_PROXY`
78
+
79
+
80
+ __*prodution.env*__
81
+ ```env
82
+ JAYLOG_APP_NAME=mlleu-bot
83
+ JAYLOG_LOG_DIR=C:\logs
84
+ JAYLOG_LOG_HTTP_ENDPOINT=https://meu-backend.com/logs/add
85
+ JAYLOG_LOG_HTTP_API_KEY=minha-chave
86
+ JAYLOG_LOG_HTTP_PROXY=http://username:password@proxy.com:8080
87
+ ```
88
+
89
+ __*main.py*__
90
+ ```python
91
+ from jaylog import JaylogSettings, get_logger
92
+
93
+
94
+ settings = JaylogSettings(_env_file='prodution.env', _secrets_dir='/caminho/secrets/')
95
+ logger = get_logger(settings)
96
+
97
+ logger.info("Mensagem de log")
98
+ logger.error("Erro ao processar")
99
+ ```
100
+
101
+
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "jaylog"
3
+ version = "0.1.4"
4
+ description = "Customized Python logging library with file rotation and HTTP forwarding"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Gpocas", email = "gpocas01@gmail.com" }
8
+ ]
9
+
10
+ classifiers = [
11
+ "Topic :: Software Development :: Build Tools",
12
+ "Topic :: Software Development :: Libraries :: Python Modules",
13
+ "Natural Language :: Portuguese (Brazilian)",
14
+ "Development Status :: 5 - Production/Stable",
15
+ ]
16
+
17
+ requires-python = ">=3.10,<3.14"
18
+ dependencies = [
19
+ "pydantic>=2.0,<3.0",
20
+ "pydantic-settings>=2.0,<3.0",
21
+ "requests>=2.32,<3.0",
22
+ "pillow>=11.0,<13.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ repository = "https://github.com/Gpocas/jaylog"
27
+ "Bug Tracker" = "https://github.com/Gpocas/jaylog/issues"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.10.8,<0.11.0"]
31
+ build-backend = "uv_build"
@@ -0,0 +1,5 @@
1
+ from jaylog.logger import get_logger, shutdown
2
+ from jaylog.models import LogEntry
3
+ from jaylog.settings import JaylogSettings
4
+
5
+ __all__ = ["get_logger", "shutdown", "LogEntry", "JaylogSettings"]
@@ -0,0 +1,7 @@
1
+ import logging
2
+
3
+
4
+ class ExceptionFlagFilter(logging.Filter):
5
+ def filter(self, record: logging.LogRecord) -> bool:
6
+ record.is_exception = bool(record.exc_info and record.exc_info[0] is not None)
7
+ return True
@@ -0,0 +1,110 @@
1
+ import getpass
2
+ import io
3
+ import logging
4
+ import socket
5
+ import sys
6
+ import traceback
7
+ from datetime import datetime, timezone
8
+
9
+
10
+ def _get_host_info() -> tuple[str, str, str]:
11
+ hostname = socket.gethostname()
12
+ try:
13
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
14
+ s.connect(("8.8.8.8", 80))
15
+ host_ip = s.getsockname()[0]
16
+ except OSError:
17
+ host_ip = "unknown"
18
+ try:
19
+ username = getpass.getuser()
20
+ except Exception:
21
+ username = "unknown"
22
+ return username, hostname, host_ip
23
+
24
+
25
+ _HOST_USERNAME, _HOSTNAME, _HOST_IP = _get_host_info()
26
+
27
+ _screenshot_enabled: bool = True
28
+
29
+ _MAX_BYTES = 1 * 1024 * 1024 # 1 MB
30
+
31
+
32
+ def configure_screenshot(enabled: bool) -> None:
33
+ global _screenshot_enabled
34
+ _screenshot_enabled = enabled
35
+
36
+
37
+ def _capture_screenshot() -> bytes | None:
38
+ if not _screenshot_enabled:
39
+ return None
40
+ try:
41
+ from PIL import ImageGrab
42
+
43
+ img = ImageGrab.grab()
44
+
45
+ quality = 85
46
+ scale = 1.0
47
+ buf = io.BytesIO()
48
+
49
+ while True:
50
+ buf.seek(0)
51
+ buf.truncate()
52
+
53
+ current = img
54
+ if scale < 1.0:
55
+ w = int(img.width * scale)
56
+ h = int(img.height * scale)
57
+ current = img.resize((w, h))
58
+
59
+ current.save(buf, format="JPEG", quality=quality, optimize=True)
60
+
61
+ if buf.tell() <= _MAX_BYTES:
62
+ break
63
+
64
+ if quality > 30:
65
+ quality -= 10
66
+ elif scale > 0.5:
67
+ scale -= 0.1
68
+ else:
69
+ break
70
+
71
+ buf.seek(0)
72
+ return buf.read()
73
+ except Exception as exc:
74
+ print(f"[jaylog] screenshot: {exc}", file=sys.stderr)
75
+ return None
76
+
77
+
78
+ def build_log_entry_dict(record: logging.LogRecord) -> dict:
79
+ is_exception = getattr(record, "is_exception", False)
80
+
81
+ log_message = record.getMessage()
82
+ if is_exception and record.exc_info:
83
+ tb = "".join(traceback.format_exception(*record.exc_info)).strip()
84
+ log_message = f"{log_message}\n{tb}"
85
+
86
+ return {
87
+ "log_timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
88
+ "log_level": "EXCEPTION" if is_exception else record.levelname,
89
+ "is_exception": is_exception,
90
+ "log_message": log_message,
91
+ "service": record.name,
92
+ "username": _HOST_USERNAME,
93
+ "hostname": _HOSTNAME,
94
+ "ipv4": _HOST_IP,
95
+ "service_path": record.pathname,
96
+ "log_img": _capture_screenshot() if (record.levelno >= logging.ERROR or is_exception) else None,
97
+ }
98
+
99
+
100
+ class PlainTextFormatter(logging.Formatter):
101
+ """Human-readable single-line formatter for .log files."""
102
+
103
+ def format(self, record: logging.LogRecord) -> str:
104
+ entry = build_log_entry_dict(record)
105
+ line = (
106
+ f"{entry['log_timestamp']} [{entry['log_level']}]"
107
+ f" {entry['service']} {entry['hostname']}({entry['ipv4']}) {entry['username']}"
108
+ f" | {entry['log_message']}"
109
+ )
110
+ return line
@@ -0,0 +1,4 @@
1
+ from .file_handler import JaylogFileHandler
2
+ from .http_handler import JaylogHttpHandler
3
+
4
+ __all__ = ["JaylogFileHandler", "JaylogHttpHandler"]
@@ -0,0 +1,51 @@
1
+ import time
2
+ from logging.handlers import RotatingFileHandler
3
+ from pathlib import Path
4
+
5
+ from jaylog.formatters import PlainTextFormatter
6
+
7
+
8
+ class JaylogFileHandler(RotatingFileHandler):
9
+ """
10
+ Rotating file handler that:
11
+ - rotates when the file reaches `maxBytes` (default 5 MB)
12
+ - deletes backup files older than `retention_days` (default 7) after each rollover
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ filename: Path,
18
+ max_bytes: int = 5 * 1024 * 1024,
19
+ backup_count: int = 20,
20
+ retention_days: int = 7,
21
+ encoding: str = "utf-8",
22
+ ) -> None:
23
+ filename.parent.mkdir(parents=True, exist_ok=True)
24
+ super().__init__(
25
+ filename,
26
+ maxBytes=max_bytes,
27
+ backupCount=backup_count,
28
+ encoding=encoding,
29
+ )
30
+ self.retention_days = retention_days
31
+ self.setFormatter(PlainTextFormatter())
32
+
33
+ def doRollover(self) -> None:
34
+ super().doRollover()
35
+ self._purge_old_logs()
36
+
37
+ def _purge_old_logs(self) -> None:
38
+ base = Path(self.baseFilename)
39
+ cutoff = time.time() - self.retention_days * 86400
40
+
41
+ for entry in base.parent.iterdir():
42
+ if not entry.is_file():
43
+ continue
44
+ # Match rotated backups: app.log.1, app.log.2, ...
45
+ if entry.name == base.name or not entry.name.startswith(base.name):
46
+ continue
47
+ try:
48
+ if entry.stat().st_mtime < cutoff:
49
+ entry.unlink()
50
+ except OSError:
51
+ pass
@@ -0,0 +1,79 @@
1
+ import json
2
+ import logging
3
+ import warnings
4
+ from importlib.metadata import PackageNotFoundError, version
5
+
6
+ import requests
7
+ import urllib3
8
+
9
+ from jaylog.formatters import build_log_entry_dict
10
+ from jaylog.settings import JaylogSettings
11
+
12
+ settings = JaylogSettings()
13
+
14
+ try:
15
+ _JAYLOG_VERSION = version("jaylog")
16
+ except PackageNotFoundError:
17
+ _JAYLOG_VERSION = "unknown"
18
+
19
+ proxies = {
20
+ 'http': settings.log_http_proxy,
21
+ 'https': settings.log_http_proxy
22
+ }
23
+
24
+ session = requests.Session()
25
+ if settings.log_http_proxy:
26
+ session.proxies.update(proxies)
27
+
28
+ def _to_multipart(fields: dict) -> dict:
29
+ result = {}
30
+ for key, value in fields.items():
31
+ if value is None:
32
+ continue
33
+ if isinstance(value, bytes):
34
+ result[key] = ("screenshot.jpg", value, "image/jpeg")
35
+ elif isinstance(value, bool):
36
+ result[key] = (None, "true" if value else "false")
37
+ elif not isinstance(value, str):
38
+ result[key] = (None, json.dumps(value))
39
+ else:
40
+ result[key] = (None, value)
41
+ return result
42
+
43
+
44
+ class JaylogHttpHandler(logging.Handler):
45
+ """
46
+ HTTP handler that POSTs log records as multipart/form-data to a remote endpoint.
47
+
48
+ Non-blocking behaviour is guaranteed by the QueueListener that drives this
49
+ handler — emit() runs in the listener's background thread.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ endpoint: str,
55
+ api_key: str,
56
+ timeout: float = 5.0,
57
+ ) -> None:
58
+ super().__init__()
59
+ self.endpoint = endpoint
60
+ self.timeout = timeout
61
+ self._session = session
62
+ self._session.headers["x-api-key"] = api_key
63
+ self._session.headers["x-jaylog-version"] = _JAYLOG_VERSION
64
+
65
+ def mapLogRecord(self, record: logging.LogRecord) -> dict:
66
+ return build_log_entry_dict(record)
67
+
68
+ def emit(self, record: logging.LogRecord) -> None:
69
+ try:
70
+ with warnings.catch_warnings():
71
+ warnings.simplefilter("ignore", urllib3.exceptions.InsecureRequestWarning)
72
+ self._session.post(
73
+ self.endpoint,
74
+ files=_to_multipart(self.mapLogRecord(record)),
75
+ timeout=self.timeout,
76
+ verify=False,
77
+ )
78
+ except Exception:
79
+ pass
@@ -0,0 +1,116 @@
1
+ import atexit
2
+ import logging
3
+ import os
4
+ import signal
5
+ from logging.handlers import QueueHandler, QueueListener
6
+ from queue import Queue
7
+
8
+ from jaylog.filters import ExceptionFlagFilter
9
+ from jaylog.formatters import configure_screenshot
10
+ from jaylog.handlers.file_handler import JaylogFileHandler
11
+ from jaylog.handlers.http_handler import JaylogHttpHandler
12
+ from jaylog.settings import JaylogSettings
13
+
14
+ # Registry: name -> (logger, listener) so callers can shut down cleanly
15
+ _registry: dict[str, tuple[logging.Logger, QueueListener]] = {}
16
+ _shutdown_registered = False
17
+
18
+
19
+ def _register_shutdown_hooks() -> None:
20
+ global _shutdown_registered
21
+ if _shutdown_registered:
22
+ return
23
+ _shutdown_registered = True
24
+
25
+ atexit.register(shutdown)
26
+
27
+ def _sigterm_handler(signum, frame): # noqa: ANN001
28
+ shutdown()
29
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
30
+ signal.raise_signal(signal.SIGTERM)
31
+
32
+ signal.signal(signal.SIGTERM, _sigterm_handler)
33
+
34
+
35
+ def get_logger(settings: JaylogSettings | None = None) -> logging.Logger:
36
+ """
37
+ Return a configured logger.
38
+
39
+ The logger name is read from JAYLOG_LOGGER_NAME (default: "jaylog").
40
+ Calling this multiple times with the same logger name returns the **same**
41
+ logger without re-attaching handlers.
42
+
43
+ Architecture:
44
+ logger → QueueHandler → Queue → QueueListener → [FileHandler, HttpHandler?]
45
+
46
+ The QueueListener runs in a background thread so `emit()` never blocks the
47
+ calling thread.
48
+ """
49
+ if settings is None:
50
+ settings = JaylogSettings()
51
+
52
+ name = settings.app_name
53
+
54
+ configure_screenshot(settings.log_screenshot_enabled)
55
+
56
+ if name in _registry:
57
+ return _registry[name][0]
58
+
59
+ # ------------------------------------------------------------------
60
+ # Build the actual (downstream) handlers
61
+ # ------------------------------------------------------------------
62
+ downstream: list[logging.Handler] = []
63
+
64
+ log_path = settings.log_dir / settings.log_filename
65
+ file_handler = JaylogFileHandler(
66
+ filename=log_path,
67
+ max_bytes=settings.log_max_bytes,
68
+ backup_count=settings.log_backup_count,
69
+ retention_days=settings.log_retention_days,
70
+ )
71
+ file_handler.setLevel(settings.log_level)
72
+ downstream.append(file_handler)
73
+
74
+ if settings.log_http_endpoint and settings.log_http_api_key:
75
+ http_handler = JaylogHttpHandler(
76
+ endpoint=settings.log_http_endpoint,
77
+ api_key=settings.log_http_api_key,
78
+ timeout=settings.log_http_timeout,
79
+ )
80
+ http_handler.setLevel(settings.log_level)
81
+ downstream.append(http_handler)
82
+
83
+ # ------------------------------------------------------------------
84
+ # Wire up the Queue + QueueListener
85
+ # ------------------------------------------------------------------
86
+ queue: Queue = Queue(maxsize=-1) # unbounded
87
+ queue_handler = QueueHandler(queue)
88
+ queue_handler.addFilter(ExceptionFlagFilter())
89
+
90
+ listener = QueueListener(queue, *downstream, respect_handler_level=True)
91
+ listener.start()
92
+
93
+ # ------------------------------------------------------------------
94
+ # Configure the logger
95
+ # ------------------------------------------------------------------
96
+ logger = logging.getLogger(name)
97
+ logger.setLevel(settings.log_level)
98
+ logger.addHandler(queue_handler)
99
+ logger.propagate = False
100
+
101
+ _registry[name] = (logger, listener)
102
+ _register_shutdown_hooks()
103
+ return logger
104
+
105
+
106
+ def shutdown(name: str | None = None) -> None:
107
+ """
108
+ Stop the QueueListener(s) gracefully, flushing any remaining records.
109
+
110
+ Pass a logger `name` to stop a single logger, or omit to stop all.
111
+ """
112
+ targets = [name] if name else list(_registry.keys())
113
+ for n in targets:
114
+ if n in _registry:
115
+ _, listener = _registry.pop(n)
116
+ listener.stop()
@@ -0,0 +1,16 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class LogEntry(BaseModel):
7
+ log_timestamp: datetime
8
+ log_level: str
9
+ is_exception: bool
10
+ log_message: str
11
+ service: str
12
+ username: str
13
+ hostname: str
14
+ ipv4: str
15
+ service_path: str
16
+ log_img: Optional[str] = None
@@ -0,0 +1,57 @@
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
+ # App identity
20
+ app_name: str
21
+ log_dir: Path
22
+
23
+ secrets_dir: Path | None = None
24
+
25
+ # File handler
26
+ log_level: str = "INFO"
27
+ log_max_bytes: int = 5 * 1024 * 1024 # 5 MB
28
+ log_backup_count: int = 5
29
+ log_retention_days: int = 7
30
+
31
+ # HTTP handler
32
+ log_http_endpoint: Optional[str] = None
33
+ log_http_api_key: Optional[str] = None
34
+ log_http_timeout: float = 5.0
35
+ log_http_proxy: Optional[str] = None
36
+
37
+ # Screenshot (log_img field) — desativar com JAYLOG_LOG_SCREENSHOT_ENABLED=false
38
+ log_screenshot_enabled: bool = False
39
+
40
+ @field_validator("log_dir", mode="after")
41
+ @classmethod
42
+ def validate_log_dir(cls, v: Path) -> Path:
43
+ if v.exists() and not v.is_dir():
44
+ raise ValueError(f"JAYLOG_LOG_DIR '{v}' exists but is not a directory")
45
+ return v
46
+
47
+ @computed_field
48
+ @property
49
+ def log_filename(self) -> Path:
50
+ return Path(f"{self.app_name}_{_HOSTNAME}_{_HOST_USERNAME}.log")
51
+
52
+ def reload_secrets(self):
53
+ if self.secrets_dir:
54
+ if not self.secrets_dir.exists() or not self.secrets_dir.is_dir:
55
+ raise ValueError('SECRETS_DIR is not valid directory')
56
+
57
+ return JaylogSettings(_secrets_dir=self.secrets_dir) # ty:ignore[missing-argument, unknown-argument]