feat: enhance logging configuration and add request logging context

This commit is contained in:
Abhimanyu Saharan
2026-02-11 16:49:43 +05:30
parent 56e97785d5
commit 8d0b2939a6
19 changed files with 530 additions and 38 deletions

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from fastapi import APIRouter, Depends, HTTPException, status
@@ -18,6 +17,7 @@ from app.api.deps import (
require_admin_or_agent,
)
from app.core.config import settings
from app.core.logging import get_logger
from app.core.time import utcnow
from app.db.session import get_session
from app.models.board_onboarding import BoardOnboardingSession
@@ -49,7 +49,7 @@ if TYPE_CHECKING:
from app.models.boards import Board
router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboarding"])
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
BOARD_USER_READ_DEP = Depends(get_board_for_user_read)
BOARD_USER_WRITE_DEP = Depends(get_board_for_user_write)
BOARD_OR_404_DEP = Depends(get_board_or_404)

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Literal
@@ -11,6 +10,7 @@ from fastapi import Depends, Header, HTTPException, Request, status
from sqlmodel import col, select
from app.core.agent_tokens import verify_agent_token
from app.core.logging import get_logger
from app.core.time import utcnow
from app.db.session import get_session
from app.models.agents import Agent
@@ -18,7 +18,7 @@ from app.models.agents import Agent
if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
_LAST_SEEN_TOUCH_INTERVAL = timedelta(seconds=30)
_SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal
@@ -17,6 +16,7 @@ from pydantic import BaseModel, ValidationError
from starlette.concurrency import run_in_threadpool
from app.core.config import settings
from app.core.logging import get_logger
from app.db import crud
from app.db.session import get_session
from app.models.users import User
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
from clerk_backend_api.models.user import User as ClerkUser
from sqlmodel.ext.asyncio.session import AsyncSession
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
security = HTTPBearer(auto_error=False)
SECURITY_DEP = Depends(security)
SESSION_DEP = Depends(get_session)
@@ -331,9 +331,8 @@ async def _get_or_sync_user(
)
else:
logger.debug(
"auth.user.sync clerk_user_id=%s updated=%s fetched_profile=%s",
"auth.user.sync.noop clerk_user_id=%s fetched_profile=%s",
clerk_user_id_log,
changed,
should_fetch_profile,
)
if not user.email:

View File

@@ -42,6 +42,8 @@ class Settings(BaseSettings):
log_level: str = "INFO"
log_format: str = "text"
log_use_utc: bool = False
request_log_slow_ms: int = Field(default=1000, ge=0)
request_log_include_health: bool = False
@model_validator(mode="after")
def _defaults(self) -> Self:

View File

