v0.0.51: максимальное логирование (LOG_LEVEL env) + детальные логи SSE/воркера для диагностики обрыва
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
# 2026-08-20 — Максимальное логирование (v0.0.51)
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
SSE рвётся на проде (пару минут), обработка при этом продолжается (воркер жив).
|
||||||
|
Логов в поде не было — приложение не писало в stdout, нельзя было диагностировать
|
||||||
|
обрыв (внешний шлюз vs генератор).
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
1. `site/app.py` — `setup_logging()`:
|
||||||
|
- уровень из env `LOG_LEVEL` (DEBUG/INFO/WARNING), default INFO
|
||||||
|
- `logging.basicConfig(..., stream=sys.stderr, force=True)` — в stdout/stderr пода
|
||||||
|
- формат с таймстампом; уровни для логгеров drhider/app/routes/session
|
||||||
|
- `LOG_LEVEL` задаётся через env-переменные приложения на платформе
|
||||||
|
2. `site/routes/api_bp.py` — подробные логи:
|
||||||
|
- upload: каждый файл (имя, размер), итог, ошибки
|
||||||
|
- process_stream: start, worker start/done (время, токены, zip_len),
|
||||||
|
каждое событие progress (start/done) с idx/name/elapsed,
|
||||||
|
disconnect на heartbeat/progress/complete/error (с причиной),
|
||||||
|
result/complete, error event
|
||||||
|
3. VERSION поднята до 0.0.51
|
||||||
|
|
||||||
|
## Проверка (локально, DEBUG)
|
||||||
|
Upload 1 файла + SSE: видны все события от upload до complete.
|
||||||
|
`LLM NER failed: Illegal header value b'Bearer '` — ожидаемо без ключа (локально).
|
||||||
|
|
||||||
|
## Как читать логи пода после деплоя
|
||||||
|
```
|
||||||
|
kubectl logs -n 20a75175-a58c-49cb-b8fa-e86367b1a8dc <pod> --tail=500 --timestamps
|
||||||
|
```
|
||||||
|
- Если `process_stream: disconnect on progress` — клиент/шлюз оборвал.
|
||||||
|
- Если worker дошёл до `complete`, а клиент не получил — рвёт шлюз/браузер.
|
||||||
|
- Если `worker: exception` — ошибка обработки.
|
||||||
+23
-1
@@ -12,6 +12,7 @@ DrHider — Managed Flask приложение на платформе Штур
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
# Добавляем корень проекта в sys.path для импорта пакета drhider
|
# Добавляем корень проекта в sys.path для импорта пакета drhider
|
||||||
@@ -20,7 +21,27 @@ if _sys_path_root not in sys.path:
|
|||||||
sys.path.insert(0, _sys_path_root)
|
sys.path.insert(0, _sys_path_root)
|
||||||
|
|
||||||
# Версия приложения (меняется при изменениях)
|
# Версия приложения (меняется при изменениях)
|
||||||
VERSION = "0.0.50"
|
VERSION = "0.0.51"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Настроить логирование. Уровень берётся из env LOG_LEVEL (DEBUG/INFO/WARNING)."""
|
||||||
|
level_name = os.environ.get("LOG_LEVEL", "INFO").upper()
|
||||||
|
level = getattr(logging, level_name, logging.INFO)
|
||||||
|
logging.basicConfig(
|
||||||
|
level=level,
|
||||||
|
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
stream=sys.stderr,
|
||||||
|
force=True,
|
||||||
|
)
|
||||||
|
logging.getLogger("drhider").setLevel(level)
|
||||||
|
logging.getLogger("app").setLevel(level)
|
||||||
|
logging.getLogger("routes").setLevel(level)
|
||||||
|
logging.getLogger("session").setLevel(level)
|
||||||
|
logging.getLogger(__name__).setLevel(level)
|
||||||
|
log = logging.getLogger("app")
|
||||||
|
log.info("Logging configured, level=%s, version=%s", level_name, VERSION)
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
@@ -34,6 +55,7 @@ def create_app():
|
|||||||
Returns:
|
Returns:
|
||||||
Экземпляр Flask с зарегистрированными blueprint'ами.
|
Экземпляр Flask с зарегистрированными blueprint'ами.
|
||||||
"""
|
"""
|
||||||
|
setup_logging()
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Конфигурация
|
# Конфигурация
|
||||||
|
|||||||
+31
-5
@@ -15,6 +15,7 @@ import queue
|
|||||||
import threading
|
import threading
|
||||||
import zipfile
|
import zipfile
|
||||||
import traceback
|
import traceback
|
||||||
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask import Blueprint, request, send_file, jsonify, Response, stream_with_context
|
from flask import Blueprint, request, send_file, jsonify, Response, stream_with_context
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ from session import (create_session, add_file, get_files, store_result,
|
|||||||
get_result, store_csv, get_csv, cleanup, file_count)
|
get_result, store_csv, get_csv, cleanup, file_count)
|
||||||
|
|
||||||
api_bp = Blueprint("api", __name__, url_prefix="/api")
|
api_bp = Blueprint("api", __name__, url_prefix="/api")
|
||||||
|
log = logging.getLogger("routes.api_bp")
|
||||||
|
|
||||||
|
|
||||||
def _disconnect_exceptions():
|
def _disconnect_exceptions():
|
||||||
@@ -38,6 +40,7 @@ def upload():
|
|||||||
sid = create_session()
|
sid = create_session()
|
||||||
uploaded = request.files.getlist("files")
|
uploaded = request.files.getlist("files")
|
||||||
if not uploaded:
|
if not uploaded:
|
||||||
|
log.warning("upload: no files, sid=%s", sid)
|
||||||
return jsonify({"ok": False, "error": "No file"}), 400
|
return jsonify({"ok": False, "error": "No file"}), 400
|
||||||
|
|
||||||
added = 0
|
added = 0
|
||||||
@@ -46,14 +49,19 @@ def upload():
|
|||||||
if not f.filename:
|
if not f.filename:
|
||||||
had_unnamed = True
|
had_unnamed = True
|
||||||
continue
|
continue
|
||||||
if not add_file(sid, f.filename, f.read()):
|
data = f.read()
|
||||||
|
log.info("upload: sid=%s file=%r size=%d", sid, f.filename, len(data))
|
||||||
|
if not add_file(sid, f.filename, data):
|
||||||
|
log.warning("upload: session not found/limit, sid=%s file=%r", sid, f.filename)
|
||||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||||
added += 1
|
added += 1
|
||||||
|
|
||||||
if added == 0:
|
if added == 0:
|
||||||
err = "No filename" if had_unnamed else "No file"
|
err = "No filename" if had_unnamed else "No file"
|
||||||
|
log.warning("upload: %s, sid=%s", err, sid)
|
||||||
return jsonify({"ok": False, "error": err}), 400
|
return jsonify({"ok": False, "error": err}), 400
|
||||||
|
|
||||||
|
log.info("upload: done sid=%s added=%d total=%d", sid, added, file_count(sid))
|
||||||
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
|
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
|
||||||
|
|
||||||
|
|
||||||
@@ -73,6 +81,7 @@ def process_stream(sid):
|
|||||||
return jsonify({"ok": False, "error": "No files"}), 400
|
return jsonify({"ok": False, "error": "No files"}), 400
|
||||||
|
|
||||||
all_files = [(fname, content, "") for fname, content in files]
|
all_files = [(fname, content, "") for fname, content in files]
|
||||||
|
log.info("process_stream: start sid=%s files=%d", sid, len(all_files))
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
llm = LLMClient()
|
llm = LLMClient()
|
||||||
@@ -83,6 +92,8 @@ def process_stream(sid):
|
|||||||
q.put(("progress", phase, idx, name, total_, elapsed))
|
q.put(("progress", phase, idx, name, total_, elapsed))
|
||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
|
log.info("worker: start sid=%s files=%d", sid, len(all_files))
|
||||||
|
t0 = datetime.utcnow()
|
||||||
try:
|
try:
|
||||||
zip_data, csv_str = obfuscate_files(
|
zip_data, csv_str = obfuscate_files(
|
||||||
all_files, llm_client=llm, progress_cb=progress
|
all_files, llm_client=llm, progress_cb=progress
|
||||||
@@ -91,17 +102,23 @@ def process_stream(sid):
|
|||||||
"tokens": llm.tokens_total,
|
"tokens": llm.tokens_total,
|
||||||
"llm_sec": round(llm.llm_sec, 1),
|
"llm_sec": round(llm.llm_sec, 1),
|
||||||
}
|
}
|
||||||
|
dt = (datetime.utcnow() - t0).total_seconds()
|
||||||
|
log.info("worker: done sid=%s in %.1fs tokens=%d llm_sec=%.1f zip_len=%d",
|
||||||
|
sid, dt, llm.tokens_total, llm.llm_sec, len(zip_data))
|
||||||
q.put(("result", zip_data, csv_str, stats))
|
q.put(("result", zip_data, csv_str, stats))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
log.error("worker: exception sid=%s: %r\n%s", sid, e, traceback.format_exc())
|
||||||
q.put(("error", repr(e)))
|
q.put(("error", repr(e)))
|
||||||
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
log.debug("process_stream: worker thread started sid=%s", sid)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
evt = q.get(timeout=1)
|
evt = q.get(timeout=1)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
if cancel.is_set():
|
if cancel.is_set():
|
||||||
|
log.info("process_stream: cancelled sid=%s (generator exit)", sid)
|
||||||
return
|
return
|
||||||
# Heartbeat: живая статистика LLM (для таймера в UI)
|
# Heartbeat: живая статистика LLM (для таймера в UI)
|
||||||
try:
|
try:
|
||||||
@@ -109,7 +126,8 @@ def process_stream(sid):
|
|||||||
f"event: llm\n"
|
f"event: llm\n"
|
||||||
f"data: {json.dumps({'active': llm.llm_active, 'elapsed': round(llm.llm_elapsed_now(), 1), 'tokens': llm.tokens_total})}\n\n"
|
f"data: {json.dumps({'active': llm.llm_active, 'elapsed': round(llm.llm_elapsed_now(), 1), 'tokens': llm.tokens_total})}\n\n"
|
||||||
)
|
)
|
||||||
except _disconnect_exceptions():
|
except _disconnect_exceptions() as e:
|
||||||
|
log.warning("process_stream: disconnect during heartbeat sid=%s err=%r", sid, e)
|
||||||
cancel.set()
|
cancel.set()
|
||||||
return
|
return
|
||||||
continue
|
continue
|
||||||
@@ -118,37 +136,45 @@ def process_stream(sid):
|
|||||||
|
|
||||||
if kind == "progress":
|
if kind == "progress":
|
||||||
_, phase, idx, name, total_, elapsed = evt
|
_, phase, idx, name, total_, elapsed = evt
|
||||||
|
log.debug("process_stream: event=%s idx=%d name=%r elapsed=%s sid=%s",
|
||||||
|
phase, idx, name, elapsed, sid)
|
||||||
try:
|
try:
|
||||||
yield (
|
yield (
|
||||||
f"event: {phase}\n"
|
f"event: {phase}\n"
|
||||||
f"data: {json.dumps({'idx': idx, 'name': name, 'total': total_, 'elapsed': elapsed})}\n\n"
|
f"data: {json.dumps({'idx': idx, 'name': name, 'total': total_, 'elapsed': elapsed})}\n\n"
|
||||||
)
|
)
|
||||||
except _disconnect_exceptions():
|
except _disconnect_exceptions() as e:
|
||||||
|
log.warning("process_stream: disconnect on progress sid=%s phase=%s err=%r", sid, phase, e)
|
||||||
cancel.set()
|
cancel.set()
|
||||||
return
|
return
|
||||||
|
|
||||||
elif kind == "result":
|
elif kind == "result":
|
||||||
_, zip_data, csv_str, stats = evt
|
_, zip_data, csv_str, stats = evt
|
||||||
|
log.info("process_stream: result sid=%s, storing result", sid)
|
||||||
store_result(sid, zip_data)
|
store_result(sid, zip_data)
|
||||||
if csv_str:
|
if csv_str:
|
||||||
store_csv(sid, csv_str)
|
store_csv(sid, csv_str)
|
||||||
count = 0
|
count = 0
|
||||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||||
count = len([n for n in zf.namelist() if n != "mapping.csv"])
|
count = len([n for n in zf.namelist() if n != "mapping.csv"])
|
||||||
|
log.info("process_stream: complete sid=%s count=%d stats=%r", sid, count, stats)
|
||||||
try:
|
try:
|
||||||
yield (
|
yield (
|
||||||
f"event: complete\n"
|
f"event: complete\n"
|
||||||
f"data: {json.dumps({'total': count, **stats})}\n\n"
|
f"data: {json.dumps({'total': count, **stats})}\n\n"
|
||||||
)
|
)
|
||||||
except _disconnect_exceptions():
|
except _disconnect_exceptions() as e:
|
||||||
|
log.warning("process_stream: disconnect on complete sid=%s err=%r", sid, e)
|
||||||
return
|
return
|
||||||
return
|
return
|
||||||
|
|
||||||
elif kind == "error":
|
elif kind == "error":
|
||||||
_, msg = evt
|
_, msg = evt
|
||||||
|
log.error("process_stream: error event sid=%s msg=%r", sid, msg)
|
||||||
try:
|
try:
|
||||||
yield f"event: error\ndata: {json.dumps({'error': msg})}\n\n"
|
yield f"event: error\ndata: {json.dumps({'error': msg})}\n\n"
|
||||||
except _disconnect_exceptions():
|
except _disconnect_exceptions() as e:
|
||||||
|
log.warning("process_stream: disconnect on error sid=%s err=%r", sid, e)
|
||||||
return
|
return
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user