调整 日志输出格式

This commit is contained in:
2026-04-29 16:28:23 +08:00
parent 8e4c2eef98
commit 02b3470075
8 changed files with 303 additions and 142 deletions
@@ -4,8 +4,10 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from dataclasses import dataclass
from datetime import datetime from datetime import datetime
import logging import logging
import os
from pathlib import Path from pathlib import Path
import time import time
from typing import Any from typing import Any
@@ -16,6 +18,7 @@ import psycopg2
from ..config import DbConfig, build_app_config, load_analytics_db_config, load_raw_db_config from ..config import DbConfig, build_app_config, load_analytics_db_config, load_raw_db_config
from ..feishu.client import FeishuApiError, FeishuClient from ..feishu.client import FeishuApiError, FeishuClient
from ..logging_utils import configure_logging as configure_app_logging from ..logging_utils import configure_logging as configure_app_logging
from ..logging_utils import exception_fields
from ..logging_utils import log_event as emit_log_event from ..logging_utils import log_event as emit_log_event
from ..paths import LOG_DIR, REPORT_DIR from ..paths import LOG_DIR, REPORT_DIR
@@ -190,18 +193,19 @@ LOG_FIELD_LABELS = {
"database": "数据库", "database": "数据库",
"deleted_records": "删除记录数", "deleted_records": "删除记录数",
"elapsed_ms": "耗时毫秒", "elapsed_ms": "耗时毫秒",
"env_path": "环境文件", "error": "错误",
"error_type": "异常类型",
"existing_records": "已有记录数", "existing_records": "已有记录数",
"filtered_rows": "筛选后行数", "filtered_rows": "筛选后行数",
"join_key": "关联字段", "join_key": "关联字段",
"log_file": "日志文件",
"matched_rows": "匹配行数", "matched_rows": "匹配行数",
"output": "输出文件", "missing_join_key_rows": "缺少关联字段行数",
"report_output": "本地报表输出",
"raw_rows": "Raw行数", "raw_rows": "Raw行数",
"report_rows": "报表行数", "report_rows": "报表行数",
"rows": "行数", "rows": "行数",
"stage": "阶段",
"table": "表", "table": "表",
"table_id": "表ID",
"table_name": "表名", "table_name": "表名",
"unmatched_rows": "未匹配行数", "unmatched_rows": "未匹配行数",
"warehouse_count": "仓库数量", "warehouse_count": "仓库数量",
@@ -387,13 +391,18 @@ def ensure_batch_inventory_fields(
def sync_report_to_feishu(df: pd.DataFrame, table_name: str = BATCH_FEISHU_TABLE_NAME) -> dict[str, Any]: def sync_report_to_feishu(df: pd.DataFrame, table_name: str = BATCH_FEISHU_TABLE_NAME) -> dict[str, Any]:
started_at = time.monotonic()
app_config = build_app_config() app_config = build_app_config()
client = FeishuClient(app_config) client = FeishuClient(app_config)
log_event(logging.INFO, "开始获取飞书访问令牌", stage="feishu_auth")
access_token = client.get_tenant_access_token() access_token = client.get_tenant_access_token()
log_event(logging.INFO, "飞书访问令牌获取完成", stage="feishu_auth")
log_event(logging.INFO, "开始解析飞书Base信息", stage="feishu_base")
base_info = client.parse_base_info(client.build_base_url(), access_token) base_info = client.parse_base_info(client.build_base_url(), access_token)
app_token = base_info["app_token"] or "" app_token = base_info["app_token"] or ""
client.enable_advanced_permission(access_token, app_token) client.enable_advanced_permission(access_token, app_token)
tables = client.list_tables(access_token, app_token) tables = client.list_tables(access_token, app_token)
log_event(logging.INFO, "飞书表列表读取完成", stage="feishu_tables", rows=len(tables))
target_table = next((table for table in tables if table.get("name") == table_name), None) target_table = next((table for table in tables if table.get("name") == table_name), None)
if not target_table: if not target_table:
raise FeishuApiError(f"未找到目标飞书表: {table_name}") raise FeishuApiError(f"未找到目标飞书表: {table_name}")
@@ -401,7 +410,7 @@ def sync_report_to_feishu(df: pd.DataFrame, table_name: str = BATCH_FEISHU_TABLE
if not table_id: if not table_id:
raise FeishuApiError(f"目标飞书表缺少 table_id: {table_name}") raise FeishuApiError(f"目标飞书表缺少 table_id: {table_name}")
log_event(logging.INFO, "开始同步飞书批次库存表", table_name=table_name, table_id=table_id, report_rows=len(df)) log_event(logging.INFO, "开始同步飞书批次库存表", stage="feishu_sync", table_name=table_name, report_rows=len(df))
available_fields = ensure_batch_inventory_fields(client, access_token, app_token, table_id) available_fields = ensure_batch_inventory_fields(client, access_token, app_token, table_id)
existing_records = client.list_records(access_token, app_token, table_id) existing_records = client.list_records(access_token, app_token, table_id)
existing_record_ids = [record.get("record_id") for record in existing_records if record.get("record_id")] existing_record_ids = [record.get("record_id") for record in existing_records if record.get("record_id")]
@@ -417,12 +426,13 @@ def sync_report_to_feishu(df: pd.DataFrame, table_name: str = BATCH_FEISHU_TABLE
log_event( log_event(
logging.INFO, logging.INFO,
"飞书批次库存表同步完成", "飞书批次库存表同步完成",
stage="feishu_sync",
table_name=table_name, table_name=table_name,
table_id=table_id,
existing_records=len(existing_record_ids), existing_records=len(existing_record_ids),
deleted_records=deleted_count, deleted_records=deleted_count,
created_records=created_count, created_records=created_count,
available_fields=", ".join(available_fields), available_fields=", ".join(available_fields),
elapsed_ms=int((time.monotonic() - started_at) * 1000),
) )
return { return {
"table_name": table_name, "table_name": table_name,
@@ -436,7 +446,7 @@ def sync_report_to_feishu(df: pd.DataFrame, table_name: str = BATCH_FEISHU_TABLE
def query_dataframe(db_config: DbConfig, sql: str, *, database_name: str) -> pd.DataFrame: def query_dataframe(db_config: DbConfig, sql: str, *, database_name: str) -> pd.DataFrame:
started_at = time.monotonic() started_at = time.monotonic()
log_event(logging.INFO, "开始查询数据库", database=database_name) log_event(logging.INFO, "开始查询数据库", stage="database", database=database_name)
with psycopg2.connect( with psycopg2.connect(
host=db_config.host, host=db_config.host,
port=db_config.port, port=db_config.port,
@@ -450,6 +460,7 @@ def query_dataframe(db_config: DbConfig, sql: str, *, database_name: str) -> pd.
log_event( log_event(
logging.INFO, logging.INFO,
"数据库查询完成", "数据库查询完成",
stage="database",
database=database_name, database=database_name,
rows=len(df), rows=len(df),
elapsed_ms=int((time.monotonic() - started_at) * 1000), elapsed_ms=int((time.monotonic() - started_at) * 1000),
@@ -476,7 +487,7 @@ def extract_raw_codes(df: pd.DataFrame) -> list[str]:
.drop_duplicates() .drop_duplicates()
.tolist() .tolist()
) )
log_event(logging.INFO, "已提取Raw商家编码集合", rows=len(codes), join_key="商家编码=统一规格编码") log_event(logging.INFO, "已提取Raw商家编码集合", stage="prepare", rows=len(codes), join_key="商家编码=统一规格编码")
return codes return codes
@@ -504,8 +515,10 @@ def filter_batch_inventory(df: pd.DataFrame) -> pd.DataFrame:
log_event( log_event(
logging.INFO, logging.INFO,
"Raw批次库存筛选完成", "Raw批次库存筛选完成",
stage="prepare",
raw_rows=len(df), raw_rows=len(df),
filtered_rows=len(filtered), filtered_rows=len(filtered),
missing_join_key_rows=int(missing_join_key),
warehouse_count=len(WAREHOUSE_FILTER), warehouse_count=len(WAREHOUSE_FILTER),
) )
return filtered return filtered
@@ -514,7 +527,7 @@ def filter_batch_inventory(df: pd.DataFrame) -> pd.DataFrame:
def prepare_analytics_stock(df: pd.DataFrame) -> pd.DataFrame: def prepare_analytics_stock(df: pd.DataFrame) -> pd.DataFrame:
prepared = df.copy() prepared = df.copy()
prepared["关联编码"] = prepared["统一规格编码"].map(normalize_code) prepared["关联编码"] = prepared["统一规格编码"].map(normalize_code)
log_event(logging.INFO, "店铺锁定库存整理完成", rows=len(prepared), join_key="统一规格编码") log_event(logging.INFO, "店铺锁定库存整理完成", stage="prepare", rows=len(prepared), join_key="统一规格编码")
return prepared return prepared
@@ -589,6 +602,7 @@ def build_report(batch_df: pd.DataFrame, analytics_df: pd.DataFrame) -> pd.DataF
log_event( log_event(
logging.INFO, logging.INFO,
"批次库存与店铺锁定库存汇总完成", "批次库存与店铺锁定库存汇总完成",
stage="build_report",
report_rows=len(report), report_rows=len(report),
matched_rows=int(matched_rows), matched_rows=int(matched_rows),
unmatched_rows=int(unmatched_rows), unmatched_rows=int(unmatched_rows),
@@ -597,7 +611,7 @@ def build_report(batch_df: pd.DataFrame, analytics_df: pd.DataFrame) -> pd.DataF
return report return report
def save_report(df: pd.DataFrame, output_path: Path) -> None: def save_report(df: pd.DataFrame, output_path: Path, *, log_output_path: bool = True) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True) output_path.parent.mkdir(parents=True, exist_ok=True)
started_at = time.monotonic() started_at = time.monotonic()
suffix = output_path.suffix.lower() suffix = output_path.suffix.lower()
@@ -605,13 +619,14 @@ def save_report(df: pd.DataFrame, output_path: Path) -> None:
df.to_csv(output_path, index=False, encoding="utf-8-sig") df.to_csv(output_path, index=False, encoding="utf-8-sig")
else: else:
df.to_excel(output_path, index=False) df.to_excel(output_path, index=False)
log_event( fields: dict[str, Any] = {
logging.INFO, "report_rows": len(df),
"报表输出完成", "elapsed_ms": int((time.monotonic() - started_at) * 1000),
output=output_path, }
report_rows=len(df), if log_output_path:
elapsed_ms=int((time.monotonic() - started_at) * 1000), fields["output"] = output_path
) fields["stage"] = "report_output"
log_event(logging.INFO, "报表输出完成", **fields)
def default_output_path() -> Path: def default_output_path() -> Path:
@@ -636,23 +651,33 @@ def parse_args() -> argparse.Namespace:
action="store_true", action="store_true",
help="只生成本地报表,不同步到飞书", help="只生成本地报表,不同步到飞书",
) )
parser.add_argument(
"--skip-report-output",
action="store_true",
help="不生成本地报表文件,仅保留飞书同步等后续处理",
)
parser.add_argument(
"--safe-logs",
action="store_true",
help="隐藏环境文件路径、日志文件路径、飞书表ID等配置细节",
)
return parser.parse_args() return parser.parse_args()
def main() -> int: def main() -> int:
configure_logging() configure_logging()
started_at = time.monotonic() started_at = time.monotonic()
try:
args = parse_args() args = parse_args()
output_path = Path(args.output).expanduser().resolve() safe_logs = args.safe_logs or os.getenv("INVENTORY_SAFE_LOGS") == "1"
raw_config, env_path = load_raw_db_config() output_path = None if args.skip_report_output else Path(args.output).expanduser().resolve()
report_output = "skipped" if output_path is None else "enabled"
log_event(logging.INFO, "批次库存统计任务启动", stage="start", report_output=report_output)
raw_config, _ = load_raw_db_config()
analytics_config, _ = load_analytics_db_config() analytics_config, _ = load_analytics_db_config()
log_event( log_event(logging.INFO, "配置加载完成", stage="config")
logging.INFO,
"批次库存统计任务启动",
env_path=env_path,
output=output_path,
log_file=LOG_FILE,
)
raw_df = query_dataframe(raw_config, RAW_BATCH_SQL, database_name="Raw") raw_df = query_dataframe(raw_config, RAW_BATCH_SQL, database_name="Raw")
raw_codes = extract_raw_codes(raw_df) raw_codes = extract_raw_codes(raw_df)
analytics_sql = build_analytics_lock_sql(raw_codes) analytics_sql = build_analytics_lock_sql(raw_codes)
@@ -660,19 +685,33 @@ def main() -> int:
filtered_batch_df = filter_batch_inventory(raw_df) filtered_batch_df = filter_batch_inventory(raw_df)
prepared_analytics_df = prepare_analytics_stock(analytics_df) prepared_analytics_df = prepare_analytics_stock(analytics_df)
report_df = build_report(filtered_batch_df, prepared_analytics_df) report_df = build_report(filtered_batch_df, prepared_analytics_df)
save_report(report_df, output_path)
if output_path is None:
log_event(logging.INFO, "已跳过本地报表输出", stage="report_output", report_rows=len(report_df))
else:
save_report(report_df, output_path, log_output_path=not safe_logs)
if args.skip_feishu_sync: if args.skip_feishu_sync:
log_event(logging.INFO, "已跳过飞书批次库存表同步", table_name=args.feishu_table) log_event(logging.INFO, "已跳过飞书批次库存表同步", stage="feishu_sync", table_name=args.feishu_table)
else: else:
sync_report_to_feishu(report_df, table_name=args.feishu_table) sync_report_to_feishu(report_df, table_name=args.feishu_table)
log_event( log_event(
logging.INFO, logging.INFO,
"批次库存统计任务完成", "批次库存统计任务完成",
output=output_path, stage="finish",
report_rows=len(report_df), report_rows=len(report_df),
elapsed_ms=int((time.monotonic() - started_at) * 1000), elapsed_ms=int((time.monotonic() - started_at) * 1000),
) )
return 0 return 0
except Exception as exc: # noqa: BLE001
log_event(
logging.ERROR,
"批次库存统计任务失败",
stage="failed",
**exception_fields(exc),
elapsed_ms=int((time.monotonic() - started_at) * 1000),
)
return 1
if __name__ == "__main__": if __name__ == "__main__":
+3 -12
View File
@@ -113,13 +113,8 @@ class RawDbConfig:
@classmethod @classmethod
def from_env(cls) -> "RawDbConfig": def from_env(cls) -> "RawDbConfig":
config, env_path = load_raw_db_config() config, _ = load_raw_db_config()
log_event( log_event(logging.INFO, "已加载Raw数据库配置")
logging.INFO,
"已加载Raw数据库配置",
env_path=env_path,
database=config.name,
)
return cls( return cls(
host=config.host, host=config.host,
port=config.port, port=config.port,
@@ -129,11 +124,7 @@ class RawDbConfig:
) )
def connect(self): def connect(self):
log_event( log_event(logging.INFO, "开始连接Raw数据库")
logging.INFO,
"开始连接Raw数据库",
database=self.name,
)
return psycopg2.connect( return psycopg2.connect(
host=self.host, host=self.host,
port=self.port, port=self.port,
@@ -1,12 +1,14 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from pathlib import Path from pathlib import Path
import sys import sys
import time import time
from .cli import main as channel_inventory_sync_main from .cli import main as channel_inventory_sync_main
from ..logging_utils import configure_logging, log_event from ..logging_utils import configure_logging, log_event
from ..logging_utils import exception_fields
from ..paths import LOG_DIR from ..paths import LOG_DIR
@@ -18,36 +20,43 @@ LOG_FIELD_LABELS = {
"argv": "参数", "argv": "参数",
"cwd": "工作目录", "cwd": "工作目录",
"elapsed_ms": "耗时毫秒", "elapsed_ms": "耗时毫秒",
"error": "错误",
"error_type": "异常类型",
"exit_code": "退出码", "exit_code": "退出码",
"log_file": "日志文件", "log_file": "日志文件",
"stage": "阶段",
} }
def main() -> int: def main() -> int:
configure_logging(LOG_FILE, stream=sys.stderr) configure_logging(LOG_FILE, stream=sys.stderr)
started_at = time.monotonic() started_at = time.monotonic()
log_event( start_fields = {"argv": sys.argv[1:], "stage": "start"}
logger, if os.getenv("INVENTORY_SAFE_LOGS") != "1":
LOG_FIELD_LABELS, start_fields.update(cwd=Path.cwd(), log_file=LOG_FILE)
logging.INFO, log_event(logger, LOG_FIELD_LABELS, logging.INFO, "飞书同步入口启动", **start_fields)
"飞书同步入口启动",
argv=sys.argv[1:],
cwd=Path.cwd(),
log_file=LOG_FILE,
)
try: try:
exit_code = channel_inventory_sync_main() exit_code = channel_inventory_sync_main()
except SystemExit as exc: except SystemExit as exc:
exit_code = int(exc.code or 0) if isinstance(exc.code, int) else 1 exit_code = int(exc.code or 0) if isinstance(exc.code, int) else 1
except Exception: except Exception as exc: # noqa: BLE001
logger.exception("事件=%s", "飞书同步入口异常") log_event(
logger,
LOG_FIELD_LABELS,
logging.ERROR,
"渠道库存同步入口异常",
stage="failed",
**exception_fields(exc),
elapsed_ms=int((time.monotonic() - started_at) * 1000),
)
return 1 return 1
log_event( log_event(
logger, logger,
LOG_FIELD_LABELS, LOG_FIELD_LABELS,
logging.INFO if exit_code == 0 else logging.ERROR, logging.INFO if exit_code == 0 else logging.ERROR,
"飞书同步入口完成", "渠道库存同步入口完成" if exit_code == 0 else "渠道库存同步入口失败",
stage="finish" if exit_code == 0 else "failed",
exit_code=exit_code, exit_code=exit_code,
elapsed_ms=int((time.monotonic() - started_at) * 1000), elapsed_ms=int((time.monotonic() - started_at) * 1000),
) )
+55 -4
View File
@@ -8,15 +8,65 @@ import sys
from typing import Any from typing import Any
SENSITIVE_PATTERN = re.compile(r"(?i)(password|passwd|secret|token|authorization)\s*[:=]\s*([^\s,;]+)") SENSITIVE_KEYWORDS = (
"app_id",
"app_token",
"authorization",
"base_url",
"db_host",
"db_name",
"db_password",
"db_port",
"db_user",
"env_path",
"field_id",
"host",
"password",
"passwd",
"secret",
"selected_table_id",
"selected_view_id",
"table_id",
"token",
"url",
"user",
)
SENSITIVE_PATTERN = re.compile(r"(?i)(password|passwd|secret|token|authorization|app_id|app_token)\s*[:=]\s*([^\s,;]+)")
FEISHU_APP_PATH_PATTERN = re.compile(r"(/bitable/v1/apps/)[^/?\s]+")
FEISHU_QUERY_TOKEN_PATTERN = re.compile(r"([?&]token=)[^&\s]+")
def is_sensitive_key(key: str) -> bool:
normalized = key.lower()
return any(keyword in normalized for keyword in SENSITIVE_KEYWORDS)
def sanitize_value(value: Any) -> Any: def sanitize_value(value: Any) -> Any:
if isinstance(value, Path):
return value.name
if isinstance(value, BaseException):
return sanitize_value(str(value))
if isinstance(value, dict):
return {
key: "***" if is_sensitive_key(str(key)) else sanitize_value(item)
for key, item in value.items()
}
if isinstance(value, (list, tuple, set)):
return [sanitize_value(item) for item in value]
if isinstance(value, str): if isinstance(value, str):
return SENSITIVE_PATTERN.sub(r"\1=***", value) value = SENSITIVE_PATTERN.sub(r"\1=***", value)
value = FEISHU_APP_PATH_PATTERN.sub(r"\1***", value)
return FEISHU_QUERY_TOKEN_PATTERN.sub(r"\1***", value)
return value return value
def exception_fields(exc: BaseException) -> dict[str, Any]:
return {
"error_type": exc.__class__.__name__,
"error": sanitize_value(str(exc)),
}
def configure_logging(log_file: Path, *, stream = None, level: int = logging.INFO) -> None: def configure_logging(log_file: Path, *, stream = None, level: int = logging.INFO) -> None:
log_file.parent.mkdir(parents=True, exist_ok=True) log_file.parent.mkdir(parents=True, exist_ok=True)
formatter = logging.Formatter( formatter = logging.Formatter(
@@ -37,7 +87,8 @@ def configure_logging(log_file: Path, *, stream = None, level: int = logging.INF
def log_event(logger: logging.Logger, labels: dict[str, str], level: int, event: str, **fields: Any) -> None: def log_event(logger: logging.Logger, labels: dict[str, str], level: int, event: str, **fields: Any) -> None:
safe_event = sanitize_value(event) safe_event = sanitize_value(event)
details = " ".join( details = " ".join(
f"{labels.get(key, key)}={sanitize_value(value)!r}" for key, value in fields.items() f"{labels.get(key, key)}={sanitize_value(value)!r}"
for key, value in fields.items()
if value is not None and not is_sensitive_key(key)
) )
logger.log(level, "事件=%s%s", safe_event, f" {details}" if details else "") logger.log(level, "事件=%s%s", safe_event, f" {details}" if details else "")
+52 -27
View File
@@ -3,10 +3,14 @@ from __future__ import annotations
import argparse import argparse
import json import json
import logging import logging
import re import os
import time import time
from typing import Any from typing import Any
from inventory_analytics.logging_utils import exception_fields
from inventory_analytics.logging_utils import log_event as emit_log_event
from inventory_analytics.logging_utils import sanitize_value
from .feishu import FeishuClient from .feishu import FeishuClient
from .postgres import query_postgres from .postgres import query_postgres
from .service import run_feishu_summary, run_sync_brand from .service import run_feishu_summary, run_sync_brand
@@ -14,33 +18,24 @@ from .settings import build_app_config
logger = logging.getLogger("inventory_sync.cli") logger = logging.getLogger("inventory_sync.cli")
SENSITIVE_PATTERN = re.compile(r"(?i)(password|passwd|secret|token|authorization)\s*[:=]\s*([^\s,;]+)")
LOG_FIELD_LABELS = { LOG_FIELD_LABELS = {
"action": "动作", "action": "动作",
"elapsed_ms": "耗时毫秒", "elapsed_ms": "耗时毫秒",
"env_path": "环境文件",
"error": "错误", "error": "错误",
"error_type": "异常类型",
"fallback": "兜底方式", "fallback": "兜底方式",
"loaded": "已加载",
"row_count": "行数", "row_count": "行数",
"source_row_count": "源数据行数", "source_row_count": "源数据行数",
"stage": "阶段",
"table_count": "表数量", "table_count": "表数量",
"target_count": "目标数量", "target_count": "目标数量",
} }
def sanitize_value(value: Any) -> Any:
if isinstance(value, str):
return SENSITIVE_PATTERN.sub(r"\1=***", value)
return value
def log_event(level: int, event: str, **fields: Any) -> None: def log_event(level: int, event: str, **fields: Any) -> None:
safe_event = sanitize_value(event) emit_log_event(logger, LOG_FIELD_LABELS, level, event, **fields)
details = " ".join(
f"{LOG_FIELD_LABELS.get(key, key)}={sanitize_value(value)!r}" for key, value in fields.items()
)
logger.log(level, "事件=%s%s", safe_event, f" {details}" if details else "")
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
@@ -54,60 +49,90 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args() return parser.parse_args()
def safe_result_summary(result: dict[str, object]) -> dict[str, object]:
summary: dict[str, object] = {}
feishu_result = result.get("feishu")
if isinstance(feishu_result, dict):
summary["feishu"] = {"table_count": feishu_result.get("table_count")}
postgres_result = result.get("postgres")
if isinstance(postgres_result, dict):
summary["postgres"] = {"row_count": postgres_result.get("row_count")}
sync_result = result.get("sync_brand")
if isinstance(sync_result, dict):
summary["sync_brand"] = {
"target_count": sync_result.get("target_count"),
"source_row_count": sync_result.get("source_row_count"),
}
return summary
def public_result(result: dict[str, object]) -> dict[str, object]:
return sanitize_value(result)
def main() -> int: def main() -> int:
started_at = time.monotonic() started_at = time.monotonic()
try: try:
args = parse_args() args = parse_args()
log_event(logging.INFO, "飞书同步动作开始", action=args.action) safe_logs = os.getenv("INVENTORY_SAFE_LOGS") == "1"
log_event(logging.INFO, "渠道库存同步任务启动", action=args.action, stage="start")
config = build_app_config() config = build_app_config()
if config.env_path: if config.env_path:
log_event(logging.INFO, "已加载环境变量文件", env_path=config.env_path) log_event(logging.INFO, "配置加载完成", loaded=True, stage="config")
else: else:
log_event(logging.WARNING, "未找到环境变量文件", fallback="当前Shell环境变量") log_event(logging.WARNING, "未找到环境变量文件,使用当前Shell环境变量", fallback="shell", stage="config")
client = FeishuClient(config) client = FeishuClient(config)
result: dict[str, object] = {} result: dict[str, object] = {}
if args.action in {"feishu", "all"}: if args.action in {"feishu", "all"}:
log_event(logging.INFO, "开始读取飞书Base信息") log_event(logging.INFO, "开始读取飞书表信息", stage="feishu")
result["feishu"] = run_feishu_summary(client) result["feishu"] = run_feishu_summary(client)
log_event( log_event(
logging.INFO, logging.INFO,
"飞书Base信息读取完成", "飞书表信息读取完成",
stage="feishu",
table_count=result["feishu"].get("table_count") if isinstance(result["feishu"], dict) else None, table_count=result["feishu"].get("table_count") if isinstance(result["feishu"], dict) else None,
) )
if args.action in {"postgres", "all"}: if args.action in {"postgres", "all"}:
log_event(logging.INFO, "开始查询PostgreSQL") log_event(logging.INFO, "开始查询渠道库存数据", stage="database")
result["postgres"] = query_postgres(config) result["postgres"] = query_postgres(config)
log_event( log_event(
logging.INFO, logging.INFO,
"PostgreSQL查询完成", "渠道库存数据查询完成",
stage="database",
row_count=result["postgres"].get("row_count") if isinstance(result["postgres"], dict) else None, row_count=result["postgres"].get("row_count") if isinstance(result["postgres"], dict) else None,
) )
if args.action in {"sync-brand", "all"}: if args.action in {"sync-brand", "all"}:
log_event(logging.INFO, "开始同步品牌数据") log_event(logging.INFO, "开始同步渠道库存到飞书", stage="sync")
postgres_result = result.get("postgres") postgres_result = result.get("postgres")
result["sync_brand"] = run_sync_brand(config, client, postgres_result if isinstance(postgres_result, dict) else None) result["sync_brand"] = run_sync_brand(config, client, postgres_result if isinstance(postgres_result, dict) else None)
log_event( log_event(
logging.INFO, logging.INFO,
"品牌数据同步完成", "渠道库存同步到飞书完成",
stage="sync",
target_count=result["sync_brand"].get("target_count") if isinstance(result["sync_brand"], dict) else None, target_count=result["sync_brand"].get("target_count") if isinstance(result["sync_brand"], dict) else None,
source_row_count=result["sync_brand"].get("source_row_count") if isinstance(result["sync_brand"], dict) else None, source_row_count=result["sync_brand"].get("source_row_count") if isinstance(result["sync_brand"], dict) else None,
) )
print(json.dumps(result, ensure_ascii=False, indent=2)) if safe_logs:
print(json.dumps(safe_result_summary(result), ensure_ascii=False, indent=2))
else:
print(json.dumps(public_result(result), ensure_ascii=False, indent=2))
log_event( log_event(
logging.INFO, logging.INFO,
"飞书同步动作完成", "渠道库存同步任务完成",
action=args.action, action=args.action,
stage="finish",
elapsed_ms=int((time.monotonic() - started_at) * 1000), elapsed_ms=int((time.monotonic() - started_at) * 1000),
) )
return 0 return 0
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
log_event( log_event(
logging.ERROR, logging.ERROR,
"飞书同步动作失败", "渠道库存同步任务失败",
error=exc, stage="failed",
**exception_fields(exc),
elapsed_ms=int((time.monotonic() - started_at) * 1000), elapsed_ms=int((time.monotonic() - started_at) * 1000),
) )
return 1 return 1
+15 -31
View File
@@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import re
from typing import Any from typing import Any
from inventory_analytics.logging_utils import log_event as emit_log_event
from .feishu import FeishuApiError, FeishuClient from .feishu import FeishuApiError, FeishuClient
from .postgres import query_postgres from .postgres import query_postgres
from .settings import AppConfig from .settings import AppConfig
@@ -11,7 +12,6 @@ from .specs import BRAND_SYNC_TARGETS, FIELD_SPECS, PRIMARY_FIELD_NAME, PRIMARY_
logger = logging.getLogger("inventory_sync.service") logger = logging.getLogger("inventory_sync.service")
SENSITIVE_PATTERN = re.compile(r"(?i)(password|passwd|secret|token|authorization)\s*[:=]\s*([^\s,;]+)")
LOG_FIELD_LABELS = { LOG_FIELD_LABELS = {
"advanced_permission_enabled": "高级权限已开启", "advanced_permission_enabled": "高级权限已开启",
@@ -20,26 +20,16 @@ LOG_FIELD_LABELS = {
"deleted_records": "删除记录数", "deleted_records": "删除记录数",
"existing_records": "已有记录数", "existing_records": "已有记录数",
"rows": "行数", "rows": "行数",
"selected_table_id": "选中表ID",
"source_rows": "源数据行数", "source_rows": "源数据行数",
"stage": "阶段",
"table_count": "表数量", "table_count": "表数量",
"table_name": "表名", "table_name": "表名",
"target_count": "目标数量", "target_count": "目标数量",
} }
def sanitize_value(value: Any) -> Any:
if isinstance(value, str):
return SENSITIVE_PATTERN.sub(r"\1=***", value)
return value
def log_event(level: int, event: str, **fields: Any) -> None: def log_event(level: int, event: str, **fields: Any) -> None:
safe_event = sanitize_value(event) emit_log_event(logger, LOG_FIELD_LABELS, level, event, **fields)
details = " ".join(
f"{LOG_FIELD_LABELS.get(key, key)}={sanitize_value(value)!r}" for key, value in fields.items()
)
logger.log(level, "事件=%s%s", safe_event, f" {details}" if details else "")
def build_feishu_record(row: dict[str, Any]) -> dict[str, Any]: def build_feishu_record(row: dict[str, Any]) -> dict[str, Any]:
@@ -64,34 +54,24 @@ def build_feishu_record(row: dict[str, Any]) -> dict[str, Any]:
def run_feishu_summary(client: FeishuClient) -> dict[str, Any]: def run_feishu_summary(client: FeishuClient) -> dict[str, Any]:
log_event(logging.INFO, "开始获取飞书租户访问令牌") log_event(logging.INFO, "开始获取飞书访问令牌", stage="feishu_auth")
access_token = client.get_tenant_access_token() access_token = client.get_tenant_access_token()
log_event(logging.INFO, "飞书租户访问令牌获取完成") log_event(logging.INFO, "飞书访问令牌获取完成", stage="feishu_auth")
base_info = client.parse_base_info(client.build_base_url(), access_token) base_info = client.parse_base_info(client.build_base_url(), access_token)
log_event(logging.INFO, "飞书Base信息解析完成", selected_table_id=base_info["selected_table_id"]) log_event(logging.INFO, "飞书Base信息解析完成", stage="feishu_base")
advanced_permission_enabled = client.enable_advanced_permission(access_token, base_info["app_token"] or "") advanced_permission_enabled = client.enable_advanced_permission(access_token, base_info["app_token"] or "")
tables = client.list_tables(access_token, base_info["app_token"] or "") tables = client.list_tables(access_token, base_info["app_token"] or "")
log_event( log_event(
logging.INFO, logging.INFO,
"飞书Base表列表读取完成", "飞书Base表列表读取完成",
stage="feishu_tables",
table_count=len(tables), table_count=len(tables),
advanced_permission_enabled=advanced_permission_enabled, advanced_permission_enabled=advanced_permission_enabled,
) )
return { return {
"base_url": base_info["base_url"],
"app_token": base_info["app_token"],
"auth_mode": "tenant_access_token", "auth_mode": "tenant_access_token",
"selected_table_id": base_info["selected_table_id"],
"selected_view_id": base_info["selected_view_id"],
"advanced_permission_enabled": advanced_permission_enabled, "advanced_permission_enabled": advanced_permission_enabled,
"folder": { "tables": [{"name": table.get("name"), "revision": table.get("revision")} for table in tables],
"name": base_info["app_token"],
"type": "base",
"tables": [
{"table_id": table.get("table_id"), "name": table.get("name"), "revision": table.get("revision")}
for table in tables
],
},
"table_count": len(tables), "table_count": len(tables),
} }
@@ -107,6 +87,7 @@ def sync_brand_target(
log_event( log_event(
logging.INFO, logging.INFO,
"开始同步目标飞书表", "开始同步目标飞书表",
stage="sync_target",
brand_name=target.brand_name, brand_name=target.brand_name,
table_name=target.table_name, table_name=target.table_name,
source_rows=len(postgres_rows or []), source_rows=len(postgres_rows or []),
@@ -128,6 +109,7 @@ def sync_brand_target(
log_event( log_event(
logging.INFO, logging.INFO,
"目标飞书表已有记录读取完成", "目标飞书表已有记录读取完成",
stage="sync_target",
brand_name=target.brand_name, brand_name=target.brand_name,
table_name=target.table_name, table_name=target.table_name,
existing_records=len(existing_record_ids), existing_records=len(existing_record_ids),
@@ -142,6 +124,7 @@ def sync_brand_target(
log_event( log_event(
logging.INFO, logging.INFO,
"目标飞书表同步完成", "目标飞书表同步完成",
stage="sync_target",
brand_name=target.brand_name, brand_name=target.brand_name,
table_name=target.table_name, table_name=target.table_name,
deleted_records=deleted_count, deleted_records=deleted_count,
@@ -162,13 +145,13 @@ def sync_brand_target(
def run_sync_brand(config: AppConfig, client: FeishuClient, postgres_result: dict[str, Any] | None = None) -> dict[str, Any]: def run_sync_brand(config: AppConfig, client: FeishuClient, postgres_result: dict[str, Any] | None = None) -> dict[str, Any]:
log_event(logging.INFO, "开始准备品牌同步", target_count=len(BRAND_SYNC_TARGETS)) log_event(logging.INFO, "开始准备品牌同步", stage="sync_prepare", target_count=len(BRAND_SYNC_TARGETS))
access_token = client.get_tenant_access_token() access_token = client.get_tenant_access_token()
base_info = client.parse_base_info(client.build_base_url(), access_token) base_info = client.parse_base_info(client.build_base_url(), access_token)
app_token = base_info["app_token"] or "" app_token = base_info["app_token"] or ""
client.enable_advanced_permission(access_token, app_token) client.enable_advanced_permission(access_token, app_token)
tables = client.list_tables(access_token, app_token) tables = client.list_tables(access_token, app_token)
log_event(logging.INFO, "品牌同步准备完成", table_count=len(tables)) log_event(logging.INFO, "品牌同步准备完成", stage="sync_prepare", table_count=len(tables))
results = [] results = []
source_row_count = 0 source_row_count = 0
@@ -181,6 +164,7 @@ def run_sync_brand(config: AppConfig, client: FeishuClient, postgres_result: dic
log_event( log_event(
logging.INFO, logging.INFO,
"目标品牌PostgreSQL数据已准备", "目标品牌PostgreSQL数据已准备",
stage="sync_prepare",
brand_name=target.brand_name, brand_name=target.brand_name,
table_name=target.table_name, table_name=target.table_name,
rows=len(postgres_rows or []), rows=len(postgres_rows or []),
+4 -3
View File
@@ -6,7 +6,7 @@ push: []
pull: [] pull: []
deployments: deployments:
- name: inventory-channel-inventory-sync-daily-0915 - name: channel-inventory-sync-0915
version: "1.0.0" version: "1.0.0"
description: "每天 09:15 执行渠道库存统计并写入飞书多维表格。" description: "每天 09:15 执行渠道库存统计并写入飞书多维表格。"
entrypoint: prefect_inventory_scheduler.py:inventory_channel_inventory_sync_flow entrypoint: prefect_inventory_scheduler.py:inventory_channel_inventory_sync_flow
@@ -22,14 +22,15 @@ deployments:
work_pool: work_pool:
name: inventory-analytics-pool name: inventory-analytics-pool
- name: inventory-batch-inventory-report-daily-0915 - name: batch-inventory-repor-0915
version: "1.0.0" version: "1.0.0"
description: "每天 09:15 生成批次库存统计报表并写入飞书多维表格。" description: "每天 09:15 生成批次库存统计数据并写入飞书多维表格,不输出本地报表文件。"
entrypoint: prefect_inventory_scheduler.py:inventory_batch_inventory_report_flow entrypoint: prefect_inventory_scheduler.py:inventory_batch_inventory_report_flow
parameters: parameters:
output: "" output: ""
feishu_table: "批次库存" feishu_table: "批次库存"
skip_feishu_sync: false skip_feishu_sync: false
skip_report_output: true
tags: tags:
- inventory - inventory
- feishu - feishu
+66 -5
View File
@@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import logging
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
import time
def bootstrap_prefect_env() -> None: def bootstrap_prefect_env() -> None:
@@ -22,28 +24,65 @@ def bootstrap_prefect_env() -> None:
bootstrap_prefect_env() bootstrap_prefect_env()
from prefect import flow, task from prefect import flow, get_run_logger, task
from inventory_analytics.logging_utils import exception_fields
from inventory_analytics.channel_inventory_sync.entrypoint import main as channel_inventory_sync_main from inventory_analytics.channel_inventory_sync.entrypoint import main as channel_inventory_sync_main
from inventory_analytics.batch_inventory_report.cli import main as batch_inventory_report_main from inventory_analytics.batch_inventory_report.cli import main as batch_inventory_report_main
@task(name="run-channel-inventory-sync") @task(name="run-channel-inventory-sync")
def run_channel_inventory_sync_task() -> None: def run_channel_inventory_sync_task() -> None:
logger = get_run_logger()
started_at = time.monotonic()
logger.info("渠道库存统计开始,action=all,安全日志模式=enabled")
original_argv = sys.argv[:] original_argv = sys.argv[:]
original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS")
root_logger = logging.getLogger()
original_handlers = root_logger.handlers[:]
original_level = root_logger.level
try: try:
os.environ["INVENTORY_SAFE_LOGS"] = "1"
sys.argv = ["sync_channel_inventory.py", "--action", "all"] sys.argv = ["sync_channel_inventory.py", "--action", "all"]
exit_code = channel_inventory_sync_main() exit_code = channel_inventory_sync_main()
except Exception as exc: # noqa: BLE001
logger.error("渠道库存统计异常,%s", exception_fields(exc))
raise
finally: finally:
sys.argv = original_argv sys.argv = original_argv
if original_safe_logs is None:
os.environ.pop("INVENTORY_SAFE_LOGS", None)
else:
os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs
root_logger.handlers = original_handlers
root_logger.setLevel(original_level)
if exit_code != 0: if exit_code != 0:
logger.error("渠道库存统计失败,退出码=%s", exit_code)
raise RuntimeError(f"渠道库存统计执行失败,退出码: {exit_code}") raise RuntimeError(f"渠道库存统计执行失败,退出码: {exit_code}")
logger.info("渠道库存统计完成,耗时毫秒=%s", int((time.monotonic() - started_at) * 1000))
@task(name="run-batch-inventory-report") @task(name="run-batch-inventory-report")
def run_batch_inventory_report_task(output: str = "", feishu_table: str = "批次库存", skip_feishu_sync: bool = False) -> None: def run_batch_inventory_report_task(
output: str = "",
feishu_table: str = "批次库存",
skip_feishu_sync: bool = False,
skip_report_output: bool = True,
) -> None:
logger = get_run_logger()
started_at = time.monotonic()
logger.info(
"批次库存统计开始,飞书同步=%s,本地报表输出=%s,安全日志模式=enabled",
"disabled" if skip_feishu_sync else "enabled",
"skipped" if skip_report_output else "enabled",
)
original_argv = sys.argv[:] original_argv = sys.argv[:]
original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS")
root_logger = logging.getLogger()
original_handlers = root_logger.handlers[:]
original_level = root_logger.level
try: try:
os.environ["INVENTORY_SAFE_LOGS"] = "1"
sys.argv = ["generate_batch_inventory_report.py"] sys.argv = ["generate_batch_inventory_report.py"]
if output: if output:
sys.argv.extend(["--output", output]) sys.argv.extend(["--output", output])
@@ -51,11 +90,25 @@ def run_batch_inventory_report_task(output: str = "", feishu_table: str = "批
sys.argv.extend(["--feishu-table", feishu_table]) sys.argv.extend(["--feishu-table", feishu_table])
if skip_feishu_sync: if skip_feishu_sync:
sys.argv.append("--skip-feishu-sync") sys.argv.append("--skip-feishu-sync")
if skip_report_output:
sys.argv.append("--skip-report-output")
sys.argv.append("--safe-logs")
exit_code = batch_inventory_report_main() exit_code = batch_inventory_report_main()
except Exception as exc: # noqa: BLE001
logger.error("批次库存统计异常,%s", exception_fields(exc))
raise
finally: finally:
sys.argv = original_argv sys.argv = original_argv
if original_safe_logs is None:
os.environ.pop("INVENTORY_SAFE_LOGS", None)
else:
os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs
root_logger.handlers = original_handlers
root_logger.setLevel(original_level)
if exit_code != 0: if exit_code != 0:
logger.error("批次库存统计失败,退出码=%s", exit_code)
raise RuntimeError(f"批次库存统计报表执行失败,退出码: {exit_code}") raise RuntimeError(f"批次库存统计报表执行失败,退出码: {exit_code}")
logger.info("批次库存统计完成,耗时毫秒=%s", int((time.monotonic() - started_at) * 1000))
@flow(name="inventory-channel-inventory-sync") @flow(name="inventory-channel-inventory-sync")
@@ -68,8 +121,14 @@ def inventory_batch_inventory_report_flow(
output: str = "", output: str = "",
feishu_table: str = "批次库存", feishu_table: str = "批次库存",
skip_feishu_sync: bool = False, skip_feishu_sync: bool = False,
skip_report_output: bool = True,
) -> None: ) -> None:
run_batch_inventory_report_task(output=output, feishu_table=feishu_table, skip_feishu_sync=skip_feishu_sync) run_batch_inventory_report_task(
output=output,
feishu_table=feishu_table,
skip_feishu_sync=skip_feishu_sync,
skip_report_output=skip_report_output,
)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
@@ -81,6 +140,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--output", default="", help="批次库存统计输出路径,可选") parser.add_argument("--output", default="", help="批次库存统计输出路径,可选")
parser.add_argument("--feishu-table", default="批次库存", help="批次库存统计同步目标飞书表名") parser.add_argument("--feishu-table", default="批次库存", help="批次库存统计同步目标飞书表名")
parser.add_argument("--skip-feishu-sync", action="store_true", help="批次库存统计只生成本地文件,不写飞书") parser.add_argument("--skip-feishu-sync", action="store_true", help="批次库存统计只生成本地文件,不写飞书")
parser.add_argument("--write-report-output", action="store_true", help="Prefect 执行批次库存统计时同时生成本地报表文件")
return parser.parse_args() return parser.parse_args()
@@ -90,17 +150,18 @@ def main() -> None:
"output": args.output, "output": args.output,
"feishu_table": args.feishu_table, "feishu_table": args.feishu_table,
"skip_feishu_sync": args.skip_feishu_sync, "skip_feishu_sync": args.skip_feishu_sync,
"skip_report_output": not args.write_report_output,
} }
if args.serve_channel_inventory_sync: if args.serve_channel_inventory_sync:
inventory_channel_inventory_sync_flow.serve( inventory_channel_inventory_sync_flow.serve(
name="inventory-channel-inventory-sync-daily-0915", name="channel-inventory-sync-0915",
cron="15 9 * * *", cron="15 9 * * *",
tags=["inventory", "feishu", "channel-inventory-sync"], tags=["inventory", "feishu", "channel-inventory-sync"],
) )
return return
if args.serve_batch_inventory_report: if args.serve_batch_inventory_report:
inventory_batch_inventory_report_flow.serve( inventory_batch_inventory_report_flow.serve(
name="inventory-batch-inventory-report-daily-0915", name="batch-inventory-repor-0915",
cron="15 9 * * *", cron="15 9 * * *",
parameters=flow_kwargs, parameters=flow_kwargs,
tags=["inventory", "feishu", "batch-inventory-report"], tags=["inventory", "feishu", "batch-inventory-report"],