diff --git a/.env.example b/.env.example index d7e6c5a..73de37c 100644 --- a/.env.example +++ b/.env.example @@ -165,6 +165,7 @@ LOG_LEVEL=INFO # LOG_STORE_MESSAGE_CONTENT=False # Store message/callback content in DB logs LOG_STORE_RAW_UPDATES=False # Store raw update payload snippets in DB logs LOG_EXPORT_INCLUDE_SENSITIVE=False # Include content/raw update columns in admin CSV export +LOG_ADMIN_HIDE=False # Hide admin actions from "All message logs" UI and CSV export # Admin Logging Configuration LOG_CHAT_ID=-1001234567890 # Telegram chat/group ID for admin notifications diff --git a/README.md b/README.md index 79cc3b6..2fc3dd7 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,18 @@ | `SEVERPAY_LIFETIME_MINUTES` | (Опционально) Время жизни платежной ссылки в минутах (30–4320). | +
+ Настройки логирования + + | Переменная | Описание | Пример | + | --- | --- | --- | + | `LOGS_PAGE_SIZE` | Количество записей на странице в разделе админ-логов. | `10` | + | `LOG_STORE_MESSAGE_CONTENT` | Сохранять ли содержимое сообщений/колбэков в БД логов (`true`/`false`). | `false` | + | `LOG_STORE_RAW_UPDATES` | Сохранять ли превью сырого Telegram update в БД логов (`true`/`false`). | `false` | + | `LOG_EXPORT_INCLUDE_SENSITIVE` | Добавлять ли в CSV экспорт чувствительные поля (`content`, `raw_update_preview`). | `false` | + | `LOG_ADMIN_HIDE` | Скрывать админские события (`ADMIN_IDS`) в интерфейсе «Все логи сообщений» и в CSV экспорте (`true`/`false`). Логи продолжают записываться в БД. | `true` | +
+
Настройки подписок diff --git a/bot/handlers/admin/logs_admin.py b/bot/handlers/admin/logs_admin.py index 6316d29..fca23d0 100644 --- a/bot/handlers/admin/logs_admin.py +++ b/bot/handlers/admin/logs_admin.py @@ -171,9 +171,17 @@ async def view_all_logs_handler(callback: types.CallbackQuery, await callback.answer("Error processing request.", show_alert=True) return + hide_admin_events = bool(settings.LOG_ADMIN_HIDE) logs_models = await message_log_dal.get_all_message_logs( - session, settings.LOGS_PAGE_SIZE, page_idx * settings.LOGS_PAGE_SIZE) - total_logs_count = await message_log_dal.count_all_message_logs(session) + session, + settings.LOGS_PAGE_SIZE, + page_idx * settings.LOGS_PAGE_SIZE, + hide_admin_events=hide_admin_events, + ) + total_logs_count = await message_log_dal.count_all_message_logs( + session, + hide_admin_events=hide_admin_events, + ) await _display_formatted_logs( target_message=callback.message, @@ -344,8 +352,12 @@ async def export_logs_csv_handler(callback: types.CallbackQuery, try: # Get all logs (limit to 10000 for performance) logs_models = await message_log_dal.get_all_message_logs( - session, limit=10000, offset=0) - + session, + limit=10000, + offset=0, + hide_admin_events=bool(settings.LOG_ADMIN_HIDE), + ) + if not logs_models: await callback.message.answer(_( "admin_logs_csv_no_data" @@ -355,7 +367,7 @@ async def export_logs_csv_handler(callback: types.CallbackQuery, # Create CSV content csv_buffer = io.StringIO() csv_writer = csv.writer(csv_buffer, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) - + # Write header headers = [ _("admin_csv_header_log_id"), @@ -374,16 +386,16 @@ async def export_logs_csv_handler(callback: types.CallbackQuery, _("admin_csv_header_raw_update_preview"), ]) csv_writer.writerow(headers) - + # Write data rows for log in logs_models: # Format timestamp timestamp_str = log.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') if log.timestamp else '' - + # Clean content and raw_update_preview (remove newlines and quotes for CSV) content_clean = (log.content or '').replace('\n', ' ').replace('\r', ' ').strip() raw_update_clean = (log.raw_update_preview or '').replace('\n', ' ').replace('\r', ' ').strip() - + row = [ log.log_id or '', timestamp_str, @@ -400,21 +412,21 @@ async def export_logs_csv_handler(callback: types.CallbackQuery, raw_update_clean, ]) csv_writer.writerow(row) - + # Create file csv_content = csv_buffer.getvalue() csv_buffer.close() - + # Generate filename with current timestamp now = datetime.now() filename = f"message_logs_{now.strftime('%Y%m%d_%H%M%S')}.csv" - + # Send as document csv_file = types.BufferedInputFile( csv_content.encode('utf-8-sig'), # BOM for Excel compatibility filename=filename ) - + await callback.message.answer_document( csv_file, caption=_( @@ -423,7 +435,7 @@ async def export_logs_csv_handler(callback: types.CallbackQuery, date=now.strftime('%Y-%m-%d %H:%M:%S') ) ) - + except Exception as e: logging.error(f"Error exporting logs to CSV: {e}", exc_info=True) await callback.message.answer(_( diff --git a/bot/handlers/user/payment.py b/bot/handlers/user/payment.py index 0dd571e..53ebdd7 100644 --- a/bot/handlers/user/payment.py +++ b/bot/handlers/user/payment.py @@ -645,8 +645,20 @@ async def yookassa_webhook_route(request: web.Request): yookassa_service, lknpd_service) if not processed: - await session.rollback() - return web.Response(status=503, text="yookassa_processing_failed_retry") + # process_successful_payment uses False for permanent business failures + # (e.g. user not found / metadata issues) and may have already updated + # the payment status. Commit the status and ACK the webhook to stop + # indefinite provider retries. + try: + await session.commit() + except Exception: + await session.rollback() + logging.exception( + "Failed to commit failure status for YooKassa payment %s", + payment_dict_for_processing.get("id"), + ) + return web.Response(status=503, text="yookassa_processing_failed_retry") + return web.Response(status=200, text="ok") await session.commit() else: logging.warning( diff --git a/config/settings.py b/config/settings.py index eea4f32..fb2087f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -568,6 +568,11 @@ class Settings(BaseSettings): description="Include content/raw update fields in admin CSV export", ) + LOG_ADMIN_HIDE: bool = Field( + default=False, + description="Hide admin-generated events from admin logs UI and CSV export", + ) + @field_validator('LOG_LEVEL', mode='before') @classmethod def normalize_log_level(cls, v): diff --git a/db/dal/message_log_dal.py b/db/dal/message_log_dal.py index 576a1a3..6a0e16b 100644 --- a/db/dal/message_log_dal.py +++ b/db/dal/message_log_dal.py @@ -22,16 +22,23 @@ async def create_message_log(session: AsyncSession, return None -async def get_all_message_logs(session: AsyncSession, limit: int, - offset: int) -> List[MessageLog]: - stmt = select(MessageLog).order_by( - MessageLog.timestamp.desc()).limit(limit).offset(offset) +async def get_all_message_logs(session: AsyncSession, + limit: int, + offset: int, + hide_admin_events: bool = False) -> List[MessageLog]: + stmt = select(MessageLog) + if hide_admin_events: + stmt = stmt.where(MessageLog.is_admin_event.is_(False)) + stmt = stmt.order_by(MessageLog.timestamp.desc()).limit(limit).offset(offset) result = await session.execute(stmt) return result.scalars().all() -async def count_all_message_logs(session: AsyncSession) -> int: +async def count_all_message_logs(session: AsyncSession, + hide_admin_events: bool = False) -> int: stmt = select(func.count()).select_from(MessageLog) + if hide_admin_events: + stmt = stmt.where(MessageLog.is_admin_event.is_(False)) result = await session.execute(stmt) return result.scalar_one()