-
-
Notifications
You must be signed in to change notification settings - Fork 195
Add operation log related interfaces #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
01d2ee1
Add operation log related interfaces
wu-clan 0abcb98
Update to native ASGI middleware
wu-clan c4d1f3d
add the opera model class to the __init__.py
wu-clan 3c3f543
Merge branch 'master' into add-opera-log
wu-clan ae904fb
Executable code collation
wu-clan 91c4389
Reply to the access middleware
wu-clan 41ef0d0
Using the request extension params in the login log
wu-clan 0d387b7
Fix the whitelist list
wu-clan a91a241
Fix username resolution
wu-clan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
from typing import Annotated | ||
|
||
from fastapi import APIRouter, Query | ||
|
||
from backend.app.common.casbin_rbac import DependsRBAC | ||
from backend.app.common.jwt import DependsJwtAuth | ||
from backend.app.common.pagination import PageDepends, paging_data | ||
from backend.app.common.response.response_schema import response_base | ||
from backend.app.database.db_mysql import CurrentSession | ||
from backend.app.schemas.opera_log import GetAllOperaLog | ||
from backend.app.services.opera_log_service import OperaLogService | ||
|
||
router = APIRouter() | ||
|
||
|
||
@router.get('', summary='(模糊条件)分页获取操作日志', dependencies=[DependsJwtAuth, PageDepends]) | ||
async def get_all_opera_logs( | ||
db: CurrentSession, | ||
username: Annotated[str | None, Query()] = None, | ||
status: Annotated[bool | None, Query()] = None, | ||
ipaddr: Annotated[str | None, Query()] = None, | ||
): | ||
log_select = await OperaLogService.get_select(username=username, status=status, ipaddr=ipaddr) | ||
page_data = await paging_data(db, log_select, GetAllOperaLog) | ||
return response_base.success(data=page_data) | ||
|
||
|
||
@router.delete('', summary='(批量)删除操作日志', dependencies=[DependsRBAC]) | ||
async def delete_opera_log(pk: Annotated[list[int], Query(...)]): | ||
count = await OperaLogService.delete(pk) | ||
if count > 0: | ||
return response_base.success() | ||
return response_base.fail() | ||
|
||
|
||
@router.delete('/all', summary='清空操作日志', dependencies=[DependsRBAC]) | ||
async def delete_all_opera_logs(): | ||
count = await OperaLogService.delete_all() | ||
if count > 0: | ||
return response_base.success() | ||
return response_base.fail() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
from typing import NoReturn | ||
|
||
from sqlalchemy import select, desc, and_, delete, Select | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
|
||
from backend.app.crud.base import CRUDBase | ||
from backend.app.models import OperaLog | ||
from backend.app.schemas.opera_log import CreateOperaLog, UpdateOperaLog | ||
|
||
|
||
class CRUDOperaLogDao(CRUDBase[OperaLog, CreateOperaLog, UpdateOperaLog]): | ||
async def get_all(self, username: str | None = None, status: bool | None = None, ipaddr: str | None = None) -> Select: | ||
se = select(self.model).order_by(desc(self.model.create_time)) | ||
where_list = [] | ||
if username: | ||
where_list.append(self.model.username.like(f'%{username}%')) | ||
if status is not None: | ||
where_list.append(self.model.status == status) | ||
if ipaddr: | ||
where_list.append(self.model.ipaddr.like(f'%{ipaddr}%')) | ||
if where_list: | ||
se = se.where(and_(*where_list)) | ||
return se | ||
|
||
async def create(self, db: AsyncSession, obj_in: CreateOperaLog) -> NoReturn: | ||
await self.create_(db, obj_in) | ||
|
||
async def delete(self, db: AsyncSession, pk: list[int]) -> int: | ||
logs = await db.execute(delete(self.model).where(self.model.id.in_(pk))) | ||
return logs.rowcount | ||
|
||
async def delete_all(self, db: AsyncSession) -> int: | ||
logs = await db.execute(delete(self.model)) | ||
return logs.rowcount | ||
|
||
|
||
OperaLogDao: CRUDOperaLogDao = CRUDOperaLogDao(OperaLog) |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
import json | ||
from datetime import datetime | ||
from typing import Any | ||
|
||
from fastapi import UploadFile | ||
from starlette.background import BackgroundTask | ||
from starlette.requests import Request | ||
from starlette.types import ASGIApp, Scope, Receive, Send | ||
from user_agents import parse | ||
|
||
from backend.app.common.log import log | ||
from backend.app.core.conf import settings | ||
from backend.app.schemas.opera_log import CreateOperaLog | ||
from backend.app.services.opera_log_service import OperaLogService | ||
from backend.app.utils import request_parse | ||
|
||
|
||
class OperaLogMiddleware: | ||
"""操作日志中间件""" | ||
|
||
def __init__(self, app: ASGIApp): | ||
self.app = app | ||
|
||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | ||
if scope['type'] != 'http': | ||
await self.app(scope, receive, send) | ||
return | ||
|
||
request = Request(scope=scope, receive=receive) | ||
|
||
# 排除记录白名单 | ||
path = request.url.path | ||
if path in settings.OPERA_LOG_EXCLUDE: | ||
await self.app(scope, receive, send) | ||
return | ||
|
||
# 请求信息解析 | ||
ip = await request_parse.get_request_ip(request) | ||
user_agent = request.headers.get('User-Agent') | ||
_, os, browser = str(parse(user_agent)).replace(' ', '').split('/') | ||
if settings.LOCATION_PARSE == 'online': | ||
location = await request_parse.get_location_online(ip, user_agent) | ||
elif settings.LOCATION_PARSE == 'offline': | ||
location = request_parse.get_location_offline(ip) | ||
else: | ||
location = '未知' | ||
if request.user.is_authenticated: | ||
username = request.user.username | ||
else: | ||
username = None | ||
method = request.method | ||
args = dict(request.query_params) | ||
form_data = await request.form() | ||
if len(form_data) > 0: | ||
args = json.dumps( | ||
args.update({k: v.filename if isinstance(v, UploadFile) else v for k, v in form_data.items()}), | ||
ensure_ascii=False, | ||
) | ||
else: | ||
body = await request.body() | ||
if body: | ||
json_data = await request.json() | ||
args = json.dumps(args.update(json_data), ensure_ascii=False) | ||
args = str(args) if len(args) > 0 else None | ||
|
||
# 设置附加请求信息 | ||
request.state.ip = ip | ||
request.state.location = location | ||
request.state.os = os | ||
request.state.browser = browser | ||
|
||
# 预置响应信息 | ||
code: int = 200 | ||
msg: str = 'Success' | ||
status: bool = True | ||
err: Any = None | ||
|
||
# 执行请求 | ||
start_time = datetime.now() | ||
try: | ||
await self.app(request.scope, request.receive, send) | ||
log.info('3') | ||
except Exception as e: | ||
log.info('4') | ||
# log.exception(e) | ||
code = getattr(e, 'code', 500) | ||
msg = getattr(e, 'msg', 'Internal Server Error') | ||
status = False | ||
err = e | ||
end_time = datetime.now() | ||
summary = request.scope.get('route').summary | ||
title = summary if summary != '' else request.scope.get('route').summary | ||
cost_time = (end_time - start_time).total_seconds() / 1000.0 | ||
|
||
# 日志创建 | ||
opera_log_in = CreateOperaLog( | ||
username=username, | ||
method=method, | ||
title=title, | ||
path=path, | ||
ipaddr=ip, | ||
location=location, | ||
args=args, | ||
status=status, | ||
code=code, | ||
msg=msg, | ||
cost_time=cost_time, | ||
opera_time=start_time, | ||
) | ||
back = BackgroundTask(OperaLogService.create, opera_log_in) | ||
await back() | ||
|
||
# 错误抛出 | ||
if err: | ||
raise err from None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
from datetime import datetime | ||
|
||
from sqlalchemy import String, func | ||
from sqlalchemy.dialects.mysql import JSON | ||
from sqlalchemy.orm import Mapped, mapped_column | ||
|
||
from backend.app.database.base_class import DataClassBase, id_key | ||
|
||
|
||
class OperaLog(DataClassBase): | ||
"""操作日志表""" | ||
|
||
__tablename__ = 'sys_opera_log' | ||
|
||
id: Mapped[id_key] = mapped_column(init=False) | ||
username: Mapped[str | None] = mapped_column(String(20), comment='用户名') | ||
method: Mapped[str] = mapped_column(String(20), comment='请求类型') | ||
title: Mapped[str] = mapped_column(String(255), comment='操作模块') | ||
path: Mapped[str] = mapped_column(String(500), comment='请求路径') | ||
ipaddr: Mapped[str] = mapped_column(String(50), comment='IP地址') | ||
location: Mapped[str] = mapped_column(String(50), comment='归属地') | ||
args: Mapped[str | None] = mapped_column(JSON(), comment='请求参数') | ||
status: Mapped[bool] = mapped_column(comment='操作状态(0异常 1正常)') | ||
code: Mapped[int] = mapped_column(insert_default=200, comment='操作状态码') | ||
msg: Mapped[str | None] = mapped_column(String(255), comment='提示消息') | ||
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='请求耗时ms') | ||
opera_time: Mapped[datetime] = mapped_column(comment='操作时间') | ||
create_time: Mapped[datetime] = mapped_column(init=False, default=func.now(), comment='创建时间') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
from datetime import datetime | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class OperaLogBase(BaseModel): | ||
username: str | None | ||
method: str | ||
title: str | ||
path: str | ||
ipaddr: str | ||
location: str | ||
args: str | None | ||
status: bool | ||
code: int | ||
msg: str | None | ||
cost_time: float | ||
opera_time: datetime | ||
|
||
|
||
class CreateOperaLog(OperaLogBase): | ||
pass | ||
|
||
|
||
class UpdateOperaLog(OperaLogBase): | ||
pass | ||
|
||
|
||
class GetAllOperaLog(OperaLogBase): | ||
id: int | ||
create_time: datetime | ||
|
||
class Config: | ||
orm_mode = True |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.