forked from iti-framework/iTi-Flask
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
212 lines
7.7 KiB
Python
212 lines
7.7 KiB
Python
from iti.applications.models import SysLog
|
|
from .db import db
|
|
from flask import request, current_app, g
|
|
import time
|
|
import json
|
|
from flask_jwt_extended import current_user
|
|
from iti.applications.common.utils.sys_log_helper import (
|
|
summarize_response_json,
|
|
safe_json_dumps,
|
|
truncate_text,
|
|
)
|
|
from iti.applications.common.enums import LogType
|
|
|
|
|
|
def _summarize_response_json(obj: dict, items_sample_size: int) -> dict:
|
|
"""
|
|
响应结果摘要化
|
|
"""
|
|
try:
|
|
if isinstance(obj, dict) and isinstance(obj.get("data"), dict):
|
|
data = obj["data"]
|
|
if isinstance(data.get("items"), list):
|
|
items = data["items"]
|
|
obj = {
|
|
**obj,
|
|
"data": {
|
|
**data,
|
|
"items": {
|
|
"count": len(items),
|
|
"sample": items[:items_sample_size],
|
|
},
|
|
},
|
|
}
|
|
return obj
|
|
except Exception:
|
|
return obj
|
|
|
|
|
|
def init_sys_log(app):
|
|
max_body_chars = int(app.config.get("SYSLOG_MAX_BODY_CHARS", 2048))
|
|
items_sample_size = int(app.config.get("SYSLOG_ITEMS_SAMPLE", 5))
|
|
|
|
@app.before_request
|
|
def _syslog_before_request():
|
|
try:
|
|
view_func = app.view_functions.get(request.endpoint)
|
|
meta = getattr(view_func, "_sys_log_meta", None) if view_func else None
|
|
g._syslog_meta = meta
|
|
if not meta:
|
|
return
|
|
g._syslog_start = time.time()
|
|
headers = {k: request.headers.getlist(k) for k in request.headers.keys()}
|
|
g._syslog_req = {
|
|
"method": request.method,
|
|
"path": request.path,
|
|
"headers": headers,
|
|
"query": request.args.to_dict(),
|
|
"body": request.get_json(silent=True) or {},
|
|
"user_id": current_user.id if current_user else None,
|
|
"user_agent": request.headers.get("User-Agent", "None"),
|
|
"ip": request.remote_addr or request.headers.get("X-Forwarded-For"),
|
|
}
|
|
except Exception as e:
|
|
current_app.logger.debug(f"syslog before_request error: {e}")
|
|
|
|
@app.after_request
|
|
def _syslog_after_request(response):
|
|
try:
|
|
meta = getattr(g, "_syslog_meta", None)
|
|
if not meta or getattr(g, "_syslog_logged", False):
|
|
return response
|
|
|
|
execution_time = None
|
|
if getattr(g, "_syslog_start", None) is not None and meta.get(
|
|
"execute_time", True
|
|
):
|
|
execution_time = (time.time() - g._syslog_start) * 1000
|
|
|
|
is_stream = getattr(response, "direct_passthrough", False)
|
|
resp_json = None
|
|
resp_text = None
|
|
if not is_stream and hasattr(response, "is_json") and response.is_json:
|
|
resp_json = response.get_json(silent=True)
|
|
if resp_json is not None and isinstance(resp_json, dict):
|
|
summarized = summarize_response_json(resp_json, items_sample_size)
|
|
resp_text = safe_json_dumps(
|
|
summarized, ensure_ascii=False, max_chars=max_body_chars
|
|
)
|
|
else:
|
|
if not is_stream:
|
|
data = response.get_data(as_text=True)
|
|
resp_text = truncate_text(data or "", max_body_chars)
|
|
else:
|
|
resp_text = json.dumps(
|
|
{
|
|
"stream": True,
|
|
"content_type": response.headers.get("Content-Type"),
|
|
"content_length": response.calculate_content_length()
|
|
if hasattr(response, "calculate_content_length")
|
|
else None,
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
|
|
# success rule: prefer business code, then HTTP 2xx
|
|
success = False
|
|
if isinstance(resp_json, dict) and "code" in resp_json:
|
|
success = bool(resp_json.get("code") == 200)
|
|
else:
|
|
success = 200 <= getattr(response, "status_code", 0) < 300
|
|
|
|
req = getattr(g, "_syslog_req", {})
|
|
log = SysLog(
|
|
name=meta.get("name", ""),
|
|
desc=meta.get("desc", ""),
|
|
type=(
|
|
meta.get("type")
|
|
if isinstance(meta.get("type"), LogType)
|
|
else (meta.get("type") or LogType.OPERATION)
|
|
),
|
|
method=req.get("method"),
|
|
user_id=req.get("user_id"),
|
|
path=req.get("path"),
|
|
ip=req.get("ip"),
|
|
user_agent=req.get("user_agent"),
|
|
headers=json.dumps(req.get("headers"), ensure_ascii=False),
|
|
query_params=json.dumps(req.get("query"), ensure_ascii=False),
|
|
body_params=json.dumps(req.get("body"), ensure_ascii=False),
|
|
execution_time=execution_time,
|
|
response=resp_text,
|
|
success=success,
|
|
)
|
|
|
|
if meta.get("save_db", True):
|
|
db.session.add(log)
|
|
db.session.commit()
|
|
g._syslog_logged = True
|
|
except Exception as e:
|
|
current_app.logger.debug(f"syslog after_request error: {e}")
|
|
return response
|
|
|
|
@app.teardown_request
|
|
def _syslog_teardown_request(exc):
|
|
try:
|
|
meta = getattr(g, "_syslog_meta", None)
|
|
if not meta or getattr(g, "_syslog_logged", False):
|
|
return
|
|
# only log exceptions that didn't produce a normal response
|
|
req = getattr(g, "_syslog_req", {})
|
|
execution_time = None
|
|
if getattr(g, "_syslog_start", None) is not None and meta.get(
|
|
"execute_time", True
|
|
):
|
|
execution_time = (time.time() - g._syslog_start) * 1000
|
|
|
|
log = SysLog(
|
|
name=meta.get("name", ""),
|
|
desc=meta.get("desc", ""),
|
|
type=(
|
|
meta.get("type")
|
|
if isinstance(meta.get("type"), LogType)
|
|
else (meta.get("type") or LogType.OPERATION)
|
|
),
|
|
method=req.get("method"),
|
|
user_id=req.get("user_id"),
|
|
path=req.get("path"),
|
|
ip=req.get("ip"),
|
|
user_agent=req.get("user_agent"),
|
|
headers=json.dumps(req.get("headers"), ensure_ascii=False),
|
|
query_params=json.dumps(req.get("query"), ensure_ascii=False),
|
|
body_params=json.dumps(req.get("body"), ensure_ascii=False),
|
|
execution_time=execution_time,
|
|
response=None,
|
|
success=False,
|
|
exception=str(exc) if exc else None,
|
|
)
|
|
if meta.get("save_db", True):
|
|
db.session.add(log)
|
|
db.session.commit()
|
|
g._syslog_logged = True
|
|
except Exception as e:
|
|
current_app.logger.debug(f"syslog teardown_request error: {e}")
|
|
|
|
|
|
def sys_log(
|
|
name: str = "",
|
|
desc: str = "",
|
|
save_db: bool = True,
|
|
execute_time: bool = True,
|
|
type: LogType = LogType.OPERATION,
|
|
):
|
|
"""
|
|
轻量级标注:仅为视图函数打上 _sys_log_meta 属性,
|
|
具体日志在 init_log 注册的钩子里处理。
|
|
"""
|
|
|
|
def decorator(func):
|
|
setattr(
|
|
func,
|
|
"_sys_log_meta",
|
|
{
|
|
"name": name,
|
|
"desc": desc,
|
|
"save_db": save_db,
|
|
"execute_time": execute_time,
|
|
"type": type,
|
|
},
|
|
)
|
|
return func
|
|
|
|
return decorator
|