|
| 1 | +"""Small env-driven logging setup shared by host, runtime, and worker.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import logging |
| 6 | +import os |
| 7 | +import socket |
| 8 | +import sys |
| 9 | +from typing import TextIO |
| 10 | + |
| 11 | +LOG_CONTEXT_ATTR = "agentix_context" |
| 12 | +DEFAULT_LOG_FORMAT = f"%(asctime)s [%({LOG_CONTEXT_ATTR})s] [%(name)s] %(message)s" |
| 13 | + |
| 14 | +_context = "host" |
| 15 | +_factory_installed = False |
| 16 | +_previous_factory = logging.getLogRecordFactory() |
| 17 | + |
| 18 | + |
| 19 | +class _SafeFormatMap(dict[str, str]): |
| 20 | + def __missing__(self, key: str) -> str: |
| 21 | + return "{" + key + "}" |
| 22 | + |
| 23 | + |
| 24 | +def configure_logging( |
| 25 | + *, |
| 26 | + default_context: str = "host", |
| 27 | + stream: TextIO | None = None, |
| 28 | + force: bool = False, |
| 29 | +) -> None: |
| 30 | + """Configure stdlib logging from Agentix env vars. |
| 31 | +
|
| 32 | + Env vars: |
| 33 | + AGENTIX_LOG_LEVEL: logging level name, default INFO. |
| 34 | + AGENTIX_LOG_FORMAT: stdlib %-style format string. |
| 35 | + AGENTIX_LOG_CONTEXT: context template for this process. |
| 36 | +
|
| 37 | + Context templates support `{uname}`, `{hostname}`, `{pid}`, and |
| 38 | + `{id}`. The worker spawner sets `AGENTIX_WORKER_ID`, which backs |
| 39 | + `{id}` inside worker processes. |
| 40 | + """ |
| 41 | + set_log_context(os.environ.get("AGENTIX_LOG_CONTEXT", default_context)) |
| 42 | + level_name = os.environ.get("AGENTIX_LOG_LEVEL", "INFO").upper() |
| 43 | + level = getattr(logging, level_name, logging.INFO) |
| 44 | + log_format = os.environ.get("AGENTIX_LOG_FORMAT", DEFAULT_LOG_FORMAT) |
| 45 | + logging.basicConfig( |
| 46 | + level=level, |
| 47 | + stream=stream or sys.stderr, |
| 48 | + format=log_format, |
| 49 | + force=force, |
| 50 | + ) |
| 51 | + |
| 52 | + |
| 53 | +def set_log_context(template: str) -> None: |
| 54 | + global _context |
| 55 | + _context = _expand_context(template) |
| 56 | + _install_record_factory() |
| 57 | + |
| 58 | + |
| 59 | +def get_log_context() -> str: |
| 60 | + return _context |
| 61 | + |
| 62 | + |
| 63 | +def _expand_context(template: str) -> str: |
| 64 | + hostname = socket.gethostname() |
| 65 | + values = _SafeFormatMap( |
| 66 | + { |
| 67 | + "hostname": hostname, |
| 68 | + "uname": hostname, |
| 69 | + "pid": str(os.getpid()), |
| 70 | + "id": os.environ.get("AGENTIX_WORKER_ID", str(os.getpid())), |
| 71 | + } |
| 72 | + ) |
| 73 | + try: |
| 74 | + return template.format_map(values) |
| 75 | + except ValueError: |
| 76 | + return template |
| 77 | + |
| 78 | + |
| 79 | +def _install_record_factory() -> None: |
| 80 | + global _factory_installed |
| 81 | + if _factory_installed: |
| 82 | + return |
| 83 | + |
| 84 | + def record_factory(*args, **kwargs): |
| 85 | + record = _previous_factory(*args, **kwargs) |
| 86 | + if not hasattr(record, LOG_CONTEXT_ATTR): |
| 87 | + setattr(record, LOG_CONTEXT_ATTR, _context) |
| 88 | + return record |
| 89 | + |
| 90 | + logging.setLogRecordFactory(record_factory) |
| 91 | + _factory_installed = True |
| 92 | + |
| 93 | + |
| 94 | +__all__ = [ |
| 95 | + "DEFAULT_LOG_FORMAT", |
| 96 | + "LOG_CONTEXT_ATTR", |
| 97 | + "configure_logging", |
| 98 | + "get_log_context", |
| 99 | + "set_log_context", |
| 100 | +] |
0 commit comments