feat(logging): add LOG_ADMIN_HIDE setting and update logging functionality
Introduce a new configuration option `LOG_ADMIN_HIDE` to control the visibility of admin-generated events in the logs. Update the logging retrieval functions to respect this setting, ensuring that admin actions can be hidden from the "All message logs" UI and CSV exports. Additionally, enhance the README with detailed logging configuration options for better clarity.
This commit is contained in:
@@ -165,6 +165,7 @@ LOG_LEVEL=INFO #
|
|||||||
LOG_STORE_MESSAGE_CONTENT=False # Store message/callback content in DB logs
|
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_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_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
|
# Admin Logging Configuration
|
||||||
LOG_CHAT_ID=-1001234567890 # Telegram chat/group ID for admin notifications
|
LOG_CHAT_ID=-1001234567890 # Telegram chat/group ID for admin notifications
|
||||||
|
|||||||
@@ -118,6 +118,18 @@
|
|||||||
| `SEVERPAY_LIFETIME_MINUTES` | (Опционально) Время жизни платежной ссылки в минутах (30–4320). |
|
| `SEVERPAY_LIFETIME_MINUTES` | (Опционально) Время жизни платежной ссылки в минутах (30–4320). |
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Настройки логирования</b></summary>
|
||||||
|
|
||||||
|
| Переменная | Описание | Пример |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `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` |
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Настройки подписок</b></summary>
|
<summary><b>Настройки подписок</b></summary>
|
||||||
|
|
||||||
|
|||||||
@@ -171,9 +171,17 @@ async def view_all_logs_handler(callback: types.CallbackQuery,
|
|||||||
await callback.answer("Error processing request.", show_alert=True)
|
await callback.answer("Error processing request.", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
hide_admin_events = bool(settings.LOG_ADMIN_HIDE)
|
||||||
logs_models = await message_log_dal.get_all_message_logs(
|
logs_models = await message_log_dal.get_all_message_logs(
|
||||||
session, settings.LOGS_PAGE_SIZE, page_idx * settings.LOGS_PAGE_SIZE)
|
session,
|
||||||
total_logs_count = await message_log_dal.count_all_message_logs(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(
|
await _display_formatted_logs(
|
||||||
target_message=callback.message,
|
target_message=callback.message,
|
||||||
@@ -344,8 +352,12 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
|
|||||||
try:
|
try:
|
||||||
# Get all logs (limit to 10000 for performance)
|
# Get all logs (limit to 10000 for performance)
|
||||||
logs_models = await message_log_dal.get_all_message_logs(
|
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:
|
if not logs_models:
|
||||||
await callback.message.answer(_(
|
await callback.message.answer(_(
|
||||||
"admin_logs_csv_no_data"
|
"admin_logs_csv_no_data"
|
||||||
@@ -355,7 +367,7 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
|
|||||||
# Create CSV content
|
# Create CSV content
|
||||||
csv_buffer = io.StringIO()
|
csv_buffer = io.StringIO()
|
||||||
csv_writer = csv.writer(csv_buffer, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
csv_writer = csv.writer(csv_buffer, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
||||||
|
|
||||||
# Write header
|
# Write header
|
||||||
headers = [
|
headers = [
|
||||||
_("admin_csv_header_log_id"),
|
_("admin_csv_header_log_id"),
|
||||||
@@ -374,16 +386,16 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
|
|||||||
_("admin_csv_header_raw_update_preview"),
|
_("admin_csv_header_raw_update_preview"),
|
||||||
])
|
])
|
||||||
csv_writer.writerow(headers)
|
csv_writer.writerow(headers)
|
||||||
|
|
||||||
# Write data rows
|
# Write data rows
|
||||||
for log in logs_models:
|
for log in logs_models:
|
||||||
# Format timestamp
|
# Format timestamp
|
||||||
timestamp_str = log.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') if log.timestamp else ''
|
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)
|
# Clean content and raw_update_preview (remove newlines and quotes for CSV)
|
||||||
content_clean = (log.content or '').replace('\n', ' ').replace('\r', ' ').strip()
|
content_clean = (log.content or '').replace('\n', ' ').replace('\r', ' ').strip()
|
||||||
raw_update_clean = (log.raw_update_preview or '').replace('\n', ' ').replace('\r', ' ').strip()
|
raw_update_clean = (log.raw_update_preview or '').replace('\n', ' ').replace('\r', ' ').strip()
|
||||||
|
|
||||||
row = [
|
row = [
|
||||||
log.log_id or '',
|
log.log_id or '',
|
||||||
timestamp_str,
|
timestamp_str,
|
||||||
@@ -400,21 +412,21 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
|
|||||||
raw_update_clean,
|
raw_update_clean,
|
||||||
])
|
])
|
||||||
csv_writer.writerow(row)
|
csv_writer.writerow(row)
|
||||||
|
|
||||||
# Create file
|
# Create file
|
||||||
csv_content = csv_buffer.getvalue()
|
csv_content = csv_buffer.getvalue()
|
||||||
csv_buffer.close()
|
csv_buffer.close()
|
||||||
|
|
||||||
# Generate filename with current timestamp
|
# Generate filename with current timestamp
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
filename = f"message_logs_{now.strftime('%Y%m%d_%H%M%S')}.csv"
|
filename = f"message_logs_{now.strftime('%Y%m%d_%H%M%S')}.csv"
|
||||||
|
|
||||||
# Send as document
|
# Send as document
|
||||||
csv_file = types.BufferedInputFile(
|
csv_file = types.BufferedInputFile(
|
||||||
csv_content.encode('utf-8-sig'), # BOM for Excel compatibility
|
csv_content.encode('utf-8-sig'), # BOM for Excel compatibility
|
||||||
filename=filename
|
filename=filename
|
||||||
)
|
)
|
||||||
|
|
||||||
await callback.message.answer_document(
|
await callback.message.answer_document(
|
||||||
csv_file,
|
csv_file,
|
||||||
caption=_(
|
caption=_(
|
||||||
@@ -423,7 +435,7 @@ async def export_logs_csv_handler(callback: types.CallbackQuery,
|
|||||||
date=now.strftime('%Y-%m-%d %H:%M:%S')
|
date=now.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error exporting logs to CSV: {e}", exc_info=True)
|
logging.error(f"Error exporting logs to CSV: {e}", exc_info=True)
|
||||||
await callback.message.answer(_(
|
await callback.message.answer(_(
|
||||||
|
|||||||
@@ -645,8 +645,20 @@ async def yookassa_webhook_route(request: web.Request):
|
|||||||
yookassa_service,
|
yookassa_service,
|
||||||
lknpd_service)
|
lknpd_service)
|
||||||
if not processed:
|
if not processed:
|
||||||
await session.rollback()
|
# process_successful_payment uses False for permanent business failures
|
||||||
return web.Response(status=503, text="yookassa_processing_failed_retry")
|
# (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()
|
await session.commit()
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
|
|||||||
@@ -568,6 +568,11 @@ class Settings(BaseSettings):
|
|||||||
description="Include content/raw update fields in admin CSV export",
|
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')
|
@field_validator('LOG_LEVEL', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_log_level(cls, v):
|
def normalize_log_level(cls, v):
|
||||||
|
|||||||
@@ -22,16 +22,23 @@ async def create_message_log(session: AsyncSession,
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def get_all_message_logs(session: AsyncSession, limit: int,
|
async def get_all_message_logs(session: AsyncSession,
|
||||||
offset: int) -> List[MessageLog]:
|
limit: int,
|
||||||
stmt = select(MessageLog).order_by(
|
offset: int,
|
||||||
MessageLog.timestamp.desc()).limit(limit).offset(offset)
|
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)
|
result = await session.execute(stmt)
|
||||||
return result.scalars().all()
|
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)
|
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)
|
result = await session.execute(stmt)
|
||||||
return result.scalar_one()
|
return result.scalar_one()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user