jaylog 0.2.1__tar.gz → 0.2.4__tar.gz

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