Skip to content

Commit 5f604e9

Browse files
EstrellaXDclaude
andcommitted
perf(logging): optimize logging with rotation, async I/O, and lazy formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c173454 commit 5f604e9

39 files changed

Lines changed: 198 additions & 116 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
# [Unreleased]
2+
3+
## Backend
4+
5+
### Performance
6+
7+
- 优化日志系统性能,适配长时间运行的 Docker 环境
8+
- `RotatingFileHandler` 替代 `FileHandler`(5 MB 轮转,最多 3 个备份 = 20 MB 上限)
9+
- `QueueHandler` + `QueueListener` 实现异步日志写入,不阻塞事件循环
10+
- `GET /api/log` 仅读取最后 512 KB,防止大日志文件导致 OOM
11+
- 所有 `logger.debug(f"...")` 转为惰性 `%s` 格式化(~80 处),避免禁用 debug 时的无效字符串拼接
12+
- 移除高频缓存命中的 debug 日志噪音(Mikan、TMDB 解析器)
13+
14+
---
15+
116
# [3.2.3-beta.3] - 2026-01-30
217

318
## Backend

backend/src/module/ab_decorator/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async def wrapper(*args, **kwargs):
1616
try:
1717
return await func(*args, **kwargs)
1818
except Exception as e:
19-
logger.debug(f"URL: {args[0]}")
19+
logger.debug("URL: %s", args[0])
2020
logger.warning(e)
2121
logger.warning("Cannot connect to qBittorrent. Wait 5 min and retry...")
2222
await asyncio.sleep(300)
@@ -31,7 +31,7 @@ async def wrapper(*args, **kwargs):
3131
try:
3232
return await func(*args, **kwargs)
3333
except Exception as e:
34-
logger.debug(f"URL: {args[0]}")
34+
logger.debug("URL: %s", args[0])
3535
logger.warning("Wrong API response.")
3636
logger.debug(e)
3737

backend/src/module/api/log.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,26 @@
88
router = APIRouter(prefix="/log", tags=["log"])
99

1010

11+
_TAIL_BYTES = 512 * 1024 # 512 KB
12+
13+
1114
@router.get("", response_model=str, dependencies=[Depends(get_current_user)])
1215
async def get_log():
1316
if LOG_PATH.exists():
1417
with open(LOG_PATH, "rb") as f:
15-
return Response(f.read(), media_type="text/plain")
18+
f.seek(0, 2)
19+
size = f.tell()
20+
if size > _TAIL_BYTES:
21+
f.seek(-_TAIL_BYTES, 2)
22+
data = f.read()
23+
# Drop first partial line
24+
idx = data.find(b"\n")
25+
if idx != -1:
26+
data = data[idx + 1 :]
27+
else:
28+
f.seek(0)
29+
data = f.read()
30+
return Response(data, media_type="text/plain")
1631
else:
1732
return Response("Log file not found", status_code=404)
1833

backend/src/module/conf/log.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import atexit
12
import logging
3+
from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
24
from pathlib import Path
5+
from queue import SimpleQueue
36

47
from .config import settings
58

69
LOG_ROOT = Path("data")
710
LOG_PATH = LOG_ROOT / "log.txt"
811

12+
_listener: QueueListener | None = None
13+
914

1015
def setup_logger(level: int = logging.INFO, reset: bool = False):
16+
global _listener
17+
1118
level = logging.DEBUG if settings.log.debug_enable else level
1219
LOG_ROOT.mkdir(exist_ok=True)
1320

@@ -19,15 +26,26 @@ def setup_logger(level: int = logging.INFO, reset: bool = False):
1926
logging.addLevelName(logging.WARNING, "WARNING:")
2027
LOGGING_FORMAT = "[%(asctime)s] %(levelname)-8s %(message)s"
2128
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
29+
formatter = logging.Formatter(LOGGING_FORMAT, datefmt=TIME_FORMAT)
30+
31+
file_handler = RotatingFileHandler(
32+
LOG_PATH, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"
33+
)
34+
file_handler.setFormatter(formatter)
35+
36+
stream_handler = logging.StreamHandler()
37+
stream_handler.setFormatter(formatter)
38+
39+
log_queue: SimpleQueue = SimpleQueue()
40+
queue_handler = QueueHandler(log_queue)
41+
42+
_listener = QueueListener(log_queue, file_handler, stream_handler, respect_handler_level=True)
43+
_listener.start()
44+
atexit.register(_listener.stop)
45+
2246
logging.basicConfig(
2347
level=level,
24-
format=LOGGING_FORMAT,
25-
datefmt=TIME_FORMAT,
26-
encoding="utf-8",
27-
handlers=[
28-
logging.FileHandler(LOG_PATH, encoding="utf-8"),
29-
logging.StreamHandler(),
30-
],
48+
handlers=[queue_handler],
3149
)
3250

