fix: error_logs.path VARCHAR(500) → TEXT — 서버 응답 불가 해결

멀티 티커 요청 URL이 500자를 초과하면서 error_logs INSERT가
StringDataRightTruncationError로 실패, 미들웨어에서 매 에러 응답마다
DB rollback 대기가 발생해 서버가 응답 불가 수준으로 느려졌음 (2562회 반복).

- error_logs.path: String(500) → Text (model + migration j1b2c3d4e5f6)
- request_logs.path: String(500) → Text (model + migration i0a1b2c3d4e5)
- docker-compose: alembic/ + alembic.ini 볼륨 마운트 추가
  (이전에는 컨테이너 내부에만 있어 마이그레이션 적용 불가)
- middleware: 요청마다 찍히던 logger.info → logger.debug 로 변경

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 5bcba8632c
commit 409966a478

@ -0,0 +1,37 @@
"""fix request_log path column to TEXT
Revision ID: i0a1b2c3d4e5
Revises: h9b0c1d2e3f4
Create Date: 2026-04-13
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "i0a1b2c3d4e5"
down_revision: Union[str, Sequence[str], None] = "h9b0c1d2e3f4"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# path stores the full URL (path + query string) and can exceed 500 chars
# for multi-ticker requests — widen to TEXT
op.alter_column(
"request_logs",
"path",
existing_type=sa.String(500),
type_=sa.Text(),
existing_nullable=True,
)
def downgrade() -> None:
op.alter_column(
"request_logs",
"path",
existing_type=sa.Text(),
type_=sa.String(500),
existing_nullable=True,
)

@ -0,0 +1,39 @@
"""fix error_log path column to TEXT
Revision ID: j1b2c3d4e5f6
Revises: i0a1b2c3d4e5
Create Date: 2026-04-14
error_logs.path was VARCHAR(500), causing StringDataRightTruncationError
for multi-ticker requests whose URLs exceed 500 chars. This blocked every
error log INSERT and added rollback latency to each error response.
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "j1b2c3d4e5f6"
down_revision: Union[str, Sequence[str], None] = "i0a1b2c3d4e5"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.alter_column(
"error_logs",
"path",
existing_type=sa.String(500),
type_=sa.Text(),
existing_nullable=True,
)
def downgrade() -> None:
op.alter_column(
"error_logs",
"path",
existing_type=sa.Text(),
type_=sa.String(500),
existing_nullable=True,
)

