Files
fquiz/api/app/services/task_log_service.py
T
chengkai3 cc988abdac feat:[FL-212][任务监控页面支持查看执行日志]
- 后端添加任务日志存储和查询API
  - 新增 /api/v1/admin/task-logs 端点支持上传、获取和列出任务日志
  - 日志存储到MinIO,路径格式: logs/YYYY/MM/DD/{task_id}.log
  - 新增 task_log_service 处理MinIO存储交互
  - 新增 task_log schema 定义API请求响应格式

- 前端任务监控页面添加查看日志功能
  - 在任务表格和卡片视图中添加"查看日志"按钮
  - 点击按钮打开模态框显示任务执行日志
  - 支持桌面端表格和移动端卡片两种视图

- 新增单元测试验证日志路径生成和错误处理

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-28 15:31:05 +08:00

206 lines
6.3 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
from ..core.config import get_settings
class TaskLogServiceError(RuntimeError):
pass
class TaskLogNotFoundError(TaskLogServiceError):
pass
def _get_log_path(task_id: str, timestamp: datetime | None = None) -> str:
"""
Generate log path in MinIO following the pattern: logs/YYYY/MM/DD/{task_id}.log
"""
if timestamp is None:
timestamp = datetime.now(timezone.utc)
year = timestamp.strftime("%Y")
month = timestamp.strftime("%m")
day = timestamp.strftime("%d")
return f"logs/{year}/{month}/{day}/{task_id}.log"
def upload_task_log(task_id: str, log_content: str) -> tuple[str, datetime]:
"""
Upload task execution log to MinIO.
Returns: (log_path, uploaded_at)
"""
settings = get_settings()
if not settings.minio_enabled:
raise TaskLogServiceError("MinIO storage is not enabled")
try:
import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
except ImportError as exc:
raise TaskLogServiceError("boto3 library is required for MinIO storage") from exc
uploaded_at = datetime.now(timezone.utc)
log_path = _get_log_path(task_id, uploaded_at)
try:
client_config = Config(
connect_timeout=3.0,
read_timeout=10.0,
retries={"max_attempts": 2},
)
session = boto3.session.Session(
aws_access_key_id=settings.minio_access_key,
aws_secret_access_key=settings.minio_secret_key,
)
client = session.client(
"s3",
endpoint_url=settings.minio_endpoint,
region_name=settings.minio_region,
config=client_config,
)
# Upload log content as text file
client.put_object(
Bucket=settings.minio_bucket,
Key=log_path,
Body=log_content.encode("utf-8"),
ContentType="text/plain; charset=utf-8",
)
return log_path, uploaded_at
except (BotoCoreError, ClientError) as exc:
raise TaskLogServiceError(f"Failed to upload log to MinIO: {exc}") from exc
except Exception as exc:
raise TaskLogServiceError(f"Unexpected error during log upload: {exc}") from exc
def get_task_log(task_id: str, log_date: datetime | None = None) -> tuple[str, str]:
"""
Retrieve task execution log from MinIO.
If log_date is not provided, tries to find the log for today.
Returns: (log_content, log_path)
"""
settings = get_settings()
if not settings.minio_enabled:
raise TaskLogServiceError("MinIO storage is not enabled")
try:
import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
except ImportError as exc:
raise TaskLogServiceError("boto3 library is required for MinIO storage") from exc
if log_date is None:
log_date = datetime.now(timezone.utc)
log_path = _get_log_path(task_id, log_date)
try:
client_config = Config(
connect_timeout=3.0,
read_timeout=10.0,
retries={"max_attempts": 2},
)
session = boto3.session.Session(
aws_access_key_id=settings.minio_access_key,
aws_secret_access_key=settings.minio_secret_key,
)
client = session.client(
"s3",
endpoint_url=settings.minio_endpoint,
region_name=settings.minio_region,
config=client_config,
)
response = client.get_object(
Bucket=settings.minio_bucket,
Key=log_path,
)
body = response.get("Body")
if body is None:
raise TaskLogServiceError("Log file body is empty")
log_content = body.read().decode("utf-8")
return log_content, log_path
except ClientError as exc:
error_code = exc.response.get("Error", {}).get("Code", "")
if error_code in {"404", "NoSuchKey", "NotFound"}:
raise TaskLogNotFoundError(f"Log not found for task {task_id} at path {log_path}") from exc
raise TaskLogServiceError(f"Failed to retrieve log from MinIO: {exc}") from exc
except (BotoCoreError, UnicodeDecodeError) as exc:
raise TaskLogServiceError(f"Failed to read log content: {exc}") from exc
except Exception as exc:
raise TaskLogServiceError(f"Unexpected error during log retrieval: {exc}") from exc
def list_task_logs(task_id: str) -> list[str]:
"""
List all available logs for a task across all dates.
Returns: list of log paths
"""
settings = get_settings()
if not settings.minio_enabled:
raise TaskLogServiceError("MinIO storage is not enabled")
try:
import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
except ImportError as exc:
raise TaskLogServiceError("boto3 library is required for MinIO storage") from exc
try:
client_config = Config(
connect_timeout=3.0,
read_timeout=10.0,
retries={"max_attempts": 2},
)
session = boto3.session.Session(
aws_access_key_id=settings.minio_access_key,
aws_secret_access_key=settings.minio_secret_key,
)
client = session.client(
"s3",
endpoint_url=settings.minio_endpoint,
region_name=settings.minio_region,
config=client_config,
)
# Search for all logs matching the task_id pattern
prefix = "logs/"
suffix = f"/{task_id}.log"
log_paths: list[str] = []
paginator = client.get_paginator("list_objects_v2")
pages = paginator.paginate(Bucket=settings.minio_bucket, Prefix=prefix)
for page in pages:
for item in page.get("Contents", []):
key = item.get("Key", "")
if key.endswith(suffix):
log_paths.append(key)
return sorted(log_paths, reverse=True) # Most recent first
except (BotoCoreError, ClientError) as exc:
raise TaskLogServiceError(f"Failed to list logs from MinIO: {exc}") from exc
except Exception as exc:
raise TaskLogServiceError(f"Unexpected error during log listing: {exc}") from exc