feat(infra): add metrics endpoint, redis/postgres services, ollama url fix, llm fallback

This commit is contained in:
Crypto Rug Munch 2026-07-03 07:25:36 +07:00
parent dec3db9618
commit f2c8e1d7b9
No known key found for this signature in database
GPG key ID: 58D4300721937626
7 changed files with 86 additions and 1 deletions

9
api.py
View file

@ -47,6 +47,7 @@ from extractor import SchemaExtractor
from mconfig import PryConfig
from mcp_production import make_fallback_server, register_all
from mcp_sse import mcp_post_message, mcp_sse_endpoint
from observability import get_metrics_output, setup_tracing
from pipeline import HOOK_POINTS, get_pipeline, run_pipeline
from pryextras import BatchProcessor, TransformEngine, recorder, streams
from routers.auth import router as auth_router
@ -80,6 +81,7 @@ except ImportError:
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Startup: validate deps. Shutdown: cleanup clients."""
logger.info("pry_startup", version="3.0.0")
setup_tracing()
if not settings.api_key:
logger.warning(
"pry_api_key_unset",
@ -2810,6 +2812,13 @@ app.include_router(health_router)
app.include_router(scraping_router)
app.include_router(templates_router)
@app.get("/metrics", tags=["System"], summary="Prometheus metrics", include_in_schema=False)
async def metrics() -> Response:
"""Expose Prometheus metrics for scraping."""
data, content_type = get_metrics_output()
return Response(content=data, media_type=content_type)
# ── Review ──

1
cli.py
View file

@ -83,6 +83,7 @@ def _spinner(label: str):
if stop:
break
print(f" {CYAN}{c}{NC} {label}... ", end="\r", flush=True)
# CLI spinner runs in a daemon thread; sync sleep is acceptable here.
time.sleep(0.1)
t = threading.Thread(target=_spin, daemon=True)

View file

@ -80,3 +80,40 @@ services:
volumes:
pry-data:
pry-sessions:
redis:
image: redis:7-alpine
container_name: pry-redis
restart: unless-stopped
ports:
- "127.0.0.1:6379:6379"
volumes:
- pry-redis:/data
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
postgres:
image: postgres:15-alpine
container_name: pry-postgres
restart: unless-stopped
ports:
- "127.0.0.1:5432:5432"
volumes:
- pry-postgres:/var/lib/postgresql/data
environment:
- POSTGRES_USER=pry
- POSTGRES_PASSWORD=pry
- POSTGRES_DB=pry
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
volumes:
pry-sessions:
pry-redis:
pry-postgres:

View file

@ -94,6 +94,35 @@ class LLMRegistry:
break
raise Exception(f"All LLM providers failed. Last: {last_error}")
async def complete_or_empty(
self,
prompt: str,
system: str = "",
provider_name: str = "",
max_tokens: int = 1024,
temperature: float = 0.7,
model: str = "",
fallback: bool = True,
) -> LLMResponse:
"""Complete via LLM, returning an empty response on total failure.
Use this at API boundaries where a 500 is worse than degraded output.
"""
try:
return await self.complete(
prompt, system, provider_name, max_tokens, temperature, model, fallback
)
except Exception as e: # noqa: BLE001
logger.warning("llm_complete_failed_all_providers", extra={"error": str(e)[:120]})
return LLMResponse(
text="",
provider="none",
model="",
input_tokens=0,
output_tokens=0,
cost_usd=0.0,
)
async def embed(self, text: str, provider_name: str = "", model: str = "") -> list[float]:
names = [provider_name] if provider_name else list(self.fallback_chain)
for name in names:

View file

@ -50,6 +50,10 @@ dependencies = [
"structlog>=24.0.0",
"sqlalchemy>=2.0.0",
"aiosqlite>=0.19.0",
"prometheus-client>=0.21.0",
"opentelemetry-api>=1.29.0",
"opentelemetry-sdk>=1.29.0",
"opentelemetry-instrumentation-fastapi>=0.50b0",
]
[project.optional-dependencies]

View file

@ -25,7 +25,7 @@ class PrySettings(BaseSettings):
port: int = 8002
# Ollama LLM endpoint
ollama_url: str = "http://100.100.18.18:11434"
ollama_url: str = "http://100.104.130.92:11434"
# FlareSolverr Cloudflare bypass endpoint
flaresolverr_url: str = "http://flaresolverr:8191/v1"

View file

@ -314,6 +314,11 @@ class UltimateScraper:
driver = uc.Chrome(headless=True, use_subprocess=True)
driver.get(url)
# Note: time.sleep is used inside a sync thread-pool task because
# undetected_chromedriver's blocking API runs in asyncio.to_thread.
# Replacing with asyncio.sleep would require restructuring the tier
# to be fully async; this sync sleep is bounded and does not block
# the event loop directly.
time.sleep(random.uniform(2, 4))
html = driver.page_source
driver.quit()