3351
# Suppress verbose HTTP request logs from httpx

backend/src/module/conf/uvicorn_logging.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
},
1212
"handlers": {
1313
"file": {
14-
"class": "logging.FileHandler",
14+
"class": "logging.handlers.RotatingFileHandler",
1515
"filename": LOG_PATH,
1616
"formatter": "default",
1717
"encoding": "utf-8",
18+
"maxBytes": 5242880,
19+
"backupCount": 3,
1820
},
1921
"console": {
2022
"class": "logging.StreamHandler",

backend/src/module/database/bangumi.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,11 @@ def find_semantic_duplicate(self, data: Bangumi) -> Optional[Bangumi]:
116116
)
117117
if is_semantic_match:
118118
logger.debug(
119-
f"[Database] Found semantic duplicate: '{data.title_raw}' matches "
120-
f"existing '{candidate.title_raw}' (official: {data.official_title})"
119+
"[Database] Found semantic duplicate: '%s' matches "
120+
"existing '%s' (official: %s)",
121+
data.title_raw,
122+
candidate.title_raw,
123+
data.official_title,
121124
)
122125
return candidate
123126

@@ -177,7 +180,9 @@ def _is_duplicate(self, data: Bangumi) -> bool:
177180
def add(self, data: Bangumi) -> bool:
178181
if self._is_duplicate(data):
179182
logger.debug(
180-
f"[Database] Skipping duplicate: {data.official_title} ({data.group_name})"
183+
"[Database] Skipping duplicate: %s (%s)",
184+
data.official_title,
185+
data.group_name,
181186
)
182187
return False
183188

@@ -195,7 +200,7 @@ def add(self, data: Bangumi) -> bool:
195200
self.session.add(data)
196201
self.session.commit()
197202
_invalidate_bangumi_cache()
198-
logger.debug(f"[Database] Insert {data.official_title} into database.")
203+
logger.debug("[Database] Insert %s into database.", data.official_title)
199204
return True
200205

201206
def add_all(self, datas: list[Bangumi]) -> int:
@@ -250,12 +255,14 @@ def add_all(self, datas: list[Bangumi]) -> int:
250255
if not unique_to_add:
251256
if semantic_merged > 0:
252257
logger.debug(
253-
f"[Database] {semantic_merged} bangumi merged as aliases, "
254-
f"rest were duplicates."
258+
"[Database] %s bangumi merged as aliases, "
259+
"rest were duplicates.",
260+
semantic_merged,
255261
)
256262
else:
257263
logger.debug(
258-
f"[Database] All {len(datas)} bangumi already exist, skipping."
264+
"[Database] All %s bangumi already exist, skipping.",
265+
len(datas),
259266
)
260267
return 0
261268

@@ -265,12 +272,16 @@ def add_all(self, datas: list[Bangumi]) -> int:
265272
skipped = len(datas) - len(unique_to_add) - semantic_merged
266273
if skipped > 0 or semantic_merged > 0:
267274
logger.debug(
268-
f"[Database] Insert {len(unique_to_add)} bangumi, "
269-
f"skipped {skipped} duplicates, merged {semantic_merged} as aliases."
275+
"[Database] Insert %s bangumi, "
276+
"skipped %s duplicates, merged %s as aliases.",
277+
len(unique_to_add),
278+
skipped,
279+
semantic_merged,
270280
)
271281
else:
272282
logger.debug(
273-
f"[Database] Insert {len(unique_to_add)} bangumi into database."
283+
"[Database] Insert %s bangumi into database.",
284+
len(unique_to_add),
274285
)
275286
return len(unique_to_add)
276287

@@ -289,14 +300,14 @@ def update(self, data: Bangumi | BangumiUpdate, _id: int = None) -> bool:
289300
self.session.add(db_data)
290301
self.session.commit()
291302
_invalidate_bangumi_cache()
292-
logger.debug(f"[Database] Update {data.official_title}")
303+
logger.debug("[Database] Update %s", data.official_title)
293304
return True
294305

295306
def update_all(self, datas: list[Bangumi]):
296307
self.session.add_all(datas)
297308
self.session.commit()
298309
_invalidate_bangumi_cache()
299-
logger.debug(f"[Database] Update {len(datas)} bangumi.")
310+
logger.debug("[Database] Update %s bangumi.", len(datas))
300311

301312
def update_rss(self, title_raw: str, rss_set: str):
302313
statement = select(Bangumi).where(Bangumi.title_raw == title_raw)
@@ -308,7 +319,7 @@ def update_rss(self, title_raw: str, rss_set: str):
308319
self.session.add(bangumi)
309320
self.session.commit()
310321
_invalidate_bangumi_cache()
311-
logger.debug(f"[Database] Update {title_raw} rss_link to {rss_set}.")
322+
logger.debug("[Database] Update %s rss_link to %s.", title_raw, rss_set)
312323

313324
def update_poster(self, title_raw: str, poster_link: str):
314325
statement = select(Bangumi).where(Bangumi.title_raw == title_raw)
@@ -319,7 +330,7 @@ def update_poster(self, title_raw: str, poster_link: str):
319330
self.session.add(bangumi)
320331
self.session.commit()
321332
_invalidate_bangumi_cache()
322-
logger.debug(f"[Database] Update {title_raw} poster_link to {poster_link}.")
333+
logger.debug("[Database] Update %s poster_link to %s.", title_raw, poster_link)
323334

324335
def delete_one(self, _id: int):
325336
statement = select(Bangumi).where(Bangumi.id == _id)
@@ -329,7 +340,7 @@ def delete_one(self, _id: int):
329340
self.session.delete(bangumi)
330341
self.session.commit()
331342
_invalidate_bangumi_cache()
332-
logger.debug(f"[Database] Delete bangumi id: {_id}.")
343+
logger.debug("[Database] Delete bangumi id: %s.", _id)
333344

334345
def delete_all(self):
335346
statement = delete(Bangumi)
@@ -362,7 +373,7 @@ def search_id(self, _id: int) -> Optional[Bangumi]:
362373
if bangumi is None:
363374
logger.warning(f"[Database] Cannot find bangumi id: {_id}.")
364375
return None
365-
logger.debug(f"[Database] Find bangumi id: {_id}.")
376+
logger.debug("[Database] Find bangumi id: %s.", _id)
366377
return bangumi
367378

368379
def search_ids(self, ids: list[int]) -> list[Bangumi]:
@@ -426,7 +437,8 @@ def match_list(self, torrent_list: list, rss_link: str) -> list:
426437
self.session.commit()
427438
_invalidate_bangumi_cache()
428439
logger.debug(
429-
f"[Database] Batch updated rss_link for {len(rss_updated)} bangumi."
440+
"[Database] Batch updated rss_link for %s bangumi.",
441+
len(rss_updated),
430442
)
431443
return unmatched
432444

@@ -485,7 +497,7 @@ def disable_rule(self, _id: int):
485497
self.session.add(bangumi)
486498
self.session.commit()
487499
_invalidate_bangumi_cache()
488-
logger.debug(f"[Database] Disable rule {bangumi.title_raw}.")
500+
logger.debug("[Database] Disable rule %s.", bangumi.title_raw)
489501

490502
def search_rss(self, rss_link: str) -> list[Bangumi]:
491503
statement = select(Bangumi).where(func.instr(rss_link, Bangumi.rss_link) > 0)
@@ -502,7 +514,7 @@ def archive_one(self, _id: int) -> bool:
502514
self.session.add(bangumi)
503515
self.session.commit()
504516
_invalidate_bangumi_cache()
505-
logger.debug(f"[Database] Archived bangumi id: {_id}.")
517+
logger.debug("[Database] Archived bangumi id: %s.", _id)
506518
return True
507519

508520
def unarchive_one(self, _id: int) -> bool:
@@ -515,7 +527,7 @@ def unarchive_one(self, _id: int) -> bool:
515527
self.session.add(bangumi)
516528
self.session.commit()
517529
_invalidate_bangumi_cache()
518-
logger.debug(f"[Database] Unarchived bangumi id: {_id}.")
530+
logger.debug("[Database] Unarchived bangumi id: %s.", _id)
519531
return True
520532

521533
def match_by_save_path(self, save_path: str) -> Optional[Bangumi]:
@@ -615,8 +627,12 @@ def set_needs_review(
615627
self.session.commit()
616628
_invalidate_bangumi_cache()
617629
logger.debug(
618-
f"[Database] Marked bangumi id {_id} as needs_review: {reason} "
619-
f"(suggested: season={suggested_season_offset}, episode={suggested_episode_offset})"
630+
"[Database] Marked bangumi id %s as needs_review: %s "
631+
"(suggested: season=%s, episode=%s)",
632+
_id,
633+
reason,
634+
suggested_season_offset,
635+
suggested_episode_offset,
620636
)
621637
return True
622638

@@ -632,5 +648,5 @@ def clear_needs_review(self, _id: int) -> bool:
632648
self.session.add(bangumi)
633649
self.session.commit()
634650
_invalidate_bangumi_cache()
635-
logger.debug(f"[Database] Cleared needs_review for bangumi id {_id}")
651+
logger.debug("[Database] Cleared needs_review for bangumi id %s", _id)
636652
return True

backend/src/module/database/rss.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ def add(self, data: RSSItem) -> bool:
1616
result = self.session.execute(statement)
1717
db_data = result.scalar_one_or_none()
1818
if db_data:
19-
logger.debug(f"RSS Item {data.url} already exists.")
19+
logger.debug("RSS Item %s already exists.", data.url)
2020
return False
2121
else:
22-
logger.debug(f"RSS Item {data.url} not exists, adding...")
22+
logger.debug("RSS Item %s not exists, adding...", data.url)
2323
self.session.add(data)
2424
self.session.commit()
2525
self.session.refresh(data)
@@ -36,7 +36,7 @@ def add_all(self, data: list[RSSItem]):
3636
if new_items:
3737
self.session.add_all(new_items)
3838
self.session.commit()
39-
logger.debug(f"Batch inserted {len(new_items)} RSS items.")
39+
logger.debug("Batch inserted %s RSS items.", len(new_items))
4040

4141
def update(self, _id: int, data: RSSUpdate) -> bool:
4242
statement = select(RSSItem).where(RSSItem.id == _id)

backend/src/module/database/torrent.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ def __init__(self, session: Session):
1414
def add(self, data: Torrent):
1515
self.session.add(data)
1616
self.session.commit()
17-
logger.debug(f"Insert {data.name} in database.")
17+
logger.debug("Insert %s in database.", data.name)
1818

1919
def add_all(self, datas: list[Torrent]):
2020
self.session.add_all(datas)
2121
self.session.commit()
22-
logger.debug(f"Insert {len(datas)} torrents in database.")
22+
logger.debug("Insert %s torrents in database.", len(datas))
2323

2424
def update(self, data: Torrent):
2525
self.session.add(data)
2626
self.session.commit()
27-
logger.debug(f"Update {data.name} in database.")
27+
logger.debug("Update %s in database.", data.name)
2828

2929
def update_all(self, datas: list[Torrent]):
3030
self.session.add_all(datas)
@@ -33,7 +33,7 @@ def update_all(self, datas: list[Torrent]):
3333
def update_one_user(self, data: Torrent):
3434
self.session.add(data)
3535
self.session.commit()
36-
logger.debug(f"Update {data.name} in database.")
36+
logger.debug("Update %s in database.", data.name)
3737

3838
def search(self, _id: int) -> Torrent | None:
3939
result = self.session.execute(select(Torrent).where(Torrent.id == _id))
@@ -82,6 +82,6 @@ def update_qb_hash(self, torrent_id: int, qb_hash: str) -> bool:
8282
torrent.qb_hash = qb_hash
8383
self.session.add(torrent)
8484
self.session.commit()
85-
logger.debug(f"Updated qb_hash for torrent {torrent_id}: {qb_hash}")
85+
logger.debug("Updated qb_hash for torrent %s: %s", torrent_id, qb_hash)
8686
return True
8787
return False

0 commit comments

Comments
 (0)