添加 prefect UI日志显示

This commit is contained in:
2026-05-12 10:55:19 +08:00
parent 989fe75485
commit 9d85346e56
5 changed files with 128 additions and 2 deletions
@@ -18,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 configure_logging_with_extra_handler
from ..logging_utils import exception_fields 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
@@ -212,6 +213,16 @@ LOG_FIELD_LABELS = {
} }
def configure_logging() -> None: def configure_logging() -> None:
if os.getenv("INVENTORY_PREFECT_LOG_BRIDGE") == "1":
handler = PrefectLogHandler()
handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s %(levelname)s %(name)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
)
configure_logging_with_extra_handler(LOG_FILE, handler)
else:
configure_app_logging(LOG_FILE) configure_app_logging(LOG_FILE)
@@ -219,6 +230,21 @@ def log_event(level: int, event: str, **fields: Any) -> None:
emit_log_event(logger, LOG_FIELD_LABELS, level, event, **fields) emit_log_event(logger, LOG_FIELD_LABELS, level, event, **fields)
class PrefectLogHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
try:
from prefect.logging import get_run_logger
prefect_logger = get_run_logger()
except Exception:
return
message = self.format(record)
log_method = getattr(prefect_logger, record.levelname.lower(), None)
if log_method is None:
log_method = prefect_logger.info
log_method(message)
@dataclass(frozen=True) @dataclass(frozen=True)
class FeishuFieldSpec: class FeishuFieldSpec:
field_name: str # 飞书字段名 field_name: str # 飞书字段名
@@ -8,6 +8,7 @@ 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 configure_logging_with_extra_handler
from ..logging_utils import exception_fields from ..logging_utils import exception_fields
from ..paths import LOG_DIR from ..paths import LOG_DIR
@@ -28,7 +29,32 @@ LOG_FIELD_LABELS = {
} }
class PrefectLogHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
try:
from prefect.logging import get_run_logger
prefect_logger = get_run_logger()
except Exception:
return
message = self.format(record)
log_method = getattr(prefect_logger, record.levelname.lower(), None)
if log_method is None:
log_method = prefect_logger.info
log_method(message)
def main() -> int: def main() -> int:
if os.getenv("INVENTORY_PREFECT_LOG_BRIDGE") == "1":
handler = PrefectLogHandler()
handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s %(levelname)s %(name)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
)
configure_logging_with_extra_handler(LOG_FILE, handler, stream=sys.stderr)
else:
configure_logging(LOG_FILE, stream=sys.stderr) configure_logging(LOG_FILE, stream=sys.stderr)
started_at = time.monotonic() started_at = time.monotonic()
start_fields = {"argv": sys.argv[1:], "stage": "start"} start_fields = {"argv": sys.argv[1:], "stage": "start"}
+12
View File
@@ -84,6 +84,18 @@ def configure_logging(log_file: Path, *, stream = None, level: int = logging.INF
root_logger.addHandler(file_handler) root_logger.addHandler(file_handler)
def configure_logging_with_extra_handler(
log_file: Path,
extra_handler: logging.Handler,
*,
stream = None,
level: int = logging.INFO,
) -> None:
configure_logging(log_file, stream=stream, level=level)
extra_handler.setLevel(level)
logging.getLogger().addHandler(extra_handler)
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(
+50
View File
@@ -2,21 +2,41 @@ from __future__ import annotations
import csv import csv
import io import io
import logging
import time
from typing import Any from typing import Any
import psycopg2 import psycopg2
from inventory_analytics.logging_utils import log_event as emit_log_event
from .settings import AppConfig from .settings import AppConfig
from .specs import BrandSyncTarget from .specs import BrandSyncTarget
from .sql import DEFAULT_SYNC_TARGET, POSTGRES_QUERY, build_postgres_query from .sql import DEFAULT_SYNC_TARGET, POSTGRES_QUERY, build_postgres_query
logger = logging.getLogger("inventory_sync.postgres")
LOG_FIELD_LABELS = {
"elapsed_ms": "耗时毫秒",
"row_count": "行数",
"stage": "阶段",
"statement_timeout_ms": "查询超时毫秒",
}
def log_event(level: int, event: str, **fields: Any) -> None:
emit_log_event(logger, LOG_FIELD_LABELS, level, event, **fields)
def query_postgres( def query_postgres(
config: AppConfig, config: AppConfig,
brand_name: str | None = None, brand_name: str | None = None,
warehouse_keys: tuple[int | str, ...] | None = None, warehouse_keys: tuple[int | str, ...] | None = None,
target: BrandSyncTarget | None = None, target: BrandSyncTarget | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
started_at = time.monotonic()
statement_timeout_ms = 300_000
active_brand_name = brand_name or (target.brand_name if target else DEFAULT_SYNC_TARGET.brand_name) active_brand_name = brand_name or (target.brand_name if target else DEFAULT_SYNC_TARGET.brand_name)
active_warehouse_keys = warehouse_keys or (target.warehouse_keys if target else DEFAULT_SYNC_TARGET.warehouse_keys) active_warehouse_keys = warehouse_keys or (target.warehouse_keys if target else DEFAULT_SYNC_TARGET.warehouse_keys)
query = ( query = (
@@ -29,19 +49,49 @@ def query_postgres(
) )
output = io.StringIO() output = io.StringIO()
try: try:
log_event(logging.INFO, "开始连接PostgreSQL", stage="database_connect")
with psycopg2.connect( with psycopg2.connect(
host=config.analytics_db_host, host=config.analytics_db_host,
port=config.analytics_db_port, port=config.analytics_db_port,
user=config.analytics_db_user, user=config.analytics_db_user,
password=config.analytics_db_password, password=config.analytics_db_password,
dbname=config.analytics_db_name, dbname=config.analytics_db_name,
connect_timeout=10,
) as conn: ) as conn:
log_event(
logging.INFO,
"PostgreSQL连接完成",
stage="database_connect",
elapsed_ms=int((time.monotonic() - started_at) * 1000),
)
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute("SET TIME ZONE 'Asia/Shanghai';") cur.execute("SET TIME ZONE 'Asia/Shanghai';")
cur.execute("SET statement_timeout = %s;", (statement_timeout_ms,))
copy_started_at = time.monotonic()
log_event(
logging.INFO,
"开始执行渠道库存SQL",
stage="database_query",
statement_timeout_ms=statement_timeout_ms,
)
cur.copy_expert(f"COPY ({query}) TO STDOUT WITH CSV HEADER", output) cur.copy_expert(f"COPY ({query}) TO STDOUT WITH CSV HEADER", output)
log_event(
logging.INFO,
"渠道库存SQL执行完成",
stage="database_query",
elapsed_ms=int((time.monotonic() - copy_started_at) * 1000),
)
except psycopg2.Error as exc: except psycopg2.Error as exc:
raise RuntimeError(f"PostgreSQL 查询失败: {exc}") from exc raise RuntimeError(f"PostgreSQL 查询失败: {exc}") from exc
parse_started_at = time.monotonic()
output.seek(0) output.seek(0)
rows = list(csv.DictReader(output)) rows = list(csv.DictReader(output))
log_event(
logging.INFO,
"渠道库存查询结果解析完成",
stage="database_parse",
row_count=len(rows),
elapsed_ms=int((time.monotonic() - parse_started_at) * 1000),
)
return {"row_count": len(rows), "rows": rows} return {"row_count": len(rows), "rows": rows}
+12
View File
@@ -50,11 +50,13 @@ def run_channel_inventory_sync_task() -> None:
logger.info("渠道库存统计开始,action=all,安全日志模式=enabled") logger.info("渠道库存统计开始,action=all,安全日志模式=enabled")
original_argv = sys.argv[:] original_argv = sys.argv[:]
original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS") original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS")
original_prefect_log_bridge = os.environ.get("INVENTORY_PREFECT_LOG_BRIDGE")
root_logger = logging.getLogger() root_logger = logging.getLogger()
original_handlers = root_logger.handlers[:] original_handlers = root_logger.handlers[:]
original_level = root_logger.level original_level = root_logger.level
try: try:
os.environ["INVENTORY_SAFE_LOGS"] = "1" os.environ["INVENTORY_SAFE_LOGS"] = "1"
os.environ["INVENTORY_PREFECT_LOG_BRIDGE"] = "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 except Exception as exc: # noqa: BLE001
@@ -66,6 +68,10 @@ def run_channel_inventory_sync_task() -> None:
os.environ.pop("INVENTORY_SAFE_LOGS", None) os.environ.pop("INVENTORY_SAFE_LOGS", None)
else: else:
os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs
if original_prefect_log_bridge is None:
os.environ.pop("INVENTORY_PREFECT_LOG_BRIDGE", None)
else:
os.environ["INVENTORY_PREFECT_LOG_BRIDGE"] = original_prefect_log_bridge
root_logger.handlers = original_handlers root_logger.handlers = original_handlers
root_logger.setLevel(original_level) root_logger.setLevel(original_level)
if exit_code != 0: if exit_code != 0:
@@ -91,11 +97,13 @@ def run_batch_inventory_report_task(
) )
original_argv = sys.argv[:] original_argv = sys.argv[:]
original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS") original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS")
original_prefect_log_bridge = os.environ.get("INVENTORY_PREFECT_LOG_BRIDGE")
root_logger = logging.getLogger() root_logger = logging.getLogger()
original_handlers = root_logger.handlers[:] original_handlers = root_logger.handlers[:]
original_level = root_logger.level original_level = root_logger.level
try: try:
os.environ["INVENTORY_SAFE_LOGS"] = "1" os.environ["INVENTORY_SAFE_LOGS"] = "1"
os.environ["INVENTORY_PREFECT_LOG_BRIDGE"] = "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])
@@ -116,6 +124,10 @@ def run_batch_inventory_report_task(
os.environ.pop("INVENTORY_SAFE_LOGS", None) os.environ.pop("INVENTORY_SAFE_LOGS", None)
else: else:
os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs
if original_prefect_log_bridge is None:
os.environ.pop("INVENTORY_PREFECT_LOG_BRIDGE", None)
else:
os.environ["INVENTORY_PREFECT_LOG_BRIDGE"] = original_prefect_log_bridge
root_logger.handlers = original_handlers root_logger.handlers = original_handlers
root_logger.setLevel(original_level) root_logger.setLevel(original_level)
if exit_code != 0: if exit_code != 0: