feat: API request/response logging with admin log viewer
- Add api_logs table (migration 0007) and ApiLog model - Add web/lib/api_logger.py — httpx wrapper that records every outbound call - Wire api_logger into vk_sync, vk_catalog, and connections test endpoints - Add /admin/logs page with filters (service, method, status, time range, URL search) and expandable request/response detail - Add "Логи" nav link for admin users Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,9 @@ import os
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import httpx
|
||||
from celery import shared_task
|
||||
|
||||
import web.lib.api_logger as api_logger
|
||||
from web.config import settings
|
||||
from web.database import SessionLocal
|
||||
from web.models.connections import (
|
||||
@@ -63,14 +63,14 @@ def _name_for_vk(name: str) -> str:
|
||||
return name.replace(";", ",")
|
||||
|
||||
|
||||
def _vk_post(method: str, data: dict, token: str) -> dict:
|
||||
def _vk_post(method: str, data: dict, token: str, user_id: int | None = None) -> dict:
|
||||
data = {**data, "access_token": token, "v": settings.VK_API_VERSION}
|
||||
r = httpx.post(f"{VK_API}/{method}", data=data, timeout=30)
|
||||
r = api_logger.post(f"{VK_API}/{method}", user_id=user_id, data=data, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def _upload_photo(token: str, group_id: str) -> str | None:
|
||||
def _upload_photo(token: str, group_id: str, user_id: int | None = None) -> str | None:
|
||||
"""Upload the default product photo and return photo_id, or None on failure."""
|
||||
photo_path = settings.VK_DEFAULT_PHOTO_PATH
|
||||
if not os.path.exists(photo_path):
|
||||
@@ -78,7 +78,7 @@ def _upload_photo(token: str, group_id: str) -> str | None:
|
||||
return None
|
||||
try:
|
||||
# Step 1: get upload URL
|
||||
resp = _vk_post("market.getProductPhotoUploadServer", {"group_id": group_id}, token)
|
||||
resp = _vk_post("market.getProductPhotoUploadServer", {"group_id": group_id}, token, user_id=user_id)
|
||||
if "error" in resp:
|
||||
logger.warning("getProductPhotoUploadServer error: %s", resp["error"])
|
||||
return None
|
||||
@@ -86,12 +86,12 @@ def _upload_photo(token: str, group_id: str) -> str | None:
|
||||
|
||||
# Step 2: upload file
|
||||
with open(photo_path, "rb") as f:
|
||||
up = httpx.post(upload_url, files={"file": f}, timeout=30)
|
||||
up = api_logger.post(upload_url, user_id=user_id, files={"file": f}, timeout=30)
|
||||
up.raise_for_status()
|
||||
upload_obj = up.text
|
||||
|
||||
# Step 3: save
|
||||
resp2 = _vk_post("market.saveProductPhoto", {"upload_response": upload_obj}, token)
|
||||
resp2 = _vk_post("market.saveProductPhoto", {"upload_response": upload_obj}, token, user_id=user_id)
|
||||
if "error" in resp2:
|
||||
logger.warning("saveProductPhoto error: %s", resp2["error"])
|
||||
return None
|
||||
@@ -104,7 +104,7 @@ def _upload_photo(token: str, group_id: str) -> str | None:
|
||||
def _get_photo_id(user_id: int, token: str, group_id: str) -> str | None:
|
||||
"""Upload photo once per sync run per user, cache the result."""
|
||||
if user_id not in _PHOTO_CACHE:
|
||||
_PHOTO_CACHE[user_id] = _upload_photo(token, group_id)
|
||||
_PHOTO_CACHE[user_id] = _upload_photo(token, group_id, user_id=user_id)
|
||||
return _PHOTO_CACHE[user_id]
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ def _ensure_album(db, user_id: int, vk_group_id: str, group_name: str, token: st
|
||||
resp = _vk_post("market.addAlbum", {
|
||||
"owner_id": f"-{vk_group_id}",
|
||||
"title": group_name,
|
||||
}, token)
|
||||
}, token, user_id=user_id)
|
||||
if "error" in resp:
|
||||
logger.warning("market.addAlbum error for '%s': %s", group_name, resp["error"])
|
||||
return None
|
||||
@@ -212,7 +212,7 @@ def _sync_product(
|
||||
"category_id": settings.VK_CATEGORY_ID,
|
||||
"price": price_kopecks,
|
||||
"stock_amount": stock,
|
||||
}, token)
|
||||
}, token, user_id=user_id)
|
||||
if "error" in resp:
|
||||
logger.warning("market.edit error product=%s: %s", product.evotor_id, resp["error"])
|
||||
return
|
||||
@@ -223,12 +223,12 @@ def _sync_product(
|
||||
"owner_id": owner_id,
|
||||
"item_id": product.vk_product_id,
|
||||
"album_ids": old_album_id,
|
||||
}, token)
|
||||
}, token, user_id=user_id)
|
||||
resp_album = _vk_post("market.addToAlbum", {
|
||||
"owner_id": owner_id,
|
||||
"item_ids": product.vk_product_id,
|
||||
"album_ids": album_id,
|
||||
}, token)
|
||||
}, token, user_id=user_id)
|
||||
if "error" in resp_album:
|
||||
logger.warning("market.addToAlbum error product=%s: %s", product.evotor_id, resp_album["error"])
|
||||
else:
|
||||
@@ -255,7 +255,7 @@ def _sync_product(
|
||||
"price": price_kopecks,
|
||||
"main_photo_id": photo_id,
|
||||
"stock_amount": stock,
|
||||
}, token)
|
||||
}, token, user_id=user_id)
|
||||
if "error" in resp:
|
||||
logger.warning("market.add error product=%s: %s", product.evotor_id, resp["error"])
|
||||
return
|
||||
@@ -269,7 +269,7 @@ def _sync_product(
|
||||
"owner_id": owner_id,
|
||||
"item_ids": vk_item_id,
|
||||
"album_ids": album_id,
|
||||
}, token)
|
||||
}, token, user_id=user_id)
|
||||
if "error" in resp2:
|
||||
logger.warning("market.addToAlbum error product=%s: %s", product.evotor_id, resp2["error"])
|
||||
|
||||
@@ -310,7 +310,7 @@ def _delete_orphans(db, user_id, vk_group_id, owned_ids, token, results):
|
||||
resp = _vk_post("market.delete", {
|
||||
"owner_id": owner_id,
|
||||
"item_id": vk_p.vk_product_id,
|
||||
}, token)
|
||||
}, token, user_id=user_id)
|
||||
if "error" in resp:
|
||||
logger.warning("market.delete error id=%s: %s", vk_p.vk_product_id, resp["error"])
|
||||
results["errors"] += 1
|
||||
|
||||
Reference in New Issue
Block a user