feat(V2): /health/ready con ping client + middleware request log strutturato + request_id correlation

- /health/ready: ping di tutti i client (exchange, env) cached con
  timeout 2s, status ready|degraded|not_ready, opt-in 503 via
  READY_FAILS_ON_DEGRADED.
- Middleware mcp.request: 1 riga JSON per HTTP request con request_id,
  method, path, status_code, duration_ms, actor, bot_tag, exchange,
  tool, client_ip, user_agent.
- request_id propagato in request.state, audit log e error envelope per
  correlazione cross-cutting.
- Aggiunto async health() come probe minimo a bybit/alpaca/macro/
  sentiment/deribit (hyperliquid lo aveva già).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-05-01 09:03:28 +02:00
parent 9afd087152
commit 8ecc1a24a9
13 changed files with 509 additions and 2 deletions
@@ -99,6 +99,10 @@ class AlpacaClient:
if not self._http.is_closed:
await self._http.aclose()
async def health(self) -> dict[str, Any]:
"""Probe minimo per /health/ready: nessuna chiamata di rete."""
return {"status": "ok", "paper": self.paper}
# ── Helpers ──────────────────────────────────────────────────
@property
@@ -75,6 +75,10 @@ class BybitClient:
if self._owns_http:
await self._http.aclose()
async def health(self) -> dict[str, Any]:
"""Probe minimo per /health/ready: nessuna chiamata di rete."""
return {"status": "ok", "testnet": self.testnet}
# ── auth helpers ───────────────────────────────────────────
def _timestamp_ms(self) -> str:
@@ -97,6 +97,10 @@ class DeribitClient:
def is_testnet(self) -> dict:
return {"testnet": self.testnet, "base_url": self.base_url}
async def health(self) -> dict:
"""Probe minimo per /health/ready: nessuna chiamata di rete."""
return {"status": "ok", "testnet": self.testnet}
async def get_ticker(self, instrument_name: str) -> dict:
import datetime as _dt
raw = await self._request("public/ticker", {"instrument_name": instrument_name})
@@ -8,6 +8,8 @@ istanziato dal `ClientRegistry`.
"""
from __future__ import annotations
from typing import Any
class MacroClient:
"""Wrapper credenziali FRED/Finnhub. Stateless, no HTTP session."""
@@ -18,3 +20,7 @@ class MacroClient:
async def aclose(self) -> None: # pragma: no cover - no-op, no resources
return None
async def health(self) -> dict[str, Any]:
"""Probe minimo per /health/ready: nessuna chiamata di rete."""
return {"status": "ok"}
@@ -9,6 +9,8 @@ e per essere istanziato dal `ClientRegistry`.
"""
from __future__ import annotations
from typing import Any
class SentimentClient:
"""Wrapper credenziali CryptoPanic/LunarCrush. Stateless, no HTTP session."""
@@ -19,3 +21,7 @@ class SentimentClient:
async def aclose(self) -> None: # pragma: no cover - no-op, no resources
return None
async def health(self) -> dict[str, Any]:
"""Probe minimo per /health/ready: nessuna chiamata di rete."""
return {"status": "ok"}