@ -107,7 +107,7 @@ class ErrorLoggingMiddleware(BaseHTTPMiddleware):
# Store request details for potential error logging # Store request details for potential error logging
request_info = self._extract_request_info_minimal(request) request_info = self._extract_request_info_minimal(request)
logger.info(f"Processing request {request_id}: {request.method} {request.url.path}") logger.debug(f"Processing request {request_id}: {request.method} {request.url.path}")
try: try:
# Process the request # Process the request
@ -122,7 +122,7 @@ class ErrorLoggingMiddleware(BaseHTTPMiddleware):
# Check if response indicates an error (4xx or 5xx) # Check if response indicates an error (4xx or 5xx)
if response.status_code >= 400: if response.status_code >= 400:
logger.info(f"Error response detected: {response.status_code} for request {request_id}") logger.debug(f"Error response detected: {response.status_code} for request {request_id}")
# Full extraction for error logging (body + headers) # Full extraction for error logging (body + headers)
request_info = await self._extract_request_info(request) request_info = await self._extract_request_info(request)
@ -150,7 +150,7 @@ class ErrorLoggingMiddleware(BaseHTTPMiddleware):
error_detail = {"raw_response": str(response_body)} error_detail = {"raw_response": str(response_body)}
# Log the error with response body # Log the error with response body
logger.info(f"Logging error for request {request_id}") logger.debug(f"Logging error for request {request_id}")
await self._log_error( await self._log_error(
request_id=request_id, request_id=request_id,
request_info=request_info, request_info=request_info,
@ -271,23 +271,19 @@ class ErrorLoggingMiddleware(BaseHTTPMiddleware):
): ):
"""Log error to database""" """Log error to database"""
logger.info(f"_log_error called for request {request_id}, status {status_code}")
# Skip logging errors for log deletion endpoints to avoid logging the deletion of logs # Skip logging errors for log deletion endpoints to avoid logging the deletion of logs
endpoint = request_info["endpoint"] endpoint = request_info["endpoint"]
method = request_info["method"] method = request_info["method"]
# Don't log errors for DELETE requests to log management endpoints # Don't log errors for DELETE requests to log management endpoints
if (method == "DELETE" and if (method == "DELETE" and
(endpoint.startswith("/api/v1/admin/requests/logs") or (endpoint.startswith("/api/v1/admin/requests/logs") or
endpoint.startswith("/api/v1/admin/errors/logs"))): endpoint.startswith("/api/v1/admin/errors/logs"))):
logger.info(f"Skipping error log for log deletion endpoint: {method} {endpoint}")
return return
try: try:
# Get database session # Get database session
from app.core.database import AsyncSessionLocal from app.core.database import AsyncSessionLocal
logger.info(f"Creating database session for request {request_id}")
async with AsyncSessionLocal() as db: async with AsyncSessionLocal() as db:
# Extract error information from detail if available # Extract error information from detail if available
if error_detail and isinstance(error_detail, dict): if error_detail and isinstance(error_detail, dict):
@ -328,10 +324,8 @@ class ErrorLoggingMiddleware(BaseHTTPMiddleware):
) )
db.add(error_log) db.add(error_log)
logger.info(f"Error log added to session for request {request_id}")
await db.commit() await db.commit()
logger.info(f"Error log committed to database for request {request_id}")
logger.error( logger.error(
f"Error logged - Request ID: {request_id}, " f"Error logged - Request ID: {request_id}, "
f"Endpoint: {request_info['endpoint']}, " f"Endpoint: {request_info['endpoint']}, "

@ -18,7 +18,7 @@ class ErrorLog(Base):
request_id = Column(String(50), index=True) request_id = Column(String(50), index=True)
endpoint = Column(String(200), index=True) endpoint = Column(String(200), index=True)
method = Column(String(10)) method = Column(String(10))
path = Column(String(500)) path = Column(Text)
query_params = Column(JSON, nullable=True) query_params = Column(JSON, nullable=True)
request_body = Column(JSON, nullable=True) request_body = Column(JSON, nullable=True)
headers = Column(JSON, nullable=True) headers = Column(JSON, nullable=True)

@ -18,7 +18,7 @@ class RequestLog(Base):
request_id = Column(String(50), index=True) request_id = Column(String(50), index=True)
endpoint = Column(String(200), index=True) endpoint = Column(String(200), index=True)
method = Column(String(10)) method = Column(String(10))
path = Column(String(500)) path = Column(Text)
query_params = Column(JSON, nullable=True) query_params = Column(JSON, nullable=True)
request_body = Column(JSON, nullable=True) request_body = Column(JSON, nullable=True)
headers = Column(JSON, nullable=True) headers = Column(JSON, nullable=True)

@ -59,6 +59,8 @@ services:
- redis - redis
volumes: volumes:
- ./app:/app/app # Mount app directory for development - ./app:/app/app # Mount app directory for development
- ./alembic:/app/alembic # Mount alembic for live migration access
- ./alembic.ini:/app/alembic.ini
- ./stock_oracle_analyzer.py:/app/stock_oracle_analyzer.py - ./stock_oracle_analyzer.py:/app/stock_oracle_analyzer.py
- ./API_DOCUMENTATION.md:/app/API_DOCUMENTATION.md # API documentation - ./API_DOCUMENTATION.md:/app/API_DOCUMENTATION.md # API documentation
- ./yfinance_plus:/app/yfinance_plus # Mount yfinance_plus for development - ./yfinance_plus:/app/yfinance_plus # Mount yfinance_plus for development

Loading…
Cancel
Save