Update
This commit is contained in:
parent
7c3818f7c4
commit
f6ce09a0f5
@ -20,9 +20,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends gcc python3-dev bash nginx vim curl procps net-tools
|
||||
|
||||
RUN pip install poetry -i https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& poetry install
|
||||
RUN pip install uv -i https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
&& uv add --requirements requirements.txt
|
||||
|
||||
COPY --from=web /opt/vue-fastapi-admin/web/dist /opt/vue-fastapi-admin/web/dist
|
||||
ADD /deploy/web.conf /etc/nginx/sites-available/web.conf
|
||||
|
||||
25
Makefile
25
Makefile
@ -36,14 +36,15 @@ targets:
|
||||
|
||||
.PHONY: install
|
||||
install: ## Install dependencies
|
||||
poetry install
|
||||
uv add pyproject.toml
|
||||
|
||||
|
||||
.PHONY: run
|
||||
run: start
|
||||
|
||||
.PHONY: start
|
||||
start: ## Starts the server
|
||||
poetry run python run.py
|
||||
python run.py
|
||||
|
||||
# Check, lint and format targets
|
||||
# ------------------------------
|
||||
@ -53,28 +54,24 @@ check: check-format lint
|
||||
|
||||
.PHONY: check-format
|
||||
check-format: ## Dry-run code formatter
|
||||
poetry run black ./ --check
|
||||
poetry run isort ./ --profile black --check
|
||||
black ./ --check
|
||||
isort ./ --profile black --check
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run ruff
|
||||
poetry run ruff check ./app
|
||||
ruff check ./app
|
||||
|
||||
.PHONY: format
|
||||
format: ## Run code formatter
|
||||
poetry run black ./
|
||||
poetry run isort ./ --profile black
|
||||
black ./
|
||||
isort ./ --profile black
|
||||
|
||||
.PHONY: check-lockfile
|
||||
check-lockfile: ## Compares lock file with pyproject.toml
|
||||
poetry lock --check
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the test suite
|
||||
$(eval include .env)
|
||||
$(eval export $(sh sed 's/=.*//' .env))
|
||||
|
||||
poetry run pytest -vv -s --cache-clear ./
|
||||
pytest -vv -s --cache-clear ./
|
||||
|
||||
.PHONY: clean-db
|
||||
clean-db: ## 删除migrations文件夹和db.sqlite3
|
||||
@ -83,8 +80,8 @@ clean-db: ## 删除migrations文件夹和db.sqlite3
|
||||
|
||||
.PHONY: migrate
|
||||
migrate: ## 运行aerich migrate命令生成迁移文件
|
||||
poetry run aerich migrate
|
||||
aerich migrate
|
||||
|
||||
.PHONY: upgrade
|
||||
upgrade: ## 运行aerich upgrade命令应用迁移
|
||||
poetry run aerich upgrade
|
||||
aerich upgrade
|
||||
37
README-en.md
37
README-en.md
@ -86,19 +86,48 @@ password:123456
|
||||
#### Backend
|
||||
The backend service requires the following environment:
|
||||
- Python 3.11
|
||||
- [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
|
||||
|
||||
#### Method 1 (Recommended): Install Dependencies with uv
|
||||
1. Install uv
|
||||
```sh
|
||||
pip install uv
|
||||
```
|
||||
|
||||
2. Create and activate virtual environment
|
||||
```sh
|
||||
uv venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# or
|
||||
.\.venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. Install dependencies
|
||||
```sh
|
||||
uv add pyproject.toml
|
||||
```
|
||||
|
||||
4. Start the backend service
|
||||
```sh
|
||||
python run.py
|
||||
```
|
||||
|
||||
#### Method 2: Install Dependencies with Pip
|
||||
1. Create a Python virtual environment:
|
||||
```sh
|
||||
poetry shell
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# or
|
||||
.\venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
2. Install project dependencies:
|
||||
```sh
|
||||
poetry install
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Start the backend service:
|
||||
```sh
|
||||
make run
|
||||
python run.py
|
||||
```
|
||||
The backend service is now running, and you can visit http://localhost:9999/docs to view the API documentation.
|
||||
|
||||
|
||||
50
README.md
50
README.md
@ -87,33 +87,49 @@ password:123456
|
||||
启动项目需要以下环境:
|
||||
- Python 3.11
|
||||
|
||||
#### 方法一(推荐):[Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer) 安装依赖
|
||||
#### 方法一(推荐):使用 uv 安装依赖
|
||||
1. 安装 uv
|
||||
```sh
|
||||
pip install uv
|
||||
```
|
||||
|
||||
2. 创建并激活虚拟环境
|
||||
```sh
|
||||
uv venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
.\.venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
```sh
|
||||
uv add pyproject.toml
|
||||
```
|
||||
|
||||
4. 启动服务
|
||||
```sh
|
||||
python run.py
|
||||
```
|
||||
|
||||
#### 方法二:使用 Pip 安装依赖
|
||||
1. 创建虚拟环境
|
||||
```sh
|
||||
poetry shell
|
||||
```
|
||||
2. 安装依赖
|
||||
```sh
|
||||
poetry install
|
||||
```
|
||||
3. 启动服务
|
||||
```sh
|
||||
make run
|
||||
```
|
||||
#### 方法二:Pip 安装依赖
|
||||
1. 创建虚拟环境
|
||||
```sh
|
||||
python3.11 -m venv venv
|
||||
python3 -m venv venv
|
||||
```
|
||||
|
||||
2. 激活虚拟环境
|
||||
```sh
|
||||
source venv/bin/activate
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
.\venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
```sh
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
3. 启动服务
|
||||
|
||||
4. 启动服务
|
||||
```sh
|
||||
python run.py
|
||||
```
|
||||
|
||||
@ -30,7 +30,7 @@ async def get_audit_log_list(
|
||||
if summary:
|
||||
q &= Q(summary__icontains=summary)
|
||||
if status:
|
||||
q &= Q(status__icontains=status)
|
||||
q &= Q(status=status)
|
||||
if start_time and end_time:
|
||||
q &= Q(created_at__range=[start_time, end_time])
|
||||
elif start_time:
|
||||
|
||||
@ -43,6 +43,7 @@ def make_middlewares():
|
||||
HttpAuditLogMiddleware,
|
||||
methods=["GET", "POST", "PUT", "DELETE"],
|
||||
exclude_paths=[
|
||||
"/api/v1/base/access_token",
|
||||
"/docs",
|
||||
"/openapi.json",
|
||||
],
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import Response
|
||||
@ -45,10 +47,77 @@ class BackGroundTaskMiddleware(SimpleBaseMiddleware):
|
||||
|
||||
|
||||
class HttpAuditLogMiddleware(BaseHTTPMiddleware):
|
||||
def __init__(self, app, methods: list, exclude_paths: list):
|
||||
def __init__(self, app, methods: list[str], exclude_paths: list[str]):
|
||||
super().__init__(app)
|
||||
self.methods = methods
|
||||
self.exclude_paths = exclude_paths
|
||||
self.audit_log_paths = ["/api/v1/auditlog/list"]
|
||||
self.max_body_size = 1024 * 1024 # 1MB 响应体大小限制
|
||||
|
||||
async def get_request_args(self, request: Request) -> dict:
|
||||
args = {}
|
||||
# 获取查询参数
|
||||
for key, value in request.query_params.items():
|
||||
args[key] = value
|
||||
|
||||
# 获取请求体
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
try:
|
||||
body = await request.json()
|
||||
args.update(body)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
body = await request.form()
|
||||
args.update(body)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return args
|
||||
|
||||
async def get_response_body(self, request: Request, response: Response) -> Any:
|
||||
# 检查Content-Length
|
||||
content_length = response.headers.get("content-length")
|
||||
if content_length and int(content_length) > self.max_body_size:
|
||||
return {"code": 0, "msg": "Response too large to log", "data": None}
|
||||
|
||||
if hasattr(response, "body"):
|
||||
body = response.body
|
||||
else:
|
||||
body_chunks = []
|
||||
async for chunk in response.body_iterator:
|
||||
if not isinstance(chunk, bytes):
|
||||
chunk = chunk.encode(response.charset)
|
||||
body_chunks.append(chunk)
|
||||
|
||||
response.body_iterator = self._async_iter(body_chunks)
|
||||
body = b"".join(body_chunks)
|
||||
|
||||
if any(request.url.path.startswith(path) for path in self.audit_log_paths):
|
||||
try:
|
||||
data = self.lenient_json(body)
|
||||
# 只保留基本信息,去除详细的响应内容
|
||||
if isinstance(data, dict):
|
||||
data.pop("response_body", None)
|
||||
if "data" in data and isinstance(data["data"], list):
|
||||
for item in data["data"]:
|
||||
item.pop("response_body", None)
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return self.lenient_json(body)
|
||||
|
||||
def lenient_json(self, v: Any) -> Any:
|
||||
if isinstance(v, (str, bytes)):
|
||||
try:
|
||||
return json.loads(v)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return v
|
||||
|
||||
async def _async_iter(self, items: list[bytes]) -> AsyncGenerator[bytes, None]:
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
async def get_request_log(self, request: Request, response: Response) -> dict:
|
||||
"""
|
||||
@ -73,23 +142,29 @@ class HttpAuditLogMiddleware(BaseHTTPMiddleware):
|
||||
user_obj: User = await AuthControl.is_authed(token)
|
||||
data["user_id"] = user_obj.id if user_obj else 0
|
||||
data["username"] = user_obj.username if user_obj else ""
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
data["user_id"] = 0
|
||||
data["username"] = ""
|
||||
return data
|
||||
|
||||
async def before_request(self, request: Request):
|
||||
pass
|
||||
request_args = await self.get_request_args(request)
|
||||
request.state.request_args = request_args
|
||||
|
||||
async def after_request(self, request: Request, response: Response, process_time: int):
|
||||
if request.method in self.methods: # 请求方法为配置的记录方法
|
||||
if request.method in self.methods:
|
||||
for path in self.exclude_paths:
|
||||
if re.search(path, request.url.path, re.I) is not None:
|
||||
return
|
||||
data: dict = await self.get_request_log(request=request, response=response)
|
||||
data["response_time"] = process_time # 响应时间
|
||||
data["response_time"] = process_time
|
||||
|
||||
data["request_args"] = request.state.request_args
|
||||
data["response_body"] = await self.get_response_body(request, response)
|
||||
await AuditLog.create(**data)
|
||||
|
||||
return response
|
||||
|
||||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
||||
start_time: datetime = datetime.now()
|
||||
await self.before_request(request)
|
||||
|
||||
@ -49,7 +49,7 @@ class Menu(BaseModel, TimestampMixin):
|
||||
icon = fields.CharField(max_length=100, null=True, description="菜单图标")
|
||||
path = fields.CharField(max_length=100, description="菜单路径", index=True)
|
||||
order = fields.IntField(default=0, description="排序", index=True)
|
||||
parent_id = fields.IntField(default=0, max_length=10, description="父菜单ID", index=True)
|
||||
parent_id = fields.IntField(default=0, description="父菜单ID", index=True)
|
||||
is_hidden = fields.BooleanField(default=False, description="是否隐藏")
|
||||
component = fields.CharField(max_length=100, description="组件")
|
||||
keepalive = fields.BooleanField(default=True, description="存活")
|
||||
@ -85,3 +85,5 @@ class AuditLog(BaseModel, TimestampMixin):
|
||||
path = fields.CharField(max_length=255, default="", description="请求路径", index=True)
|
||||
status = fields.IntField(default=-1, description="状态码", index=True)
|
||||
response_time = fields.IntField(default=0, description="响应时间(单位ms)", index=True)
|
||||
request_args = fields.JSONField(null=True, description="请求参数")
|
||||
response_body = fields.JSONField(null=True, description="返回数据")
|
||||
|
||||
@ -10,8 +10,7 @@ class BaseApi(BaseModel):
|
||||
tags: str = Field(..., description="API标签", example="User")
|
||||
|
||||
|
||||
class ApiCreate(BaseApi):
|
||||
...
|
||||
class ApiCreate(BaseApi): ...
|
||||
|
||||
|
||||
class ApiUpdate(BaseApi):
|
||||
|
||||
@ -8,8 +8,7 @@ class BaseDept(BaseModel):
|
||||
parent_id: int = Field(0, description="父部门ID")
|
||||
|
||||
|
||||
class DeptCreate(BaseDept):
|
||||
...
|
||||
class DeptCreate(BaseDept): ...
|
||||
|
||||
|
||||
class DeptUpdate(BaseDept):
|
||||
|
||||
110
pyproject.toml
110
pyproject.toml
@ -1,55 +1,87 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "vue-fastapi-admin"
|
||||
version = "0.1.0"
|
||||
description = "Vue Fastapi admin"
|
||||
authors = ["mizhexiaoxiao <mizhexiaoxiao@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
fastapi = "0.111.0"
|
||||
tortoise-orm = "^0.20.1"
|
||||
pydantic = "^2.7.1"
|
||||
email-validator = "^2.0.0.post2"
|
||||
passlib = "^1.7.4"
|
||||
pyjwt = "^2.7.0"
|
||||
black = "^23.7.0"
|
||||
isort = "^5.12.0"
|
||||
ruff = "^0.0.281"
|
||||
loguru = "^0.7.0"
|
||||
pydantic-settings = "^2.0.3"
|
||||
argon2-cffi = "^23.1.0"
|
||||
pydantic-core = "^2.18.2"
|
||||
annotated-types = "^0.6.0"
|
||||
setuptools = "^70.0.0"
|
||||
uvicorn = "^0.30.1"
|
||||
h11 = "^0.14.0"
|
||||
aerich = "^0.7.2"
|
||||
authors = [
|
||||
{name = "mizhexiaoxiao", email = "mizhexiaoxiao@gmail.com"},
|
||||
]
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastapi==0.111.0",
|
||||
"tortoise-orm==0.23.0",
|
||||
"pydantic==2.10.5",
|
||||
"email-validator==2.2.0",
|
||||
"passlib==1.7.4",
|
||||
"pyjwt==2.10.1",
|
||||
"black==24.10.0",
|
||||
"isort==5.13.2",
|
||||
"ruff==0.9.1",
|
||||
"loguru==0.7.3",
|
||||
"pydantic-settings==2.7.1",
|
||||
"argon2-cffi==23.1.0",
|
||||
"pydantic-core==2.27.2",
|
||||
"annotated-types==0.7.0",
|
||||
"setuptools==75.8.0",
|
||||
"uvicorn==0.34.0",
|
||||
"h11==0.14.0",
|
||||
"aerich==0.8.1",
|
||||
"aiosqlite==0.20.0",
|
||||
"anyio==4.8.0",
|
||||
"argon2-cffi-bindings==21.2.0",
|
||||
"asyncclick==8.1.8",
|
||||
"certifi==2024.12.14",
|
||||
"cffi==1.17.1",
|
||||
"click==8.1.8",
|
||||
"dictdiffer==0.9.0",
|
||||
"dnspython==2.7.0",
|
||||
"fastapi-cli==0.0.7",
|
||||
"httpcore==1.0.7",
|
||||
"httptools==0.6.4",
|
||||
"httpx==0.28.1",
|
||||
"idna==3.10",
|
||||
"iso8601==2.1.0",
|
||||
"jinja2==3.1.5",
|
||||
"markdown-it-py==3.0.0",
|
||||
"markupsafe==3.0.2",
|
||||
"mdurl==0.1.2",
|
||||
"mypy-extensions==1.0.0",
|
||||
"orjson==3.10.14",
|
||||
"packaging==24.2",
|
||||
"pathspec==0.12.1",
|
||||
"platformdirs==4.3.6",
|
||||
"pycparser==2.22",
|
||||
"pygments==2.19.1",
|
||||
"pypika-tortoise==0.3.2",
|
||||
"python-dotenv==1.0.1",
|
||||
"python-multipart==0.0.20",
|
||||
"pytz==2024.2",
|
||||
"pyyaml==6.0.2",
|
||||
"rich==13.9.4",
|
||||
"rich-toolkit==0.13.2",
|
||||
"shellingham==1.5.4",
|
||||
"sniffio==1.3.1",
|
||||
"starlette==0.37.2",
|
||||
"typer==0.15.1",
|
||||
"typing-extensions==4.12.2",
|
||||
"ujson==5.10.0",
|
||||
"uvloop==0.21.0",
|
||||
"watchfiles==1.0.4",
|
||||
"websockets==14.1",
|
||||
"pyproject-toml>=0.1.0",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ["py310", "py311"]
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "tsinghua"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
|
||||
priority = "primary"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
extend-select = [
|
||||
# "I", # isort
|
||||
# "B", # flake8-bugbear
|
||||
# "C4", # flake8-comprehensions
|
||||
# "PGH", # pygrep-hooks
|
||||
# "RUF", # ruff
|
||||
# "W", # pycodestyle
|
||||
# "YTT", # flake8-2020
|
||||
]
|
||||
ignore = [
|
||||
lint.extend-select = []
|
||||
lint.ignore = [
|
||||
"F403",
|
||||
"F405",
|
||||
]
|
||||
|
||||
[tool.aerich]
|
||||
tortoise_orm = "app.settings.TORTOISE_ORM"
|
||||
location = "./migrations"
|
||||
|
||||
@ -1,60 +1,60 @@
|
||||
aerich==0.7.2
|
||||
aiosqlite==0.17.0
|
||||
annotated-types==0.6.0
|
||||
anyio==4.4.0
|
||||
aerich==0.8.1
|
||||
aiosqlite==0.20.0
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
argon2-cffi==23.1.0
|
||||
argon2-cffi-bindings==21.2.0
|
||||
black==23.12.1
|
||||
certifi==2024.7.4
|
||||
cffi==1.17.0
|
||||
click==8.1.7
|
||||
asyncclick==8.1.8
|
||||
black==24.10.0
|
||||
certifi==2024.12.14
|
||||
cffi==1.17.1
|
||||
click==8.1.8
|
||||
dictdiffer==0.9.0
|
||||
dnspython==2.6.1
|
||||
email_validator==2.2.0
|
||||
dnspython==2.7.0
|
||||
email-validator==2.2.0
|
||||
fastapi==0.111.0
|
||||
fastapi-cli==0.0.5
|
||||
fastapi-cli==0.0.7
|
||||
h11==0.14.0
|
||||
httpcore==1.0.5
|
||||
httptools==0.6.1
|
||||
httpx==0.27.0
|
||||
idna==3.8
|
||||
iso8601==1.1.0
|
||||
httpcore==1.0.7
|
||||
httptools==0.6.4
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
iso8601==2.1.0
|
||||
isort==5.13.2
|
||||
Jinja2==3.1.4
|
||||
loguru==0.7.2
|
||||
jinja2==3.1.5
|
||||
loguru==0.7.3
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==2.1.5
|
||||
markupsafe==3.0.2
|
||||
mdurl==0.1.2
|
||||
mypy-extensions==1.0.0
|
||||
orjson==3.10.7
|
||||
packaging==24.1
|
||||
orjson==3.10.14
|
||||
packaging==24.2
|
||||
passlib==1.7.4
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.2.2
|
||||
platformdirs==4.3.6
|
||||
pycparser==2.22
|
||||
pydantic==2.9.0b1
|
||||
pydantic-settings==2.4.0
|
||||
pydantic_core==2.23.0
|
||||
Pygments==2.18.0
|
||||
PyJWT==2.9.0
|
||||
pypika-tortoise==0.1.6
|
||||
pydantic==2.10.5
|
||||
pydantic-core==2.27.2
|
||||
pydantic-settings==2.7.1
|
||||
pygments==2.19.1
|
||||
pyjwt==2.10.1
|
||||
pypika-tortoise==0.3.2
|
||||
python-dotenv==1.0.1
|
||||
python-multipart==0.0.9
|
||||
pytz==2024.1
|
||||
PyYAML==6.0.2
|
||||
rich==13.8.0
|
||||
ruff==0.0.281
|
||||
setuptools==70.3.0
|
||||
python-multipart==0.0.20
|
||||
pytz==2024.2
|
||||
pyyaml==6.0.2
|
||||
rich==13.9.4
|
||||
rich-toolkit==0.13.2
|
||||
ruff==0.9.1
|
||||
setuptools==75.8.0
|
||||
shellingham==1.5.4
|
||||
sniffio==1.3.1
|
||||
starlette==0.37.2
|
||||
tomlkit==0.13.2
|
||||
tortoise-orm==0.20.1
|
||||
typer==0.12.5
|
||||
typing_extensions==4.12.2
|
||||
tzdata==2024.1
|
||||
tortoise-orm==0.23.0
|
||||
typer==0.15.1
|
||||
typing-extensions==4.12.2
|
||||
ujson==5.10.0
|
||||
uvicorn==0.30.6
|
||||
uvloop==0.20.0
|
||||
watchfiles==0.23.0
|
||||
websockets==13.0
|
||||
uvicorn==0.34.0
|
||||
uvloop==0.21.0; sys_platform != 'win32'
|
||||
watchfiles==1.0.4
|
||||
websockets==14.1
|
||||
|
||||
11
run.py
11
run.py
@ -1,4 +1,13 @@
|
||||
import uvicorn
|
||||
from uvicorn.config import LOGGING_CONFIG
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True, log_config="uvicorn_loggin_config.json")
|
||||
# 修改默认日志配置
|
||||
LOGGING_CONFIG["formatters"]["default"]["fmt"] = "%(asctime)s - %(levelname)s - %(message)s"
|
||||
LOGGING_CONFIG["formatters"]["default"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
|
||||
LOGGING_CONFIG["formatters"]["access"][
|
||||
"fmt"
|
||||
] = '%(asctime)s - %(levelname)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
|
||||
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=9988, reload=True, log_config=LOGGING_CONFIG)
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(levelprefix)s %(message)s",
|
||||
"use_colors": null
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": "%(asctime)s - %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr"
|
||||
},
|
||||
"access": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {
|
||||
"handlers": [
|
||||
"default"
|
||||
],
|
||||
"level": "INFO"
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"level": "INFO"
|
||||
},
|
||||
"uvicorn.access": {
|
||||
"handlers": [
|
||||
"access"
|
||||
],
|
||||
"level": "INFO",
|
||||
"propagate": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { NInput, NSelect } from 'naive-ui'
|
||||
import { NInput, NSelect, NPopover } from 'naive-ui'
|
||||
import TheIcon from '@/components/icon/TheIcon.vue'
|
||||
|
||||
import CommonPage from '@/components/page/CommonPage.vue'
|
||||
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
|
||||
@ -78,6 +79,16 @@ const methodOptions = [
|
||||
},
|
||||
]
|
||||
|
||||
function formatJSON(data) {
|
||||
try {
|
||||
return typeof data === 'string'
|
||||
? JSON.stringify(JSON.parse(data), null, 2)
|
||||
: JSON.stringify(data, null, 2)
|
||||
} catch (e) {
|
||||
return data || '无数据'
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户名称',
|
||||
@ -121,6 +132,62 @@ const columns = [
|
||||
width: 'auto',
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{
|
||||
title: '请求体',
|
||||
key: 'request_body',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: (row) => {
|
||||
return h(
|
||||
NPopover,
|
||||
{
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
},
|
||||
{
|
||||
trigger: () =>
|
||||
h('div', { style: 'cursor: pointer;' }, [h(TheIcon, { icon: 'carbon:data-view' })]),
|
||||
default: () =>
|
||||
h(
|
||||
'pre',
|
||||
{
|
||||
style:
|
||||
'max-height: 400px; overflow: auto; background-color: #f5f5f5; padding: 8px; border-radius: 4px;',
|
||||
},
|
||||
formatJSON(row.request_args)
|
||||
),
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '响应体',
|
||||
key: 'response_body',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
render: (row) => {
|
||||
return h(
|
||||
NPopover,
|
||||
{
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
},
|
||||
{
|
||||
trigger: () =>
|
||||
h('div', { style: 'cursor: pointer;' }, [h(TheIcon, { icon: 'carbon:data-view' })]),
|
||||
default: () =>
|
||||
h(
|
||||
'pre',
|
||||
{
|
||||
style:
|
||||
'max-height: 400px; overflow: auto; background-color: #f5f5f5; padding: 8px; border-radius: 4px;',
|
||||
},
|
||||
formatJSON(row.response_body)
|
||||
),
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '响应时间(s)',
|
||||
key: 'response_time',
|
||||
@ -203,7 +270,7 @@ const columns = [
|
||||
@keypress.enter="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="创建时间" :label-width="70">
|
||||
<QueryBarItem label="操作时间" :label-width="70">
|
||||
<NDatePicker
|
||||
v-model:value="datetimeRange"
|
||||
type="datetimerange"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user