From 8ef3d175533d99d487b8d0892ca6df219992bc4b Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Fri, 13 Mar 2026 10:35:49 +0800 Subject: [PATCH] Integrate Log Center for error reporting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: DRF custom exception handler → Log Center (async, non-blocking) - Frontend: global error handlers + axios 5xx interceptor → Log Center - CI/CD: failure step reports build/deploy errors with actual logs - K8S: add LOG_CENTER env vars to backend deployment - Registered projects: video_backend, video_web --- .gitea/workflows/deploy.yaml | 63 +++++++++++++++++++++++++++---- backend/config/settings.py | 1 + backend/utils/__init__.py | 0 backend/utils/log_center.py | 72 ++++++++++++++++++++++++++++++++++++ k8s/backend-deployment.yaml | 7 ++++ web/src/lib/api.ts | 11 +++++- web/src/lib/logCenter.ts | 43 +++++++++++++++++++++ web/src/main.tsx | 13 +++++++ 8 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 backend/utils/__init__.py create mode 100644 backend/utils/log_center.py create mode 100644 web/src/lib/logCenter.ts diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 42b273a..31ecf18 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -28,6 +28,7 @@ jobs: password: ${{ secrets.SWR_PASSWORD }} - name: Build and Push Backend + id: build_backend run: | set -o pipefail docker buildx build \ @@ -37,6 +38,7 @@ jobs: ./backend 2>&1 | tee /tmp/build.log - name: Build and Push Web + id: build_web run: | set -o pipefail docker buildx build \ @@ -59,6 +61,7 @@ jobs: kubeconfig: ${{ secrets.KUBE_CONFIG }} - name: Apply K8s Manifests + id: deploy run: | # Replace image placeholders sed -i "s|\${CI_REGISTRY_IMAGE}/video-backend:latest|${{ secrets.SWR_SERVER }}/${{ secrets.SWR_ORG }}/video-backend:latest|g" k8s/backend-deployment.yaml @@ -74,17 +77,61 @@ jobs: kubectl rollout restart deployment/video-web } 2>&1 | tee /tmp/deploy.log - - name: Report failure + # ===== Log Center: failure reporting ===== + - name: Report failure to Log Center if: failure() run: | BUILD_LOG="" DEPLOY_LOG="" - if [ -f /tmp/build.log ]; then - BUILD_LOG=$(tail -50 /tmp/build.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + FAILED_STEP="unknown" + + if [[ "${{ steps.build_backend.outcome }}" == "failure" || "${{ steps.build_web.outcome }}" == "failure" ]]; then + FAILED_STEP="build" + if [ -f /tmp/build.log ]; then + BUILD_LOG=$(tail -50 /tmp/build.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + fi + elif [[ "${{ steps.deploy.outcome }}" == "failure" ]]; then + FAILED_STEP="deploy" + if [ -f /tmp/deploy.log ]; then + DEPLOY_LOG=$(tail -50 /tmp/deploy.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + fi fi - if [ -f /tmp/deploy.log ]; then - DEPLOY_LOG=$(tail -50 /tmp/deploy.log | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + + ERROR_LOG="${BUILD_LOG}${DEPLOY_LOG}" + if [ -z "$ERROR_LOG" ]; then + ERROR_LOG="No captured output. Check Gitea Actions UI for details." fi - echo "Build failed on branch ${{ github.ref_name }}" - echo "Build log: ${BUILD_LOG}" - echo "Deploy log: ${DEPLOY_LOG}" + + if [[ "$FAILED_STEP" == "deploy" ]]; then + SOURCE="deployment" + ERROR_TYPE="DeployError" + else + SOURCE="cicd" + ERROR_TYPE="DockerBuildError" + fi + + curl -s -X POST "https://qiyuan-log-center-api.airlabs.art/api/v1/logs/report" \ + -H "Content-Type: application/json" \ + -d "{ + \"project_id\": \"video_backend\", + \"environment\": \"${{ github.ref_name }}\", + \"level\": \"ERROR\", + \"source\": \"${SOURCE}\", + \"commit_hash\": \"${{ github.sha }}\", + \"repo_url\": \"https://gitea.airlabs.art/zyc/video-shuoshan.git\", + \"error\": { + \"type\": \"${ERROR_TYPE}\", + \"message\": \"[${FAILED_STEP}] Build and Deploy failed on branch ${{ github.ref_name }}\", + \"stack_trace\": [\"${ERROR_LOG}\"] + }, + \"context\": { + \"job_name\": \"build-and-deploy\", + \"step_name\": \"${FAILED_STEP}\", + \"workflow\": \"${{ github.workflow }}\", + \"run_id\": \"${{ github.run_number }}\", + \"branch\": \"${{ github.ref_name }}\", + \"actor\": \"${{ github.actor }}\", + \"commit\": \"${{ github.sha }}\", + \"run_url\": \"https://gitea.airlabs.art/${{ github.repository }}/actions/runs/${{ github.run_number }}\" + } + }" || true diff --git a/backend/config/settings.py b/backend/config/settings.py index 3f3165d..1635bc9 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -103,6 +103,7 @@ REST_FRAMEWORK = { 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', ), + 'EXCEPTION_HANDLER': 'utils.log_center.custom_exception_handler', } # JWT settings diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/log_center.py b/backend/utils/log_center.py new file mode 100644 index 0000000..1841720 --- /dev/null +++ b/backend/utils/log_center.py @@ -0,0 +1,72 @@ +"""Log Center integration — runtime error reporting.""" + +import os +import json +import traceback +import threading +from urllib.request import Request, urlopen +from urllib.error import URLError + +LOG_CENTER_URL = os.getenv('LOG_CENTER_URL', 'https://qiyuan-log-center-api.airlabs.art') +LOG_CENTER_ENABLED = os.getenv('LOG_CENTER_ENABLED', 'true').lower() in ('true', '1', 'yes') +PROJECT_ID = 'video_backend' +ENVIRONMENT = os.getenv('ENVIRONMENT', 'development') + + +def report_error(exc, context=None): + """Report a runtime error to Log Center (async, non-blocking).""" + if not LOG_CENTER_ENABLED: + return + + tb = traceback.extract_tb(exc.__traceback__) + last_frame = tb[-1] if tb else None + + payload = { + 'project_id': PROJECT_ID, + 'environment': ENVIRONMENT, + 'level': 'ERROR', + 'error': { + 'type': type(exc).__name__, + 'message': str(exc), + 'file_path': last_frame.filename if last_frame else 'unknown', + 'line_number': last_frame.lineno if last_frame else 0, + 'stack_trace': traceback.format_exception(exc), + }, + 'context': context or {}, + } + + # Fire-and-forget in background thread + threading.Thread(target=_send, args=(payload,), daemon=True).start() + + +def _send(payload): + """Send payload to Log Center API.""" + try: + data = json.dumps(payload).encode('utf-8') + req = Request( + f'{LOG_CENTER_URL}/api/v1/logs/report', + data=data, + headers={'Content-Type': 'application/json'}, + method='POST', + ) + urlopen(req, timeout=3) + except (URLError, Exception): + pass # Silent failure — never affect main business + + +def custom_exception_handler(exc, context): + """DRF custom exception handler with Log Center reporting.""" + from rest_framework.views import exception_handler + + # Report to Log Center + request = context.get('request') + report_error(exc, { + 'view': str(context.get('view', '')), + 'request_path': getattr(request, 'path', None), + 'request_method': getattr(request, 'method', None), + 'user': str(getattr(request, 'user', 'anonymous')), + }) + + # Default DRF handling + response = exception_handler(exc, context) + return response diff --git a/k8s/backend-deployment.yaml b/k8s/backend-deployment.yaml index b03e2e9..ce6d357 100644 --- a/k8s/backend-deployment.yaml +++ b/k8s/backend-deployment.yaml @@ -43,6 +43,13 @@ spec: # CORS - name: CORS_ALLOWED_ORIGINS value: "https://video-huoshan-web.airlabs.art" + # Log Center + - name: LOG_CENTER_URL + value: "https://qiyuan-log-center-api.airlabs.art" + - name: LOG_CENTER_ENABLED + value: "true" + - name: ENVIRONMENT + value: "production" livenessProbe: httpGet: path: /healthz/ diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index e772f3a..3347903 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -1,8 +1,9 @@ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import type { User, Quota, AuthTokens, AdminStats, AdminUser, AdminUserDetail, AdminRecord, SystemSettings, ProfileOverview, PaginatedResponse, } from '../types'; +import { reportError } from './logCenter'; const api = axios.create({ baseURL: '/api/v1', @@ -47,6 +48,14 @@ api.interceptors.response.use( window.location.href = '/login'; } } + // Report 5xx server errors to Log Center + if (error.response && error.response.status >= 500) { + reportError(error instanceof Error ? error : new Error(String(error)), { + api_url: (error as AxiosError).config?.url, + method: (error as AxiosError).config?.method, + status: error.response.status, + }); + } return Promise.reject(error); } ); diff --git a/web/src/lib/logCenter.ts b/web/src/lib/logCenter.ts new file mode 100644 index 0000000..04b2594 --- /dev/null +++ b/web/src/lib/logCenter.ts @@ -0,0 +1,43 @@ +/** + * Log Center integration — runtime error reporting. + * Only active when VITE_LOG_CENTER_URL is explicitly configured. + */ + +const LOG_CENTER_URL = import.meta.env.VITE_LOG_CENTER_URL || ''; +const PROJECT_ID = 'video_web'; + +export function reportError(error: Error, context?: Record) { + if (!LOG_CENTER_URL) return; + const stackLines = error.stack?.split('\n') || []; + const match = stackLines[1]?.match(/at\s+.*\s+\((.+):(\d+):\d+\)/); + + const payload = { + project_id: PROJECT_ID, + environment: import.meta.env.MODE, + level: 'ERROR', + error: { + type: error.name, + message: error.message, + file_path: match?.[1] || 'unknown', + line_number: parseInt(match?.[2] || '0'), + stack_trace: stackLines, + }, + context: { + url: window.location.href, + userAgent: navigator.userAgent, + ...context, + }, + }; + + const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' }); + if (navigator.sendBeacon) { + navigator.sendBeacon(`${LOG_CENTER_URL}/api/v1/logs/report`, blob); + } else { + fetch(`${LOG_CENTER_URL}/api/v1/logs/report`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + keepalive: true, + }).catch(() => {}); + } +} diff --git a/web/src/main.tsx b/web/src/main.tsx index 9aa52ff..02b3255 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -2,6 +2,19 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; +import { reportError } from './lib/logCenter'; + +// Global error handlers — report to Log Center +window.onerror = (_message, source, lineno, colno, error) => { + if (error) reportError(error, { source, lineno, colno }); +}; + +window.onunhandledrejection = (event: PromiseRejectionEvent) => { + const error = event.reason instanceof Error + ? event.reason + : new Error(String(event.reason)); + reportError(error, { type: 'unhandledrejection' }); +}; ReactDOM.createRoot(document.getElementById('root')!).render(