feat: persist dev bot logs
This commit is contained in:
1 parent
a43ededa1b
commit
9f62b35798
4 files changed
+38
-3
No files matched your search
@@ -65,6 +65,8 @@ Telegram-бот, который по запросу пользователя г
|
||||
- Coolify проект: `Social Post Writer Bot`, окружение `development`.
|
||||
- Coolify приложение: `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) на `spb-easy-dale`.
|
||||
- SQLite: `/app/data/social_writer.sqlite3`; persistent volume `d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data` монтируется в `/app/data`.
|
||||
- Важно при поиске логов: приложение запущено на сервере `spb-easy-dale` (`31.129.103.242`), который может отличаться от хоста управления Coolify. Ориентируйся на поле Server у приложения; `docker ps` и `docker logs --since 24h <container>` выполняй на сервере приложения, имя контейнера начинается с `d32u5zcq2g4odmdpq9hw91yn-`. Не делай вывод об отсутствии контейнера по проверке только хоста Coolify.
|
||||
- После деплоя файл приложения `/app/data/bot.log` и его ротационные копии хранятся на persistent volume выше и переживают пересоздание контейнера. Пока logging-изменение не задеплоено, используй `docker logs` и API-журнал `api_call_logs` в SQLite. API-логи сохраняются в `/app/data/social_writer.sqlite3` на том же volume.
|
||||
- Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории.
|
||||
- `DEPLOY_ENV` включает dev-поведение только при точном значении `development`; production, пустое и неизвестное значение отключают его. `BOT_ENV` отдельно выбирает Telegram Bot API. `DEV_CHAT_ID` используется для уведомления о версии; версия в файле `VERSION` (сейчас `0.1.0+30`).
|
||||
- Вики Gitea: страницы `Home`, `User-Guide`, `Developer-Guide`, `Subscription`, `Deployment`.
|
||||
|
||||
@@ -195,7 +195,10 @@ SUBSCRIPTION_PRICE_STARS = 100
|
||||
|
||||
## Данные и хранилище
|
||||
|
||||
- SQLite: `/app/data/social_writer.sqlite3` (persistent Coolify volume).
|
||||
- **SQLite:** `/app/data/social_writer.sqlite3` (persistent Coolify volume).
|
||||
- **Постоянное хранилище dev:** volume `d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data` смонтирован в `/app/data` контейнера на сервере Coolify `spb-easy-dale` (`31.129.103.242`). Сервер приложения и сервер управления Coolify могут быть разными: сначала смотри назначенный приложению Server в записи Coolify, затем проверяй контейнер именно на нём. Не ищи контейнер только на хосте Coolify.
|
||||
- **Где смотреть ошибки:** подключись к `spb-easy-dale`, найди контейнер по имени `d32u5zcq2g4odmdpq9hw91yn-*` командой `docker ps`, затем смотри его stdout/stderr через `docker logs --since 24h <container>`. После деплоя ротационный файл приложения будет доступен как `/app/data/bot.log` (до 5 MiB и 3 архива); он сохраняется в указанном persistent volume при пересоздании контейнера. До деплоя изменения логирования ещё не действуют.
|
||||
- **API-журнал ошибки:** открой `/app/data/social_writer.sqlite3`, таблицу `api_call_logs`; записи связываются по `operation_id` и времени. Файл/БД находятся в volume на сервере приложения, не в локальном checkout и не на сервере Coolify, если это другой хост.
|
||||
- **Per-user данные:** профиль аккаунта/ToV, текстовые обучающие заметки, образцы постов со скриншотов, история сгенерированных постов и идеи. В «Обучении» текст/голос/скриншоты накапливают и обновляют профиль, а не запускают генерацию. История диалогов и исходные изображения **не сохраняются**.
|
||||
- **Пустые ответы APInex:** при пустом `content` бот делает до двух повторов; третий запрос добавляет `reasoning_effort: low`. Если ответ всё ещё пустой, операция завершается ошибкой и каждая попытка попадает в API-журнал. Ошибки HTTP автоматически не повторяются. APInex документирует `low`, но не режим отключения reasoning; поддержка параметра зависит от модели.
|
||||
- **Починка формата JSON:** при синтаксически невалидном JSON выполняется не более одной отдельной попытки модели `APINEX_RESPONSE_REPAIR_MODEL`; затем исходные проверки структуры/схемы запускаются заново. Ошибки API, пустые ответы и JSON с неверной схемой автоматически не исправляются. В Coolify development задана `APINEX_RESPONSE_REPAIR_MODEL`.
|
||||
|
||||
@@ -2,10 +2,12 @@ import asyncio
|
||||
import html
|
||||
import io
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from contextlib import asynccontextmanager, nullcontext
|
||||
from pathlib import Path
|
||||
|
||||
@@ -55,7 +57,23 @@ from user_store import (
|
||||
subscription_row,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
def _configure_logging() -> None:
|
||||
handlers: list[logging.Handler] = [logging.StreamHandler()]
|
||||
try:
|
||||
log_path = Path(os.getenv("BOT_LOG_PATH", "/app/data/bot.log"))
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
handlers.append(RotatingFileHandler(
|
||||
log_path,
|
||||
maxBytes=5 * 1024 * 1024,
|
||||
backupCount=3,
|
||||
encoding="utf-8",
|
||||
))
|
||||
except OSError as exc:
|
||||
print(f"File logging unavailable ({type(exc).__name__}); using container logs", file=sys.stderr)
|
||||
logging.basicConfig(level=logging.INFO, handlers=handlers)
|
||||
|
||||
|
||||
_configure_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
VERSION = Path(__file__).with_name("VERSION").read_text(encoding="utf-8").strip()
|
||||
BOT_ENV = os.getenv("BOT_ENV", "test").strip().lower()
|
||||
@@ -618,6 +636,11 @@ async def _check_subscription(message: Message) -> bool:
|
||||
|
||||
|
||||
async def _generate_post_from_training(message: Message) -> None:
|
||||
with diagnostic_operation():
|
||||
await _generate_post_from_training_inner(message)
|
||||
|
||||
|
||||
async def _generate_post_from_training_inner(message: Message) -> None:
|
||||
if message.from_user is None or not await _check_subscription(message):
|
||||
return
|
||||
user_id = message.from_user.id
|
||||
|
||||
@@ -491,9 +491,14 @@ class GenerationClassificationRoutingTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_generate_post_uses_training_material_without_intent_classifier(self):
|
||||
message = self.make_message()
|
||||
status = AsyncMock()
|
||||
operation_ids = []
|
||||
async def run_in_thread(function, *args):
|
||||
return function(*args)
|
||||
|
||||
async def generate_with_operation(*args, **kwargs):
|
||||
operation_ids.append(apinex_client.current_operation_id())
|
||||
return ["A", "B", "C"]
|
||||
|
||||
with (
|
||||
patch.object(bot, "_check_subscription", new=AsyncMock(return_value=True)),
|
||||
patch.object(bot, "is_user_trained", return_value=True),
|
||||
@@ -502,13 +507,15 @@ class GenerationClassificationRoutingTests(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(bot, "get_post_samples", return_value=["Открыли террасу"]),
|
||||
patch.object(bot, "get_training_notes", return_value=["Работаем в Калининграде"]),
|
||||
patch.object(bot.OperationStatus, "start", new=AsyncMock(return_value=status)),
|
||||
patch.object(bot, "generate_posts", new=AsyncMock(return_value=["A", "B", "C"])) as generate,
|
||||
patch.object(bot, "generate_posts", new=AsyncMock(side_effect=generate_with_operation)) as generate,
|
||||
patch.object(bot, "_send_three_posts", new=AsyncMock()) as send_posts,
|
||||
patch.object(bot, "classify_intent", new=AsyncMock()) as classify,
|
||||
):
|
||||
await bot._generate_post_from_training(cast(Any, message))
|
||||
|
||||
classify.assert_not_awaited()
|
||||
self.assertEqual(len(operation_ids), 1)
|
||||
self.assertTrue(operation_ids[0])
|
||||
self.assertIsNotNone(generate.await_args)
|
||||
args = generate.await_args.args
|
||||
self.assertEqual(args[0]["intent"], "write_post")
|
||||
|
||||
Reference in new issue
Block a user