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,8 +1060,23 @@ 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)
|
||||
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'),
|
||||
|
||||
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'), [])
|
||||
33
ops/applogs/archive-app-log.sh
Executable file
33
ops/applogs/archive-app-log.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# روی ۲۴۹، هر شب (zareian-applog-archive.timer). app.log را به logs/archive/ میبرد و فشرده میکند.
|
||||
# برنامه با LOG_FILE_ROTATION=external از WatchedFileHandler استفاده میکند و پس از جابهجایی خودش app.log تازه
|
||||
# میسازد؛ خطی که در همان لحظه نوشته شود در فایلِ جابهجاشده میماند، گم نمیشود.
|
||||
# اینجا فقط ۳۰ روز میماند (دیسکِ ۲۴۹ پر است) و فقط فایلی پاک میشود که ۲۴۷ گرفتنش را تأیید کرده
|
||||
# (`.shipped`)؛ نسخهی همیشگی روی ۲۴۷ است (pull-app-logs.sh). فقط زیرِ ~/mathapp-v2/logs مینویسد.
|
||||
set -euo pipefail
|
||||
LOGS=/home/administrator/mathapp-v2/logs
|
||||
ARCH="$LOGS/archive"
|
||||
mkdir -p "$ARCH"
|
||||
chmod 750 "$ARCH"
|
||||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
|
||||
# app.log.1..5 از RotatingFileHandlerِ پیشین (پیش از LOG_FILE_ROTATION=external) هم یک بار بایگانی میشوند.
|
||||
for f in "$LOGS"/app.log.[0-9]*; do
|
||||
[ -e "$f" ] || continue
|
||||
n=${f##*.}
|
||||
mv "$f" "$ARCH/app-$stamp-old$n.log"
|
||||
done
|
||||
if [ -s "$LOGS/app.log" ]; then
|
||||
mv "$LOGS/app.log" "$ARCH/app-$stamp.log"
|
||||
sleep 5 # نوشتنِ در جریانِ workerها روی فایلِ قدیمی تمام شود
|
||||
fi
|
||||
for f in "$ARCH"/app-*.log; do
|
||||
[ -e "$f" ] || continue
|
||||
gzip -9 "$f"
|
||||
chmod 640 "$f.gz"
|
||||
done
|
||||
|
||||
find "$ARCH" -name 'app-*.log.gz' -mtime +30 | while read -r gz; do
|
||||
[ -e "$gz.shipped" ] && rm -f -- "$gz" "$gz.shipped"
|
||||
done
|
||||
echo "archived $(ls "$ARCH"/app-*.log.gz 2>/dev/null | wc -l) files in $ARCH"
|
||||
26
ops/applogs/pull-app-logs.sh
Executable file
26
ops/applogs/pull-app-logs.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# روی ۲۴۷، هر ساعت (zareian-applog-pull.timer). بایگانیِ فشردهی لاگِ برنامه را از ۲۴۹ میگیرد و
|
||||
# **برای همیشه** در ~/zareian-pgbackup/app-logs/ نگه میدارد (تصمیمِ مالک ۲۰۲۶-۱۰-۱۳؛ برآورد حدودِ ۱ گیگ در سال
|
||||
# با ۶۰۰ کاربرِ فعال). هیچ فایلی اینجا پاک نمیشود. کلید فقط serve-archive.sh را اجرا میکند.
|
||||
set -euo pipefail
|
||||
DEST=/home/administrator/zareian-pgbackup/app-logs
|
||||
mkdir -p "$DEST"
|
||||
r() { ssh -i /home/administrator/.ssh/zareian_applogs -o BatchMode=yes -o ConnectTimeout=20 \
|
||||
administrator@192.168.200.164 "$@"; }
|
||||
|
||||
free_kb=$(df --output=avail -k "$DEST" | tail -1)
|
||||
[ "$free_kb" -gt 3000000 ] || { echo "disk too full ($free_kb KB free); skipping" >&2; exit 1; }
|
||||
|
||||
got=0
|
||||
for name in $(r list); do
|
||||
[[ "$name" =~ ^app-[0-9]{8}T[0-9]{6}Z(-old[0-9]+)?\.log\.gz$ ]] || { echo "skip odd name: $name" >&2; continue; }
|
||||
if [ ! -s "$DEST/$name" ]; then
|
||||
r get "$name" > "$DEST/.$name.part"
|
||||
gzip -t "$DEST/.$name.part"
|
||||
chmod 600 "$DEST/.$name.part"
|
||||
mv "$DEST/.$name.part" "$DEST/$name"
|
||||
got=$((got + 1))
|
||||
fi
|
||||
r ack "$name"
|
||||
done
|
||||
echo "fetched $got new; $(ls "$DEST" | wc -l) files, $(du -sh "$DEST" | cut -f1) in $DEST"
|
||||
17
ops/applogs/serve-archive.sh
Executable file
17
ops/applogs/serve-archive.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# روی ۲۴۹: فرمانِ اجباریِ کلیدِ zareian-applogs (authorized_keys با restrict,command="…این فایل").
|
||||
# آن کلید (روی ۲۴۷) جز این سه کار هیچ کاری نمیتواند: شِل، پورت و فایلِ دیگری ندارد.
|
||||
# list نامِ فایلهای بایگانی
|
||||
# get NAME محتوای یک فایل
|
||||
# ack NAME «۲۴۷ گرفت و سالم بود» → NAME.shipped (پس از ۳۰ روز اینجا پاک شود)
|
||||
set -euo pipefail
|
||||
ARCH=/home/administrator/mathapp-v2/logs/archive
|
||||
read -r cmd name extra <<<"${SSH_ORIGINAL_COMMAND:-}"
|
||||
[ -z "${extra:-}" ] || { echo "bad request" >&2; exit 2; }
|
||||
valid() { [[ "$1" =~ ^app-[0-9]{8}T[0-9]{6}Z(-old[0-9]+)?\.log\.gz$ ]] && [ -f "$ARCH/$1" ]; }
|
||||
case "$cmd" in
|
||||
list) cd "$ARCH" && ls -1 app-*.log.gz 2>/dev/null || true ;;
|
||||
get) valid "${name:-}" || { echo "no such file" >&2; exit 2; }; cat -- "$ARCH/$name" ;;
|
||||
ack) valid "${name:-}" || { echo "no such file" >&2; exit 2; }; touch -- "$ARCH/$name.shipped" ;;
|
||||
*) echo "bad request" >&2; exit 2 ;;
|
||||
esac
|
||||
8
ops/applogs/zareian-applog-archive.service
Normal file
8
ops/applogs/zareian-applog-archive.service
Normal file
@@ -0,0 +1,8 @@
|
||||
# روی ۲۴۹.
|
||||
[Unit]
|
||||
Description=Zareian MathApp: nightly gzip of app.log into logs/archive
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=administrator
|
||||
ExecStart=/home/administrator/mathapp-v2/ops/applogs/archive-app-log.sh
|
||||
10
ops/applogs/zareian-applog-archive.timer
Normal file
10
ops/applogs/zareian-applog-archive.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
# روی ۲۴۹. ساعتِ سرور تهران است.
|
||||
[Unit]
|
||||
Description=Zareian MathApp: nightly app.log archive
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 00:05:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
8
ops/applogs/zareian-applog-pull.service
Normal file
8
ops/applogs/zareian-applog-pull.service
Normal file
@@ -0,0 +1,8 @@
|
||||
# روی ۲۴۷.
|
||||
[Unit]
|
||||
Description=Zareian MathApp: fetch archived app logs from 249 (kept forever)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=administrator
|
||||
ExecStart=/home/administrator/zareian-pgbackup/bin/pull-app-logs.sh
|
||||
11
ops/applogs/zareian-applog-pull.timer
Normal file
11
ops/applogs/zareian-applog-pull.timer
Normal file
@@ -0,0 +1,11 @@
|
||||
# روی ۲۴۷.
|
||||
[Unit]
|
||||
Description=Zareian MathApp: hourly fetch of archived app logs
|
||||
|
||||
[Timer]
|
||||
OnCalendar=hourly
|
||||
RandomizedDelaySec=300
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user