diff --git a/core/services.yaml b/core/services.yaml index 3177a6a..905b6e6 100644 --- a/core/services.yaml +++ b/core/services.yaml @@ -104,6 +104,6 @@ services: # --- 端口重定向(最后启动) --- - name: redirect80 cmd: bash /opt/micro_servers/redirect80/_run.sh - args: [] + args: ["--instance", "redirect80", "--log-level", "info"] depends_on: [waf] sleep_before: 0 diff --git a/redirect80/log_client.py b/redirect80/log_client.py index 8529be7..dc3e905 100644 --- a/redirect80/log_client.py +++ b/redirect80/log_client.py @@ -8,6 +8,7 @@ - 缓冲批量:队列满 50 条或超时 2 秒时自动 flush - 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间 - 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施 + - 日志级别过滤:set_level() 设定阈值,低于阈值的日志直接丢弃 用法(与旧版完全兼容): from log_client import logs @@ -16,6 +17,10 @@ logs.error("redirect80", "端口绑定失败", extra={"errno": 98}) logs.push("redirect80", "WARNING", "confs 不可用,使用本地配置", trace_id="startup", extra={"fallback": "redirect80.ini"}) + + # 设置日志级别(main 启动时调用) + from log_client import set_log_level + set_log_level("warn") # 只输出 warn/error """ import json @@ -37,6 +42,19 @@ _FLUSH_INTERVAL = 2.0 _TIMEOUT = 5 _QUEUE_MAXSIZE = 10000 +# ── 日志级别过滤 ──────────────────────────────────────────────────── +_LEVEL_ORDER = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "WARN": 2, "ERROR": 3, "CRITICAL": 3} +_level_threshold = 1 # 默认 INFO + + +def set_log_level(level: str): + """设置全局日志级别阈值。低于此级别的日志直接丢弃。""" + global _level_threshold + lv = level.upper() + if lv == "WARNING": + lv = "WARN" + _level_threshold = _LEVEL_ORDER.get(lv, 1) + def _safe_json(obj): """安全序列化 extra 字段,不可序列化对象转为 repr 字符串。""" @@ -79,8 +97,11 @@ class LogClient: trace_id: str = None, extra=None): """推送一条日志(非阻塞,立即返回)。 + 低于设定级别的日志直接丢弃。 队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。 """ + if _LEVEL_ORDER.get(level.upper(), 1) < _level_threshold: + return entry = { 'ts': _now_iso(), 'service': service, diff --git a/redirect80/redirect80.py b/redirect80/redirect80.py index c231417..0d2465e 100644 --- a/redirect80/redirect80.py +++ b/redirect80/redirect80.py @@ -7,12 +7,14 @@ redirect80 — HTTP 80 端口重定向服务。 技术选型:Python 内置 http.server + 多线程混合模式,无外部 Web 框架依赖。 日志通过 log_client 异步推送到 db 的 Logs 表(fail-safe)。 -配置(两层加载): +配置(三层加载): 1. confs 强依赖拉取 redirect80.ini(retry_on_refused 阻塞等待) 2. 环境变量覆盖(Docker/k8s 兼容) -confs 不可用时直接退出,不做本地回退。 + 3. 命令行参数(最高优先级) +配置不可用时直接退出,不做本地回退。 """ +import argparse import configparser import os import sys as _sys @@ -22,57 +24,124 @@ from socketserver import ThreadingMixIn # ── 路径设置 ────────────────────────────────────────────────────────── _here = os.path.dirname(os.path.abspath(__file__)) _sys.path.insert(0, os.path.join(_here, "..")) -from log_client import logs # noqa: E402 +from log_client import logs, set_log_level # noqa: E402 -# ── 配置加载(confs 强依赖 → 环境变量覆盖)──────────────────────── -_config = { - "host": "0.0.0.0", - "port": 80, - "redirect_port": 443, - "redirect_code": 301, -} - -# 1. confs 强依赖拉取配置(阻塞等待 confs 就绪,失败则退出) _sys.path.insert(0, os.path.join(_here, "..", "pymsg")) from pymsg import RpcClient # noqa: E402 -_confs_host = os.environ.get("CONFS_HOST", "127.0.0.1") -_confs_port = int(os.environ.get("CONFS_PORT", "8010")) -_confs_client = RpcClient( - _confs_host, _confs_port, "redirect80", - retry_on_refused=True, timeout=5, -) -_data = _confs_client.call("get", {"name": "redirect80.ini"}) -_ini_text = _data.get("content", "") -_ini = configparser.ConfigParser(interpolation=None) -_ini.read_string(_ini_text) -_merge_map = { - ("server", "host"): "host", - ("server", "port"): "port", - ("server", "redirect_port"): "redirect_port", - ("server", "redirect_code"): "redirect_code", -} -for (sec, key), var in _merge_map.items(): - if _ini.has_option(sec, key): - val = _ini.get(sec, key) - _config[var] = type(_config[var])(val) -logs.info("redirect80", "配置已从 confs 加载") +# ── 配置加载 ───────────────────────────────────────────────────────── + +def load_config(argv): + """三层加载: confs → ENV → CLI(最高优先级)。""" + + # argparse 先行:--help 在任何网络调用前处理 + parser = argparse.ArgumentParser( + prog="redirect80", + description="HTTP 80 端口重定向服务", + add_help=False, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="配置优先级: CLI > 环境变量 > confs > 默认值", + ) + parser.add_argument("--host", default=None, + help="绑定地址,默认 0.0.0.0") + parser.add_argument("--port", type=int, default=None, + help="监听端口,默认 80") + parser.add_argument("--redirect-port", type=int, default=None, + help="重定向目标端口,默认 443") + parser.add_argument("--redirect-code", type=int, default=None, + help="GET/HEAD 重定向状态码,默认 301") + parser.add_argument("--log-level", default=None, + help="debug|info|warn|error,默认 info") + parser.add_argument("--instance", default=None, + help="实例名(注册中心用),默认 redirect80") + parser.add_argument("--help", "-h", action="help", + help="显示此帮助并退出") + args = parser.parse_args(argv) + + # 默认值 + config = { + "host": "0.0.0.0", + "port": 80, + "redirect_port": 443, + "redirect_code": 301, + "log_level": "info", + "instance": "redirect80", + } + + # 1. confs 强依赖拉取配置(阻塞等待 confs 就绪,失败则退出) + confs_host = os.environ.get("CONFS_HOST", "127.0.0.1") + confs_port = int(os.environ.get("CONFS_PORT", "8010")) + try: + client = RpcClient( + confs_host, confs_port, "redirect80", + retry_on_refused=True, timeout=5, + ) + data = client.call("get", {"name": "redirect80.ini"}) + ini_text = data.get("content", "") + ini = configparser.ConfigParser(interpolation=None) + ini.read_string(ini_text) + + _merge = { + ("server", "host"): "host", + ("server", "port"): "port", + ("server", "redirect_port"): "redirect_port", + ("server", "redirect_code"): "redirect_code", + } + for (sec, key), var in _merge.items(): + if ini.has_option(sec, key): + val = ini.get(sec, key) + config[var] = type(config[var])(val) + logs.info("redirect80", "配置已从 confs 加载") + except Exception as e: + print(f"[redirect80] confs 获取失败: {e}", file=_sys.stderr) + _sys.exit(1) + + # 2. 环境变量覆盖 + _env_map = { + "REDIRECT80_HOST": "host", + "REDIRECT80_PORT": ("port", int), + "REDIRECT80_REDIRECT_PORT": ("redirect_port", int), + "REDIRECT80_REDIRECT_CODE": ("redirect_code", int), + } + for env_var, mapping in _env_map.items(): + val = os.environ.get(env_var) + if val is not None: + if isinstance(mapping, tuple): + config[mapping[0]] = mapping[1](val) + else: + config[mapping] = val + + # 3. CLI 参数(最高优先级) + if args.host is not None: + config["host"] = args.host + if args.port is not None: + config["port"] = args.port + if args.redirect_port is not None: + config["redirect_port"] = args.redirect_port + if args.redirect_code is not None: + config["redirect_code"] = args.redirect_code + if args.log_level is not None: + config["log_level"] = args.log_level + if args.instance is not None: + config["instance"] = args.instance + + # log_level 合法性校验 + lv = config["log_level"].lower() + if lv == "warning": + lv = "warn" + if lv not in ("debug", "info", "warn", "error"): + print(f"[redirect80] invalid log_level: {config['log_level']} " + f"(must be debug|info|warn|error)", file=_sys.stderr) + _sys.exit(1) + config["log_level"] = lv + + return config + + +# ── 模块级配置(main() 中赋值)────────────────────────────────────── +_config = {} -# 2. 环境变量最高优先级覆盖 -_env_map = { - "REDIRECT80_HOST": "host", - "REDIRECT80_PORT": ("port", int), - "REDIRECT80_REDIRECT_PORT": ("redirect_port", int), - "REDIRECT80_REDIRECT_CODE": ("redirect_code", int), -} -for env_var, mapping in _env_map.items(): - val = os.environ.get(env_var) - if val is not None: - if isinstance(mapping, tuple): - _config[mapping[0]] = mapping[1](val) - else: - _config[mapping] = val # ── 请求处理器 ────────────────────────────────────────────────────────── @@ -110,10 +179,10 @@ class RedirectHandler(BaseHTTPRequestHandler): f"{code} {self.command} {host} → {location}") def do_GET(self): - self._redirect(301) + self._redirect(_config["redirect_code"]) def do_HEAD(self): - self._redirect(301) + self._redirect(_config["redirect_code"]) def do_POST(self): self._redirect(308) @@ -158,17 +227,24 @@ class ThreadingRedirectServer(ThreadingMixIn, HTTPServer): def main(): + global _config + _config = load_config(_sys.argv[1:]) + + # 设置日志级别过滤 + set_log_level(_config["log_level"]) + host = _config["host"] port = int(_config["port"]) server = ThreadingRedirectServer((host, port), RedirectHandler) logs.info("redirect80", - f"HTTP→HTTPS 重定向服务启动 " + f"服务启动 instance={_config['instance']} " f"http://{host}:{port} → https://*:{_config['redirect_port']}") - print(f"[redirect80] 监听 http://{host}:{port} " - f"→ 重定向到 :{_config['redirect_port']}", file=_sys.stderr) + print(f"[redirect80] instance={_config['instance']} log-level={_config['log_level']} " + f"监听 http://{host}:{port} → 重定向到 :{_config['redirect_port']}", + file=_sys.stderr) try: server.serve_forever()