App logs kept forever: nightly gzip on 249 (LOG_FILE_ROTATION=external), hourly pull to 247 via a forced-command key, health warns if the archive stops
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -275,6 +275,24 @@ def _offsite_warnings(offsite: dict) -> list[str]:
|
||||
return [msg] if msg else []
|
||||
|
||||
|
||||
# بیش از این یعنی آرشیوِ شبانه (`ops/applogs/archive-app-log.sh`) چند روز است اجرا نشده؛
|
||||
# با ترافیکِ ۶۰۰ کاربرِ فعال یک روز حدودِ ۳۰ مگ است (برآورد، نه سنجش).
|
||||
APP_LOG_MAX_BYTES = 256 * 2**20
|
||||
|
||||
|
||||
def _app_log_warnings() -> list[str]:
|
||||
"""با چرخشِ بیرونی، برنامه app.log را پاک نمیکند؛ اگر آرشیوِ شبانه بخوابد فایل بیانتها بزرگ میشود."""
|
||||
if not settings.LOG_DIR or settings.LOG_FILE_ROTATION != 'external':
|
||||
return []
|
||||
try:
|
||||
size = os.path.getsize(os.path.join(settings.LOG_DIR, 'app.log'))
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
if size > APP_LOG_MAX_BYTES:
|
||||
return [f'app.log {size // 2**20} مگ است: آرشیوِ شبانهی لاگ (zareian-applog-archive.timer) اجرا نشده.']
|
||||
return []
|
||||
|
||||
|
||||
def _warnings() -> list[str]:
|
||||
"""
|
||||
چیزهایی که خرابی نیستند ولی روی یک سرورِ عمومی خطرناکاند.
|
||||
@@ -313,6 +331,7 @@ def _warnings() -> list[str]:
|
||||
'اعتبارِ سرویسِ هوش مصنوعی (GapGPT) تمام شده: دستیار برای همهی کاربرها '
|
||||
'«در دسترس نیست» میدهد تا حساب شارژ شود.'
|
||||
)
|
||||
out.extend(_app_log_warnings())
|
||||
backend = settings.CACHES['default']['BACKEND']
|
||||
if not settings.DEBUG and 'LocMem' in backend:
|
||||
out.append('کش محلیِ هر پروسه است؛ محدودیتِ نرخ بینِ workerها مشترک نیست.')
|
||||
|
||||
@@ -1060,13 +1060,28 @@ OFFSITE_WAL_SLOT = os.environ.get('OFFSITE_WAL_SLOT', '')
|
||||
# بیش از این عقبماندن هشدار است. ۶۴ مگ ≈ چهار فایلِ WAL؛ در ترافیکِ عادی عقبماندگی نزدیکِ صفر است.
|
||||
OFFSITE_WAL_MAX_LAG_BYTES = 64 * 2**20
|
||||
|
||||
# چرخشِ فایلِ لاگ: 'size' (پیشفرض، Docker و محلی) = ۶ فایلِ ۱۰ مگی و قدیمیها پاک میشوند؛
|
||||
# 'external' (سرور) = برنامه هرگز پاک نمیکند، `ops/applogs/archive-app-log.sh` هر شب app.log را جابهجا و
|
||||
# فشرده میکند و ۲۴۷ آن را برای همیشه نگه میدارد. WatchedFileHandler پس از جابهجایی فایلِ تازه باز میکند،
|
||||
# که با ۴ پروسهی gunicorn امنتر از چرخشِ داخلیِ RotatingFileHandler است (آن هر پروسه جدا میچرخاند).
|
||||
LOG_FILE_ROTATION = os.environ.get('LOG_FILE_ROTATION', 'size')
|
||||
if LOG_FILE_ROTATION not in ('size', 'external'):
|
||||
raise ImproperlyConfigured(f"LOG_FILE_ROTATION باید 'size' یا 'external' باشد، نه {LOG_FILE_ROTATION!r}")
|
||||
|
||||
if LOG_DIR:
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
LOGGING['handlers']['file'] = {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': os.path.join(LOG_DIR, 'app.log'),
|
||||
'maxBytes': 10 * 1024 * 1024, # 10MB در هر فایل، قبل از rotate
|
||||
'backupCount': 5,
|
||||
'formatter': 'json',
|
||||
}
|
||||
if LOG_FILE_ROTATION == 'external':
|
||||
LOGGING['handlers']['file'] = {
|
||||
'class': 'logging.handlers.WatchedFileHandler',
|
||||
'filename': os.path.join(LOG_DIR, 'app.log'),
|
||||
'formatter': 'json',
|
||||
}
|
||||
else:
|
||||
LOGGING['handlers']['file'] = {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': os.path.join(LOG_DIR, 'app.log'),
|
||||
'maxBytes': 10 * 1024 * 1024, # 10MB در هر فایل، قبل از rotate
|
||||
'backupCount': 5,
|
||||
'formatter': 'json',
|
||||
}
|
||||
LOGGING['root']['handlers'].append('file')
|
||||
|
||||
26
config/tests_app_log_archive.py
Normal file
26
config/tests_app_log_archive.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.test import SimpleTestCase, override_settings
|
||||
|
||||
from config import health
|
||||
|
||||
|
||||
class AppLogSizeWarningTests(SimpleTestCase):
|
||||
"""با چرخشِ بیرونی برنامه app.log را پاک نمیکند؛ اگر آرشیوِ شبانه بخوابد /api/health باید بگوید."""
|
||||
|
||||
def _with_log(self, size, rotation):
|
||||
d = tempfile.mkdtemp()
|
||||
with open(os.path.join(d, 'app.log'), 'wb') as f:
|
||||
f.truncate(size)
|
||||
with override_settings(LOG_DIR=d, LOG_FILE_ROTATION=rotation):
|
||||
return health._app_log_warnings()
|
||||
|
||||
def test_big_log_with_external_rotation_warns(self):
|
||||
self.assertEqual(len(self._with_log(health.APP_LOG_MAX_BYTES + 1, 'external')), 1)
|
||||
|
||||
def test_small_log_is_quiet(self):
|
||||
self.assertEqual(self._with_log(1024, 'external'), [])
|
||||
|
||||
def test_size_rotation_never_warns(self):
|
||||
self.assertEqual(self._with_log(health.APP_LOG_MAX_BYTES + 1, 'size'), [])
|
||||
Reference in New Issue
Block a user