← Files LogForgeARCHIVED FILE
sdk/python/logforge_out.py
7.46 KB · Nov 4, 2026 · 06:13 UTC
"""Structured diagnostics. Tag and level filters run before lazy payloads."""
from __future__ import annotations
import atexit
import copy
import inspect
import json
import logging
import os
from pathlib import Path
import re
import threading
from datetime import datetime, timezone
from uuid import uuid4
_LEVELS = {"debug": 10, "info": 20, "warn": 30, "error": 40}
_SECRET = re.compile(r"password|passwd|secret|token|authorization|cookie|api.?key", re.I)
def _clean(value, seen=None, depth=0):
if depth > 8:
return "[depth limited]"
if value is None or isinstance(value, (bool, int)):
return value
if isinstance(value, float):
return value if value == value and abs(value) != float("inf") else str(value)
if isinstance(value, str):
return value[:8192] + ("[truncated]" if len(value) > 8192 else "")
seen = set() if seen is None else seen
if id(value) in seen:
return "[circular]"
seen.add(id(value))
if isinstance(value, dict):
result = {str(k): "[REDACTED]" if _SECRET.search(str(k)) else _clean(v, seen, depth + 1) for k, v in list(value.items())[:100]}
elif isinstance(value, (list, tuple, set)):
result = [_clean(v, seen, depth + 1) for v in list(value)[:100]]
elif isinstance(value, BaseException):
result = {"name": type(value).__name__, "message": _clean(str(value))}
else:
result = _clean(str(value))
seen.remove(id(value))
return result
def _stdout(event):
print(json.dumps(event, ensure_ascii=True, allow_nan=False), flush=True)
class _Logger:
def __init__(self):
self._lock = threading.RLock()
self.tags = [t.strip().lower() for t in os.getenv("LOGFORGE_TAGS", "").split(",") if t.strip()]
self.level = os.getenv("LOGFORGE_LEVEL", "debug")
self.enabled = os.getenv("LOGFORGE_ENABLED", "true") != "false"
self.run_id = os.getenv("LOGFORGE_RUN_ID")
self.service = os.getenv("LOGFORGE_SERVICE", "application")
self.sink = _stdout
self.reset()
def configure(self, *, tags=None, level=None, enabled=None, run_id=None, service=None, sink=None):
with self._lock:
if level is not None and level not in _LEVELS:
raise ValueError("Unknown LogForge level")
if tags is not None and (not isinstance(tags, list) or not all(isinstance(t, str) for t in tags)):
raise ValueError("tags must be a list of strings")
for key, value in locals().copy().items():
if key != "self" and value is not None:
setattr(self, key, list(dict.fromkeys(t.strip().lower() for t in value if t.strip())) if key == "tags" else value)
return self
def reset(self):
with self._lock:
self._stats = {"emitted": 0, "filtered": 0, "errors": 0, "sites": {}}
self._summary_written = False
def is_enabled(self, tags, level="debug"):
tags = [tags] if isinstance(tags, str) else tags
return self.enabled and _LEVELS.get(level, 0) >= _LEVELS.get(self.level, 10) and (not self.tags or "*" in self.tags or bool({t.strip().lower() for t in tags} & set(self.tags)))
def _source(self):
frame = inspect.currentframe()
try:
while frame:
filename = frame.f_code.co_filename
if filename != __file__:
path = Path(filename).resolve()
root = Path(os.getenv("LOGFORGE_PROJECT_ROOT", os.getcwd())).resolve()
try:
filename = path.relative_to(root).as_posix()
except ValueError:
filename = str(path)
return {"file": filename, "line": frame.f_lineno, "function": frame.f_code.co_name, "language": "python"}
frame = frame.f_back
finally:
del frame
def _deliver(self, event):
try:
if len(json.dumps(event)) > 65536:
event["data"] = {"truncated": True, "reason": "payload exceeds 64 KiB"}
self.sink(event)
except Exception:
self._stats["errors"] += 1
def out(self, tag="app", payload=None, *, tags=None, level="debug", name=None, id=None, source=None, _kind="output"):
if not isinstance(tag, str):
payload, tag = tag, "app"
all_tags = list(dict.fromkeys(t.strip().lower() for t in [tag, *(tags or [])] if t.strip()))
with self._lock:
self._summary_written = False
key = id or name or tag
site = self._stats["sites"].setdefault(key, {"name": name or tag, "tags": all_tags, "emitted": 0, "filtered": 0})
if not self.is_enabled(all_tags, level):
self._stats["filtered"] += 1
site["filtered"] += 1
return None
try:
data = _clean(payload() if callable(payload) else payload)
except Exception as exc:
self._stats["errors"] += 1
data = {"diagnostic_error": _clean(exc)}
event = {"_logforge": "event", "schema_version": "1.0", "stream": "logforge.out", "event_id": str(uuid4()),
"run_id": self.run_id, "timestamp": datetime.now(timezone.utc).isoformat(), "service": self.service,
"kind": _kind, "name": name or tag, "site_id": id, "tags": all_tags, "level": level,
"source": source or self._source(), "data": data}
if _kind == "check":
event["passed"] = data is True
self._stats["emitted"] += 1
site["emitted"] += 1
self._deliver(event)
return event
def debug(self, tag, payload=None, **options):
return self.out(tag, payload, **{**options, "level": "debug"})
def info(self, tag, payload=None, **options):
return self.out(tag, payload, **{**options, "level": "info"})
def warn(self, tag, payload=None, **options):
return self.out(tag, payload, **{**options, "level": "warn"})
def error(self, tag, payload=None, **options):
return self.out(tag, payload, **{**options, "level": "error"})
def check(self, tag, name, predicate, **options):
return self.out(tag, predicate, **{**options, "name": name, "level": options.get("level", "info"), "_kind": "check"})
def stats(self):
with self._lock:
return copy.deepcopy(self._stats)
def flush(self):
with self._lock:
if not self._summary_written and (self._stats["emitted"] or self._stats["filtered"]):
self._deliver({"_logforge": "summary", "schema_version": "1.0", "run_id": self.run_id,
"service": self.service, "timestamp": datetime.now(timezone.utc).isoformat(), "stats": self.stats()})
self._summary_written = True
LogForge = _Logger()
atexit.register(LogForge.flush)
class LogForgeHandler(logging.Handler):
"""Attach to Python logging to retain existing logger.debug/info call sites."""
def __init__(self, tag="app", tags=None):
super().__init__()
self.tag, self.tags = tag, tags or []
def emit(self, record):
level = "error" if record.levelno >= 40 else "warn" if record.levelno >= 30 else "info" if record.levelno >= 20 else "debug"
LogForge.out(self.tag, lambda: {"message": record.getMessage()}, tags=self.tags, level=level, name=record.name,
source={"file": record.pathname, "line": record.lineno, "function": record.funcName, "language": "python"})
SHA-256: a616f98026d447f35367a60036e66c1cd03450e34e33b515a56ac418135c0b40