一些修改
This commit is contained in:
parent
4edb0fa93a
commit
35118b438f
@ -2,6 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from flask import Flask, redirect, url_for
|
from flask import Flask, redirect, url_for
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from app.config import DevelopmentConfig, ProductionConfig
|
from app.config import DevelopmentConfig, ProductionConfig
|
||||||
from app.extensions import db, login_manager, scheduler
|
from app.extensions import db, login_manager, scheduler
|
||||||
@ -97,6 +98,22 @@ def register_template_filters(app: Flask) -> None:
|
|||||||
|
|
||||||
return expr
|
return expr
|
||||||
|
|
||||||
|
@app.template_filter("to_cst")
|
||||||
|
def to_cst(dt, fmt: str = "%Y-%m-%d %H:%M:%S"):
|
||||||
|
"""
|
||||||
|
将 UTC 时间转换为中国标准时间字符串,如果值为空则返回空字符串。
|
||||||
|
"""
|
||||||
|
if not dt:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
tz = ZoneInfo(app.config.get("SCHEDULER_TIMEZONE", "Asia/Shanghai"))
|
||||||
|
# 如果是 naive datetime,认为是 UTC
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
|
||||||
|
return dt.astimezone(tz).strftime(fmt)
|
||||||
|
except Exception:
|
||||||
|
return str(dt)
|
||||||
|
|
||||||
|
|
||||||
def init_scheduler(app: Flask) -> None:
|
def init_scheduler(app: Flask) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -26,8 +26,7 @@ def _parse_json_field(raw: Optional[str]) -> Optional[Dict[str, Any]]:
|
|||||||
|
|
||||||
def execute_api(api_config: ApiConfig) -> None:
|
def execute_api(api_config: ApiConfig) -> None:
|
||||||
"""
|
"""
|
||||||
Execute an API call based on ApiConfig, apply retry logic, and persist ApiCallLog.
|
根据配置调用 API,带重试,并将每次尝试都记录为日志(保留历史)。
|
||||||
Only the final attempt is logged to avoid noise.
|
|
||||||
"""
|
"""
|
||||||
request_time = datetime.utcnow()
|
request_time = datetime.utcnow()
|
||||||
headers = _parse_json_field(api_config.headers) or {}
|
headers = _parse_json_field(api_config.headers) or {}
|
||||||
@ -42,13 +41,14 @@ def execute_api(api_config: ApiConfig) -> None:
|
|||||||
delay = max(api_config.retry_interval_seconds, 1)
|
delay = max(api_config.retry_interval_seconds, 1)
|
||||||
timeout = max(api_config.timeout_seconds, 1)
|
timeout = max(api_config.timeout_seconds, 1)
|
||||||
attempt = 0
|
attempt = 0
|
||||||
last_response: Optional[Response] = None
|
total_attempts = retries + 1
|
||||||
last_error: Optional[str] = None
|
last_error: Optional[str] = None
|
||||||
start_ts = time.time()
|
|
||||||
|
|
||||||
while attempt <= retries:
|
while attempt < total_attempts:
|
||||||
try:
|
last_response: Optional[Response] = None
|
||||||
|
start_ts = time.time()
|
||||||
attempt += 1
|
attempt += 1
|
||||||
|
try:
|
||||||
last_response = requests.request(
|
last_response = requests.request(
|
||||||
method=api_config.http_method,
|
method=api_config.http_method,
|
||||||
url=api_config.url,
|
url=api_config.url,
|
||||||
@ -60,13 +60,11 @@ def execute_api(api_config: ApiConfig) -> None:
|
|||||||
)
|
)
|
||||||
if 200 <= last_response.status_code < 300:
|
if 200 <= last_response.status_code < 300:
|
||||||
last_error = None
|
last_error = None
|
||||||
break
|
else:
|
||||||
last_error = f"Non-2xx status: {last_response.status_code}"
|
body_snippet = (last_response.text or "")[:200]
|
||||||
|
last_error = f"Non-2xx status: {last_response.status_code}, body: {body_snippet}"
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
last_response = None
|
|
||||||
last_error = str(exc)
|
last_error = str(exc)
|
||||||
if attempt <= retries:
|
|
||||||
time.sleep(delay)
|
|
||||||
|
|
||||||
duration_ms = int((time.time() - start_ts) * 1000)
|
duration_ms = int((time.time() - start_ts) * 1000)
|
||||||
success = last_error is None
|
success = last_error is None
|
||||||
@ -77,7 +75,7 @@ def execute_api(api_config: ApiConfig) -> None:
|
|||||||
response_time=datetime.utcnow(),
|
response_time=datetime.utcnow(),
|
||||||
success=success,
|
success=success,
|
||||||
http_status_code=last_response.status_code if last_response else None,
|
http_status_code=last_response.status_code if last_response else None,
|
||||||
error_message=last_error,
|
error_message=(f"[第{attempt}次尝试/{total_attempts}] {last_error}" if last_error else None),
|
||||||
response_body=(last_response.text[:2000] if last_response and last_response.text else None),
|
response_body=(last_response.text[:2000] if last_response and last_response.text else None),
|
||||||
duration_ms=duration_ms,
|
duration_ms=duration_ms,
|
||||||
)
|
)
|
||||||
@ -88,7 +86,12 @@ def execute_api(api_config: ApiConfig) -> None:
|
|||||||
logger.exception("Failed to persist ApiCallLog for api_id=%s", api_config.id)
|
logger.exception("Failed to persist ApiCallLog for api_id=%s", api_config.id)
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
|
||||||
if not success:
|
if success:
|
||||||
|
break
|
||||||
|
if attempt < total_attempts:
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
if last_error:
|
||||||
logger.warning("API call failed for api_id=%s error=%s", api_config.id, last_error)
|
logger.warning("API call failed for api_id=%s error=%s", api_config.id, last_error)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="btn btn-sm btn-outline-primary" href="{{ url_for('apis.edit_api', api_id=api.id) }}">编辑</a>
|
<a class="btn btn-sm btn-outline-primary" href="{{ url_for('apis.edit_api', api_id=api.id) }}">编辑</a>
|
||||||
|
<form action="{{ url_for('apis.copy_api', api_id=api.id) }}" method="post" class="d-inline">
|
||||||
|
<button class="btn btn-sm btn-outline-info" type="submit">复制</button>
|
||||||
|
</form>
|
||||||
<form action="{{ url_for('apis.toggle_api', api_id=api.id) }}" method="post" class="d-inline">
|
<form action="{{ url_for('apis.toggle_api', api_id=api.id) }}" method="post" class="d-inline">
|
||||||
<button class="btn btn-sm btn-outline-warning" type="submit">
|
<button class="btn btn-sm btn-outline-warning" type="submit">
|
||||||
{% if api.enabled %}停用{% else %}启用{% endif %}
|
{% if api.enabled %}停用{% else %}启用{% endif %}
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">{{ log.api.name }}</h5>
|
<h5 class="card-title">{{ log.api.name }}</h5>
|
||||||
<p class="card-text mb-1"><strong>请求时间:</strong> {{ log.request_time }}</p>
|
<p class="card-text mb-1"><strong>请求时间:</strong> {{ log.request_time|to_cst }}</p>
|
||||||
<p class="card-text mb-1"><strong>响应时间:</strong> {{ log.response_time }}</p>
|
<p class="card-text mb-1"><strong>响应时间:</strong> {{ log.response_time|to_cst }}</p>
|
||||||
<p class="card-text mb-1"><strong>是否成功:</strong> {{ log.success }}</p>
|
<p class="card-text mb-1"><strong>是否成功:</strong> {{ log.success }}</p>
|
||||||
<p class="card-text mb-1"><strong>HTTP 状态码:</strong> {{ log.http_status_code or '-' }}</p>
|
<p class="card-text mb-1"><strong>HTTP 状态码:</strong> {{ log.http_status_code or '-' }}</p>
|
||||||
<p class="card-text mb-1"><strong>耗时 (ms):</strong> {{ log.duration_ms or '-' }}</p>
|
<p class="card-text mb-1"><strong>耗时 (ms):</strong> {{ log.duration_ms or '-' }}</p>
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
{% for log in logs %}
|
{% for log in logs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ log.api.name }}</td>
|
<td>{{ log.api.name }}</td>
|
||||||
<td>{{ log.request_time }}</td>
|
<td>{{ log.request_time|to_cst }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if log.success %}
|
{% if log.success %}
|
||||||
<span class="badge bg-success">成功</span>
|
<span class="badge bg-success">成功</span>
|
||||||
@ -61,13 +61,13 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
{% if pagination.has_prev %}
|
{% if pagination.has_prev %}
|
||||||
<li class="page-item"><a class="page-link" href="{{ url_for('logs.list_logs', page=pagination.prev_num, per_page=pagination.per_page, **request.args) }}">上一页</a></li>
|
<li class="page-item"><a class="page-link" href="{{ url_for('logs.list_logs', page=pagination.prev_num, per_page=pagination.per_page, **query_args) }}">上一页</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled"><span class="page-link">上一页</span></li>
|
<li class="page-item disabled"><span class="page-link">上一页</span></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="page-item disabled"><span class="page-link">第 {{ pagination.page }} / {{ pagination.pages }} 页</span></li>
|
<li class="page-item disabled"><span class="page-link">第 {{ pagination.page }} / {{ pagination.pages }} 页</span></li>
|
||||||
{% if pagination.has_next %}
|
{% if pagination.has_next %}
|
||||||
<li class="page-item"><a class="page-link" href="{{ url_for('logs.list_logs', page=pagination.next_num, per_page=pagination.per_page, **request.args) }}">下一页</a></li>
|
<li class="page-item"><a class="page-link" href="{{ url_for('logs.list_logs', page=pagination.next_num, per_page=pagination.per_page, **query_args) }}">下一页</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled"><span class="page-link">下一页</span></li>
|
<li class="page-item disabled"><span class="page-link">下一页</span></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -104,6 +104,32 @@ def delete_api(api_id: int):
|
|||||||
return redirect(url_for("apis.list_apis"))
|
return redirect(url_for("apis.list_apis"))
|
||||||
|
|
||||||
|
|
||||||
|
@apis_bp.route("/<int:api_id>/copy", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def copy_api(api_id: int):
|
||||||
|
src = ApiConfig.query.get_or_404(api_id)
|
||||||
|
new_api = ApiConfig(
|
||||||
|
name=f"{src.name}-副本",
|
||||||
|
description=src.description,
|
||||||
|
url=src.url,
|
||||||
|
http_method=src.http_method,
|
||||||
|
headers=src.headers,
|
||||||
|
query_params=src.query_params,
|
||||||
|
body=src.body,
|
||||||
|
schedule_type=src.schedule_type,
|
||||||
|
schedule_expression=src.schedule_expression,
|
||||||
|
timeout_seconds=src.timeout_seconds,
|
||||||
|
retry_times=src.retry_times,
|
||||||
|
retry_interval_seconds=src.retry_interval_seconds,
|
||||||
|
enabled=False, # 复制后默认不启用,避免未确认即自动调度
|
||||||
|
created_by=current_user.id,
|
||||||
|
)
|
||||||
|
db.session.add(new_api)
|
||||||
|
db.session.commit()
|
||||||
|
flash("已复制为新配置,请查看并启用。", "success")
|
||||||
|
return redirect(url_for("apis.edit_api", api_id=new_api.id))
|
||||||
|
|
||||||
|
|
||||||
@apis_bp.route("/<int:api_id>/run", methods=["POST"])
|
@apis_bp.route("/<int:api_id>/run", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def run_now(api_id: int):
|
def run_now(api_id: int):
|
||||||
|
|||||||
@ -32,7 +32,11 @@ def list_logs():
|
|||||||
page = int(request.args.get("page", 1))
|
page = int(request.args.get("page", 1))
|
||||||
per_page = int(request.args.get("per_page", 10))
|
per_page = int(request.args.get("per_page", 10))
|
||||||
pagination = query.order_by(ApiCallLog.request_time.desc()).paginate(page=page, per_page=per_page, error_out=False)
|
pagination = query.order_by(ApiCallLog.request_time.desc()).paginate(page=page, per_page=per_page, error_out=False)
|
||||||
return render_template("logs/list.html", form=form, pagination=pagination, logs=pagination.items)
|
# 组装分页参数时避免重复 page/per_page
|
||||||
|
query_args = request.args.to_dict(flat=True)
|
||||||
|
query_args.pop("page", None)
|
||||||
|
query_args.pop("per_page", None)
|
||||||
|
return render_template("logs/list.html", form=form, pagination=pagination, logs=pagination.items, query_args=query_args)
|
||||||
|
|
||||||
|
|
||||||
@logs_bp.route("/<int:log_id>", methods=["GET"])
|
@logs_bp.route("/<int:log_id>", methods=["GET"])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user