-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlog_setup.py
More file actions
103 lines (78 loc) · 3.24 KB
/
Copy pathlog_setup.py
File metadata and controls
103 lines (78 loc) · 3.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
"""Centralized logging setup.
Every process gets exactly one log file, named after the service it runs.
Previously each module attached its own RotatingFileHandler, which meant
several processes rotated the same file independently and lost lines on
rollover (bot_logic and signal_handler both wrote to trading.log).
Entry points call configure("<service>") before importing anything that
logs; library modules just call get_logger(__name__).
"""
import logging
import os
import re
from logging.handlers import RotatingFileHandler
ROOT_LOGGER_NAME = "tradex"
_FORMAT = "%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
# Payload keys whose values must never reach a log file.
_SECRET_KEYS = {"pin", "password", "passwd", "secret", "apikey", "api_key", "token"}
_configured = False
def log_directory():
"""Log destination, which differs inside the container."""
override = os.getenv("TRADEX_LOG_DIR", "").strip()
if override:
return override
return "/app/logs" if os.getenv("DOCKER_ENV") else "logs"
def configure(service):
"""Attach handlers to the shared parent logger. Idempotent."""
global _configured
logger = logging.getLogger(ROOT_LOGGER_NAME)
if _configured:
return logger
directory = log_directory()
os.makedirs(directory, exist_ok=True)
formatter = logging.Formatter(_FORMAT)
file_handler = RotatingFileHandler(
os.path.join(directory, f"{service}.log"), maxBytes=2_000_000, backupCount=5
)
file_handler.setFormatter(formatter)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.setLevel(logging.INFO)
# The logger object outlives a module reload, so drop anything stale
# rather than accumulating duplicate handlers.
for stale in list(logger.handlers):
logger.removeHandler(stale)
stale.close()
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# Handlers live here, not on the root logger, so nothing is emitted twice.
logger.propagate = False
_configured = True
return logger
def get_logger(name):
"""Return a child logger. Falls back to a generic file if no entry point ran."""
if not _configured:
configure(os.getenv("TRADEX_SERVICE", ROOT_LOGGER_NAME))
return logging.getLogger(f"{ROOT_LOGGER_NAME}.{name}")
# Secrets embedded in free text, e.g. an email subject carrying the raw
# alert JSON. Matches both "PIN": "abc" and "PIN": 778899.
_SECRET_TEXT_RE = re.compile(
r"""(["']?\b(?:pin|password|passwd|secret|api_?key|token)\b["']?\s*[:=]\s*)"""
r"""(["'][^"']*["']|[^\s,}\]]+)""",
re.IGNORECASE,
)
def redact_text(text):
"""Mask secret values inside an arbitrary string.
For text that has not been parsed into a dict yet. Redact before
truncating, or a shortened line can still expose the value.
"""
if not text:
return text
return _SECRET_TEXT_RE.sub(lambda m: m.group(1) + '"***"', str(text))
def redact(payload):
"""Copy of a signal payload with secrets masked, safe to log."""
if not isinstance(payload, dict):
return payload
return {
key: ("***" if str(key).lower().replace("-", "_") in _SECRET_KEYS else value)
for key, value in payload.items()
}