@@ -2,8 +2,8 @@
from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from time import perf_counter
from typing import TYPE_CHECKING, Any, Final
from uuid import uuid4
@@ -13,12 +13,23 @@ from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import Response
from app.core.config import settings
from app.core.logging import (
TRACE_LEVEL,
get_logger,
reset_request_id,
reset_request_route_context,
set_request_id,
set_request_route_context,
)
if TYPE_CHECKING: # pragma: no cover
from starlette.types import ASGIApp, Message, Receive, Scope, Send
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
REQUEST_ID_HEADER: Final[str] = "X-Request-Id"
_HEALTH_CHECK_PATHS: Final[frozenset[str]] = frozenset({"/health", "/healthz", "/readyz"})
ExceptionHandler = Callable[[Request, Exception], Response | Awaitable[Response]]
@@ -31,6 +42,8 @@ class RequestIdMiddleware:
self._app = app
self._header_name = header_name
self._header_name_bytes = header_name.lower().encode("latin-1")
self._slow_request_ms = settings.request_log_slow_ms
self._include_health_logs = settings.request_log_include_health
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Inject request-id into request state and response headers."""
@@ -38,18 +51,80 @@ class RequestIdMiddleware:
await self._app(scope, receive, send)
return
method = str(scope.get("method") or "UNKNOWN").upper()
path = str(scope.get("path") or "")
client = scope.get("client")
client_ip: str | None = None
if isinstance(client, tuple) and client and isinstance(client[0], str):
client_ip = client[0]
should_log = self._include_health_logs or path not in _HEALTH_CHECK_PATHS
started_at = perf_counter()
status_code: int | None = None
request_id = self._get_or_create_request_id(scope)
context_token = set_request_id(request_id)
route_context_tokens = set_request_route_context(method, path)
if should_log:
logger.log(
TRACE_LEVEL,
"http.request.start",
extra={
"method": method,
"path": path,
"client_ip": client_ip,
},
)
async def send_with_request_id(message: Message) -> None:
nonlocal status_code
if message["type"] == "http.response.start":
# Starlette uses `list[tuple[bytes, bytes]]` here.
headers: list[tuple[bytes, bytes]] = message.setdefault("headers", [])
if not any(key.lower() == self._header_name_bytes for key, _ in headers):
request_id_bytes = request_id.encode("latin-1")
headers.append((self._header_name_bytes, request_id_bytes))
status = message.get("status")
status_code = status if isinstance(status, int) else 500
if should_log:
duration_ms = int((perf_counter() - started_at) * 1000)
extra = {
"method": method,
"path": path,
"status_code": status_code,
"duration_ms": duration_ms,
"client_ip": client_ip,
}
if status_code >= 500:
logger.error("http.request.complete", extra=extra)
elif status_code >= 400:
logger.warning("http.request.complete", extra=extra)
else:
logger.debug("http.request.complete", extra=extra)
if self._slow_request_ms and duration_ms >= self._slow_request_ms:
logger.warning(
"http.request.slow",
extra={
**extra,
"slow_threshold_ms": self._slow_request_ms,
},
)
await send(message)
await self._app(scope, receive, send_with_request_id)
try:
await self._app(scope, receive, send_with_request_id)
finally:
if should_log and status_code is None:
logger.warning(
"http.request.incomplete",
extra={
"method": method,
"path": path,
"duration_ms": int((perf_counter() - started_at) * 1000),
"client_ip": client_ip,
},
)
reset_request_route_context(route_context_tokens)
reset_request_id(context_token)
def _get_or_create_request_id(self, scope: Scope) -> str:
# Accept a client-provided request id if present.

View File

@@ -7,6 +7,7 @@ import logging
import os
import sys
import time
from contextvars import ContextVar, Token
from datetime import UTC, datetime
from types import TracebackType
from typing import Any
@@ -17,6 +18,9 @@ from app.core.version import APP_NAME, APP_VERSION
TRACE_LEVEL = 5
EXC_INFO_TUPLE_SIZE = 3
logging.addLevelName(TRACE_LEVEL, "TRACE")
_REQUEST_ID_CONTEXT: ContextVar[str | None] = ContextVar("request_id", default=None)
_REQUEST_METHOD_CONTEXT: ContextVar[str | None] = ContextVar("request_method", default=None)
_REQUEST_PATH_CONTEXT: ContextVar[str | None] = ContextVar("request_path", default=None)
def _coerce_exc_info(
@@ -75,6 +79,53 @@ def _trace(self: logging.Logger, message: str, *args: object, **kwargs: object)
logging.Logger.trace = _trace # type: ignore[attr-defined]
def set_request_id(request_id: str | None) -> Token[str | None]:
"""Bind request-id to logging context for the current task."""
normalized = (request_id or "").strip() or None
return _REQUEST_ID_CONTEXT.set(normalized)
def reset_request_id(token: Token[str | None]) -> None:
"""Reset request-id context to a previous token value."""
_REQUEST_ID_CONTEXT.reset(token)
def get_request_id() -> str | None:
"""Return request-id currently bound to logging context."""
return _REQUEST_ID_CONTEXT.get()
def set_request_route_context(
method: str | None,
path: str | None,
) -> tuple[Token[str | None], Token[str | None]]:
"""Bind request method/path to logging context for the current task."""
normalized_method = (method or "").strip().upper() or None
normalized_path = (path or "").strip() or None
return (
_REQUEST_METHOD_CONTEXT.set(normalized_method),
_REQUEST_PATH_CONTEXT.set(normalized_path),
)
def reset_request_route_context(tokens: tuple[Token[str | None], Token[str | None]]) -> None:
"""Reset request method/path context to previously-bound values."""
method_token, path_token = tokens
_REQUEST_METHOD_CONTEXT.reset(method_token)
_REQUEST_PATH_CONTEXT.reset(path_token)
def get_request_method() -> str | None:
"""Return request method currently bound to logging context."""
return _REQUEST_METHOD_CONTEXT.get()
def get_request_path() -> str | None:
"""Return request path currently bound to logging context."""
return _REQUEST_PATH_CONTEXT.get()
_STANDARD_LOG_RECORD_ATTRS = {
"args",
"asctime",
@@ -117,6 +168,18 @@ class AppLogFilter(logging.Filter):
"""Attach app metadata fields to each emitted record."""
record.app = self._app_name
record.version = self._version
if not getattr(record, "request_id", None):
request_id = get_request_id()
if request_id:
record.request_id = request_id
if not getattr(record, "method", None):
method = get_request_method()
if method:
record.method = method
if not getattr(record, "path", None):
path = get_request_path()
if path:
record.path = path
return True
@@ -227,6 +290,18 @@ class AppLogger:
logger = logging.getLogger(name)
logger.disabled = True
logging.getLogger(__name__).info(
"logging.configured level=%s format=%s use_utc=%s",
level_name,
format_name,
settings.log_use_utc,
)
logging.getLogger(__name__).debug(
"logging.libraries uvicorn_level=%s sql_enabled=%s",
level_name,
level_name == "TRACE",
)
cls._configured = True
@classmethod
@@ -240,3 +315,8 @@ class AppLogger:
def configure_logging() -> None:
"""Configure global application logging once during startup."""
AppLogger.configure()
def get_logger(name: str | None = None) -> logging.Logger:
"""Return an app logger from the centralized logger configuration."""
return AppLogger.get_logger(name)

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import asyncio
import logging
from pathlib import Path
from typing import TYPE_CHECKING
@@ -16,6 +15,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from app import models as _models
from app.core.config import settings
from app.core.logging import get_logger
if TYPE_CHECKING:
from collections.abc import AsyncGenerator
@@ -42,7 +42,7 @@ async_session_maker = async_sessionmaker(
class_=AsyncSession,
expire_on_commit=False,
)
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def _alembic_config() -> Config:

View File

@@ -28,20 +28,30 @@ from app.api.tasks import router as tasks_router
from app.api.users import router as users_router
from app.core.config import settings
from app.core.error_handling import install_error_handling
from app.core.logging import configure_logging
from app.core.logging import configure_logging, get_logger
from app.db.session import init_db
if TYPE_CHECKING:
from collections.abc import AsyncIterator
configure_logging()
logger = get_logger(__name__)
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
"""Initialize application resources before serving requests."""
logger.info(
"app.lifecycle.starting environment=%s db_auto_migrate=%s",
settings.environment,
settings.db_auto_migrate,
)
await init_db()
yield
logger.info("app.lifecycle.started")
try:
yield
finally:
logger.info("app.lifecycle.stopped")
app = FastAPI(title="Mission Control API", version="0.1.0", lifespan=lifespan)
@@ -55,6 +65,9 @@ if origins:
allow_methods=["*"],
allow_headers=["*"],
)
logger.info("app.cors.enabled origins_count=%s", len(origins))
else:
logger.info("app.cors.disabled")
install_error_handling(app)
@@ -98,3 +111,4 @@ api_v1.include_router(users_router)
app.include_router(api_v1)
add_pagination(app)
logger.debug("app.routes.registered count=%s", len(app.routes))

View File

@@ -10,6 +10,7 @@ from fastapi import HTTPException, status
from sqlmodel import col
from app.core.auth import AuthContext
from app.core.logging import TRACE_LEVEL
from app.core.time import utcnow
from app.db import crud
from app.models.activity_events import ActivityEvent
@@ -256,7 +257,7 @@ class GatewayAdminLifecycleService(OpenClawDBService):
action: str = "provision",
) -> Agent:
self.logger.log(
5,
TRACE_LEVEL,
"gateway.main_agent.ensure.start gateway_id=%s action=%s",
gateway.id,
action,
@@ -331,7 +332,7 @@ class GatewayAdminLifecycleService(OpenClawDBService):
auth: AuthContext,
) -> GatewayTemplatesSyncResult:
self.logger.log(
5,
TRACE_LEVEL,
"gateway.templates.sync.start gateway_id=%s include_main=%s",
gateway.id,
query.include_main,

View File

@@ -12,6 +12,7 @@ from fastapi import HTTPException, status
from sqlmodel import col, select
from app.core.config import settings
from app.core.logging import TRACE_LEVEL
from app.core.time import utcnow
from app.models.agents import Agent
from app.models.boards import Board
@@ -172,7 +173,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
) -> None:
trace_id = GatewayDispatchService.resolve_trace_id(correlation_id, prefix="coord.nudge")
self.logger.log(
5,
TRACE_LEVEL,
"gateway.coordination.nudge.start trace_id=%s board_id=%s actor_agent_id=%s "
"target_agent_id=%s",
trace_id,
@@ -252,7 +253,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
) -> str:
trace_id = GatewayDispatchService.resolve_trace_id(correlation_id, prefix="coord.soul.read")
self.logger.log(
5,
TRACE_LEVEL,
"gateway.coordination.soul_read.start trace_id=%s board_id=%s target_agent_id=%s",
trace_id,
board.id,
@@ -322,7 +323,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
correlation_id, prefix="coord.soul.write"
)
self.logger.log(
5,
TRACE_LEVEL,
"gateway.coordination.soul_write.start trace_id=%s board_id=%s target_agent_id=%s "
"actor_agent_id=%s",
trace_id,
@@ -418,7 +419,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
payload.correlation_id, prefix="coord.ask_user"
)
self.logger.log(
5,
TRACE_LEVEL,
"gateway.coordination.ask_user.start trace_id=%s board_id=%s actor_agent_id=%s",
trace_id,
board.id,
@@ -563,7 +564,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
payload.correlation_id, prefix="coord.lead_message"
)
self.logger.log(
5,
TRACE_LEVEL,
"gateway.coordination.lead_message.start trace_id=%s board_id=%s actor_agent_id=%s",
trace_id,
board_id,
@@ -652,7 +653,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
payload.correlation_id, prefix="coord.lead_broadcast"
)
self.logger.log(
5,
TRACE_LEVEL,
"gateway.coordination.lead_broadcast.start trace_id=%s actor_agent_id=%s",
trace_id,
actor_agent.id,

View File

@@ -6,9 +6,11 @@ OpenClaw services without adding new architectural layers.
from __future__ import annotations
import logging
from logging import Logger
from typing import TYPE_CHECKING
from app.core.logging import get_logger
if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -19,7 +21,7 @@ class OpenClawDBService:
def __init__(self, session: AsyncSession) -> None:
self._session = session
# Use the concrete subclass module for logger naming.
self._logger = logging.getLogger(self.__class__.__module__)
self._logger = get_logger(self.__class__.__module__)
@property
def session(self) -> AsyncSession:
@@ -30,11 +32,11 @@ class OpenClawDBService:
self._session = value
@property
def logger(self) -> logging.Logger:
def logger(self) -> Logger:
return self._logger
@logger.setter
def logger(self, value: logging.Logger) -> None:
def logger(self, value: Logger) -> None:
self._logger = value
async def add_commit_refresh(self, model: object) -> None:

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import asyncio
import json
from dataclasses import dataclass
from time import perf_counter
from typing import Any
from urllib.parse import urlencode, urlparse, urlunparse
from uuid import uuid4
@@ -17,7 +18,10 @@ from uuid import uuid4
import websockets
from websockets.exceptions import WebSocketException
from app.core.logging import TRACE_LEVEL, get_logger
PROTOCOL_VERSION = 3
logger = get_logger(__name__)
# NOTE: These are the base gateway methods from the OpenClaw gateway repo.
# The gateway can expose additional methods at runtime via channel plugins.
@@ -165,6 +169,11 @@ def _build_gateway_url(config: GatewayConfig) -> str:
return str(urlunparse(parsed._replace(query=query)))
def _redacted_url_for_log(raw_url: str) -> str:
parsed = urlparse(raw_url)
return str(urlunparse(parsed._replace(query="", fragment="")))
async def _await_response(
ws: websockets.ClientConnection,
request_id: str,
@@ -172,6 +181,12 @@ async def _await_response(
while True:
raw = await ws.recv()
data = json.loads(raw)
logger.log(
TRACE_LEVEL,
"gateway.rpc.recv request_id=%s type=%s",
request_id,
data.get("type"),
)
if data.get("type") == "res" and data.get("id") == request_id:
ok = data.get("ok")
@@ -199,6 +214,13 @@ async def _send_request(
"method": method,
"params": params or {},
}
logger.log(
TRACE_LEVEL,
"gateway.rpc.send method=%s request_id=%s params_keys=%s",
method,
request_id,
sorted((params or {}).keys()),
)
await ws.send(json.dumps(message))
return await _await_response(ws, request_id)
@@ -229,7 +251,11 @@ async def _ensure_connected(
first_message = first_message.decode("utf-8")
data = json.loads(first_message)
if data.get("type") != "event" or data.get("event") != "connect.challenge":
pass
logger.warning(
"gateway.rpc.connect.unexpected_first_message type=%s event=%s",
data.get("type"),
data.get("event"),
)
connect_id = str(uuid4())
response = {
"type": "req",
@@ -249,6 +275,12 @@ async def openclaw_call(
) -> object:
"""Call a gateway RPC method and return the result payload."""
gateway_url = _build_gateway_url(config)
started_at = perf_counter()
logger.debug(
"gateway.rpc.call.start method=%s gateway_url=%s",
method,
_redacted_url_for_log(gateway_url),
)
try:
async with websockets.connect(gateway_url, ping_interval=None) as ws:
first_message = None
@@ -257,8 +289,19 @@ async def openclaw_call(
except TimeoutError:
first_message = None
await _ensure_connected(ws, first_message, config)
return await _send_request(ws, method, params)
payload = await _send_request(ws, method, params)
logger.debug(
"gateway.rpc.call.success method=%s duration_ms=%s",
method,
int((perf_counter() - started_at) * 1000),
)
return payload
except OpenClawGatewayError:
logger.warning(
"gateway.rpc.call.gateway_error method=%s duration_ms=%s",
method,
int((perf_counter() - started_at) * 1000),
)
raise
except (
TimeoutError,
@@ -267,6 +310,12 @@ async def openclaw_call(
ValueError,
WebSocketException,
) as exc: # pragma: no cover - network/protocol errors
logger.error(
"gateway.rpc.call.transport_error method=%s duration_ms=%s error_type=%s",
method,
int((perf_counter() - started_at) * 1000),
exc.__class__.__name__,
)
raise OpenClawGatewayError(str(exc)) from exc

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from app.core.logging import TRACE_LEVEL
from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board
from app.services.openclaw.coordination_service import AbstractGatewayMessagingService
@@ -25,7 +26,7 @@ class BoardOnboardingMessagingService(AbstractGatewayMessagingService):
correlation_id, prefix="onboarding.start"
)
self.logger.log(
5,
TRACE_LEVEL,
"gateway.onboarding.start_dispatch.start trace_id=%s board_id=%s",
trace_id,
board.id,
@@ -83,7 +84,7 @@ class BoardOnboardingMessagingService(AbstractGatewayMessagingService):
correlation_id, prefix="onboarding.answer"
)
self.logger.log(
5,
TRACE_LEVEL,
"gateway.onboarding.answer_dispatch.start trace_id=%s board_id=%s onboarding_id=%s",
trace_id,
board.id,

View File

@@ -22,6 +22,7 @@ from sqlmodel import col, select
from sse_starlette.sse import EventSourceResponse
from app.core.agent_tokens import verify_agent_token
from app.core.logging import TRACE_LEVEL
from app.core.time import utcnow
from app.db import crud
from app.db.pagination import paginate
@@ -983,7 +984,7 @@ class AgentLifecycleService(OpenClawDBService):
raise_gateway_errors: bool,
) -> None:
self.logger.log(
5,
TRACE_LEVEL,
"agent.provision.start action=%s agent_id=%s target_main=%s",
action,
agent.id,
@@ -1471,7 +1472,7 @@ class AgentLifecycleService(OpenClawDBService):
actor: ActorContextLike,
) -> AgentRead:
self.logger.log(
5,
TRACE_LEVEL,
"agent.create.start actor_type=%s board_id=%s",
actor.actor_type,
payload.board_id,
@@ -1523,7 +1524,12 @@ class AgentLifecycleService(OpenClawDBService):
payload: AgentUpdate,
options: AgentUpdateOptions,
) -> AgentRead:
self.logger.log(5, "agent.update.start agent_id=%s force=%s", agent_id, options.force)
self.logger.log(
TRACE_LEVEL,
"agent.update.start agent_id=%s force=%s",
agent_id,
options.force,
)
agent = await Agent.objects.by_id(agent_id).first(self.session)
if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -1573,7 +1579,10 @@ class AgentLifecycleService(OpenClawDBService):
actor: ActorContextLike,
) -> AgentRead:
self.logger.log(
5, "agent.heartbeat.start agent_id=%s actor_type=%s", agent_id, actor.actor_type
TRACE_LEVEL,
"agent.heartbeat.start agent_id=%s actor_type=%s",
agent_id,
actor.actor_type,
)
agent = await Agent.objects.by_id(agent_id).first(self.session)
if agent is None:
@@ -1599,7 +1608,7 @@ class AgentLifecycleService(OpenClawDBService):
actor: ActorContextLike,
) -> AgentRead:
self.logger.log(
5,
TRACE_LEVEL,
"agent.heartbeat_or_create.start actor_type=%s name=%s board_id=%s",
actor.actor_type,
payload.name,
@@ -1644,7 +1653,7 @@ class AgentLifecycleService(OpenClawDBService):
agent_id: str,
ctx: OrganizationContext,
) -> OkResponse:
self.logger.log(5, "agent.delete.start agent_id=%s", agent_id)
self.logger.log(TRACE_LEVEL, "agent.delete.start agent_id=%s", agent_id)
agent = await Agent.objects.by_id(agent_id).first(self.session)
if agent is None:
return OkResponse()

View File

@@ -9,6 +9,7 @@ from uuid import UUID
from fastapi import HTTPException, status
from app.core.logging import TRACE_LEVEL
from app.models.boards import Board
from app.schemas.gateway_api import (
GatewayResolveQuery,
@@ -88,7 +89,7 @@ class GatewaySessionService(OpenClawDBService):
user: User | None = None,
) -> tuple[Board | None, GatewayClientConfig, str | None]:
self.logger.log(
5,
TRACE_LEVEL,
"gateway.resolve.start board_id=%s gateway_url=%s",
params.board_id,
params.gateway_url,