Просмотр исходного кода

feat(obs): 接入阿里云 SLS 日志上报 + wechat_position 补 banner 版位

- 新增 tools/sls_setup.py:QueuedLogHandler 异步上报,凭证缺失自动降级为本地 file,不阻塞主链路
- execute_creation_once/apply 入口挂 SLS handler
- config 加 SLS_* 配置段(env 读取)+ wechat_position 补 1024794 小程序 banner
刘立冬 9 часов назад
Родитель
Сommit
5a4b5ef5ae

+ 31 - 7
examples/auto_put_ad_mini/config.py

@@ -414,24 +414,26 @@ SITE_SET_COMBINATIONS = [
 #   · SMART_TARGETING_NONE = 已关 · SMART_TARGETING_AUTO = 智能定向中
 SMART_TARGETING_MODE = "SMART_TARGETING_MANUAL"
 
-# --- WECHAT_POSITION 定投场景(2026-06-11 用户确认:精简为生产 4 项)---
-# 历史:9 项 preset(来自参考广告 77868332)→ 实际生产删除重建走 3 项 → 本期 + 1024797 共 4
+# --- WECHAT_POSITION 定投场景(2026-06-11 用户确认:生产 5 项小程序版位)---
+# 历史:9 项 preset(来自参考广告 77868332)→ 实际生产删除重建走 3 项 → 本期补 1024797/1024794 共 5
 # 中文映射通过 tools.scene_spec.get_wechat_position_tags(account_id) 运行时查询(进程内缓存 1h)
 #
-# 业务生效场景(小程序流量为主,公众号文章历史 SOP 但本期未启用):
+# 业务生效场景(全部小程序流量位):
 #   1024795 小程序激励式广告
 #   1024796 小程序插屏广告
 #   2100748 小程序原生广告
 #   1024797 小程序封面广告(2026-06-11 新增)
+#   1024794 小程序banner广告(2026-06-11 新增 — 见下警告)
 #
-# ⚠️ 腾讯下线警告(2026-06-11 实测):1024794 小程序banner广告**已下线**,千万别加
-#    → /adgroups/update 会报 code=1800945 "banner 广告选项已下线"
-#    → scene_spec_tags/get 接口仍返回它(腾讯枚举表未及时清理),get_wechat_position_tags 能拿到 ≠ 能用
+# ⚠️ 1024794 banner 警告(2026-06-11 实测,用户决策保留):
+#    → /adgroups/update 接口已下线,报 code=1800945 "banner 广告选项已下线,请选择其他选项"
+#    → /adgroups/add 接口未实测,scene_spec_tags/get 仍返回此 ID(账户级仍开放)
+#    → 若下次 Phase 0 POST add 也报 1800945,从此列表移除 1024794 即可
 #
 # ⚠️ 创建后锁死(2026-06-11 实测复现):wechat_position 一旦创建,update 报 code=36840
 #    → 要新增场景必须删除重建广告,代价是丢失已挂创意 + 学习数据
 WECHAT_POSITION_TARGETED_PRESET = [
-    1024795, 1024796, 2100748, 1024797,
+    1024795, 1024796, 2100748, 1024797, 1024794,
 ]
 
 # 1 账户广告数(2026-06-09 用户确认:2 条,一条有 wechat_position 定投,一条无定投)
@@ -595,3 +597,25 @@ CREATION_APPROVAL_TIMEOUT_MINUTES = 120
 # 飞书 chat_id:复用现有调控审批群(FEISHU_OPERATOR_CHAT_ID)
 # 等创意审批和调控审批要分群时,再加 FEISHU_CREATION_CHAT_ID 覆盖
 
+# ═══════════════════════════════════════════
+# 阿里云 SLS 日志上报(2026-06-11 接入,K8s pod 中 SDK 直发)
+# ═══════════════════════════════════════════
+# 从 os.environ 读取(.env 已 load_dotenv),任一缺失 → SLS_ENABLED=False → 不上报,只本地 file
+SLS_ENDPOINT          = os.environ.get("SLS_ENDPOINT", "")          # 例 cn-hangzhou.log.aliyuncs.com
+SLS_ACCESS_KEY_ID     = os.environ.get("SLS_ACCESS_KEY_ID", "")     # RAM 子账号 AK(只授 Log:PutLogs 权限)
+SLS_ACCESS_KEY_SECRET = os.environ.get("SLS_ACCESS_KEY_SECRET", "") # 同上的 SK
+SLS_PROJECT           = os.environ.get("SLS_PROJECT", "auto-put-tecent")
+SLS_LOGSTORE          = os.environ.get("SLS_LOGSTORE", "info-log")
+
+# 全开:任一缺失则降级为 False,主链路不受影响
+SLS_ENABLED = bool(SLS_ENDPOINT and SLS_ACCESS_KEY_ID and SLS_ACCESS_KEY_SECRET
+                   and SLS_PROJECT and SLS_LOGSTORE)
+
+# 上报等级 — 用户决策(2026-06-11):所有 INFO+ 上报
+# 注:material_recall 每个 landing 打 4 条 INFO,日 cron 量级约 5k-20k 条,SLS 流量成本几 RMB/月
+SLS_LOG_LEVEL = "INFO"
+
+# QueuedLogHandler 内部异步队列参数(SDK 默认 + 微调,避免长连接 idle 断)
+SLS_BATCH_SIZE_MAX = 1024      # 单次 PutLogs 最多条数
+SLS_PUT_WAIT_MS    = 2000      # 队列攒到 batch_size 或等 2s flush 一次
+

+ 7 - 0
examples/auto_put_ad_mini/execute_creation_apply.py

@@ -255,6 +255,13 @@ def main() -> int:
     for noisy in ("httpx", "httpcore", "config", "db.config"):
         logging.getLogger(noisy).setLevel(logging.WARNING)
 
+    # SLS 上报(2026-06-11 接入)— 配置缺失自动降级
+    try:
+        from tools.sls_setup import attach_sls_handler
+        attach_sls_handler()
+    except Exception as e:
+        logger.warning("[sls] 挂载异常(降级为本地 only):%s", e)
+
     if len(sys.argv) < 2:
         logger.error("用法: python execute_creation_apply.py <pending_records.json>")
         return 1

+ 7 - 0
examples/auto_put_ad_mini/execute_creation_once.py

@@ -79,6 +79,13 @@ def _setup_logging() -> None:
     for noisy in ("httpx", "httpcore", "config", "db.config"):
         logging.getLogger(noisy).setLevel(logging.WARNING)
 
+    # SLS 上报(2026-06-11 接入,K8s pod 直发)— 配置缺失自动降级,不阻塞主链路
+    try:
+        from tools.sls_setup import attach_sls_handler
+        attach_sls_handler()
+    except Exception as e:
+        logger.warning("[sls] 挂载异常(降级为本地 only):%s", e)
+
 
 def _fetch_existing_fingerprints_for_account(account_id: int) -> set[str]:
     """Task 27:拉账户下所有非 DELETED 广告 → 反推 fingerprint 集合(预校验唯一性)。

+ 108 - 0
examples/auto_put_ad_mini/tools/sls_setup.py

@@ -0,0 +1,108 @@
+"""阿里云 SLS 日志上报接入(2026-06-11)。
+
+数据流:
+  Python logger → 本地 FileHandler(全量保留)
+                → QueuedLogHandler(aliyun-log-python-sdk 自带,异步队列 + 批量 flush)
+                                                 → SLS Logstore
+
+设计原则:
+  · 配置缺失 → SLS_ENABLED=False → 不上报,主链路不受影响(降级)
+  · 上报失败 → SDK 内部 retry + 异常吞掉(不抛出),主链路不受影响
+  · INFO+ 全量上报(用户 2026-06-11 决策,日量级约 5k-20k 条)
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import Optional
+
+logger = logging.getLogger(__name__)
+
+
+def attach_sls_handler(root_logger: Optional[logging.Logger] = None) -> bool:
+    """给 root_logger 挂上 SLS QueuedLogHandler(若 SLS_ENABLED)。
+
+    Returns:
+        True 已挂上 / False 跳过(配置缺失 / SDK 缺失 / 异常)
+    """
+    if root_logger is None:
+        root_logger = logging.getLogger()
+
+    try:
+        from config import (
+            SLS_ENABLED,
+            SLS_ENDPOINT,
+            SLS_ACCESS_KEY_ID,
+            SLS_ACCESS_KEY_SECRET,
+            SLS_PROJECT,
+            SLS_LOGSTORE,
+            SLS_LOG_LEVEL,
+        )
+    except ImportError as e:
+        logger.warning("[sls] config 缺 SLS_* 变量,跳过:%s", e)
+        return False
+
+    if not SLS_ENABLED:
+        logger.info("[sls] SLS_ENABLED=False(凭证未完整配置),跳过上报")
+        return False
+
+    try:
+        from aliyun.log import QueuedLogHandler
+    except ImportError:
+        logger.warning("[sls] aliyun-log-python-sdk 未安装,跳过上报(.venv/bin/pip3 install aliyun-log-python-sdk)")
+        return False
+
+    try:
+        sls_handler = QueuedLogHandler(
+            end_point=SLS_ENDPOINT,
+            access_key_id=SLS_ACCESS_KEY_ID,
+            access_key=SLS_ACCESS_KEY_SECRET,
+            project=SLS_PROJECT,
+            log_store=SLS_LOGSTORE,
+            # 自动提取 record 标准字段
+            fields=["levelname", "name", "filename", "lineno", "thread", "process"],
+            # message 中 k=v 形式自动解析为独立字段(便于 SLS 索引/查询)
+            extract_kv=True,
+        )
+        sls_handler.setLevel(getattr(logging, SLS_LOG_LEVEL, logging.INFO))
+        sls_handler.setFormatter(logging.Formatter(
+            "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
+            datefmt="%Y-%m-%d %H:%M:%S",
+        ))
+        root_logger.addHandler(sls_handler)
+        logger.info(
+            "[sls] 已挂载 QueuedLogHandler → %s/%s/%s (level=%s)",
+            SLS_ENDPOINT, SLS_PROJECT, SLS_LOGSTORE, SLS_LOG_LEVEL,
+        )
+        return True
+    except Exception as e:
+        logger.warning("[sls] 挂载失败(降级为不上报,主链路不受影响):%s", e)
+        return False
+
+
+def smoke_test() -> bool:
+    """烟囱测试:发一条 INFO + 一条 WARNING + 一条 ERROR,等 3 秒让队列 flush。
+
+    用法:python -c "from tools.sls_setup import smoke_test; smoke_test()"
+    然后去 SLS 控制台 logstore=info-log 查看是否收到。
+    """
+    import time
+
+    logging.basicConfig(
+        level=logging.INFO,
+        format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
+        datefmt="%H:%M:%S",
+    )
+    ok = attach_sls_handler()
+    if not ok:
+        logger.error("[smoke_test] SLS 未挂上,无法测试")
+        return False
+
+    test_logger = logging.getLogger("sls_smoke_test")
+    test_logger.info("[smoke_test] hello SLS - INFO 消息 event=smoke_info")
+    test_logger.warning("[smoke_test] hello SLS - WARNING 消息 event=smoke_warn")
+    test_logger.error("[smoke_test] hello SLS - ERROR 消息 event=smoke_error code=999")
+    logger.info("[smoke_test] 已发 3 条,等 3s 让 QueuedLogHandler flush")
+    time.sleep(3)
+    logger.info("[smoke_test] 测试完成,请去 SLS 控制台 logstore 查看")
+    return True