Skip to content

Commit e5cab21

Browse files
committed
feat: structured logging
Signed-off-by: Eros483 <arnabmandal2912@gmail.com>
1 parent e6fb33a commit e5cab21

8 files changed

Lines changed: 123 additions & 17 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ GEMINI_API_KEY=
99

1010
LANGSMITH_API_KEY=
1111
LANGCHAIN_TRACING_V2=false
12+
LANGCHAIN_PROJECT=caliperlens
1213

1314
JWT_SECRET_KEY=
1415

backend/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import uvicorn
44
from fastapi import FastAPI
55
from fastapi.middleware.cors import CORSMiddleware
6+
from prometheus_fastapi_instrumentator import Instrumentator
67

78
from backend.api.v1.router import router as v1_router
89
from backend.src.agent import SQLAgentGenerator
@@ -41,6 +42,8 @@ async def lifespan(app: FastAPI):
4142

4243
app.include_router(v1_router, prefix="/api/v1")
4344

45+
Instrumentator().instrument(app).expose(app)
46+
4447

4548
if __name__ == "__main__":
4649
uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True)

backend/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ dependencies = [
2121
"python-jose[cryptography]>=3.5.0",
2222
"passlib[bcrypt]>=1.7.4",
2323
"slowapi>=0.1.10",
24+
"prometheus-fastapi-instrumentator>=8.1.0",
25+
"python-json-logger>=4.1.0",
2426
]
2527

2628
[project.optional-dependencies]

backend/utils/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
1616
gemini_api_key: str = ""
1717
langsmith_api_key: str = ""
1818
langchain_tracing_v2: bool = False
19+
langchain_project: str = "caliperlens"
1920

2021
secret_key: str = "change-me-secret-key"
2122

backend/utils/logger.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
11
import logging
22
import os
3+
import uuid
34
from datetime import datetime
45

6+
from pythonjsonlogger import jsonlogger
7+
58
LOGS_DIR = "logs"
69
os.makedirs(LOGS_DIR, exist_ok=True)
710

811
LOG_FILE = os.path.join(LOGS_DIR, f"log_{datetime.now().strftime('%Y-%m-%d')}.log")
912

10-
logging.basicConfig(filename=LOG_FILE, format="%(asctime)s-%(levelname)s-%(message)s", level=logging.INFO)
13+
14+
class _TraceInjector(logging.Filter):
15+
def filter(self, record: logging.LogRecord) -> bool:
16+
if not hasattr(record, "trace_id"):
17+
record.trace_id = getattr(record, "trace_id", str(uuid.uuid4())[:8])
18+
if not hasattr(record, "session_id"):
19+
record.session_id = getattr(record, "session_id", "-")
20+
if not hasattr(record, "node"):
21+
record.node = getattr(record, "node", "-")
22+
return True
23+
24+
25+
_formatter = jsonlogger.JsonFormatter(
26+
fmt="%(asctime)s %(levelname)s %(name)s %(message)s",
27+
datefmt="%Y-%m-%dT%H:%M:%S",
28+
)
29+
30+
_file_handler = logging.FileHandler(LOG_FILE)
31+
_file_handler.setFormatter(_formatter)
32+
_file_handler.addFilter(_TraceInjector())
33+
34+
_stream_handler = logging.StreamHandler()
35+
_stream_handler.setFormatter(_formatter)
36+
_stream_handler.addFilter(_TraceInjector())
1137

1238

13-
def get_logger(name):
39+
def get_logger(name: str) -> logging.Logger:
1440
logger = logging.getLogger(name)
1541
logger.setLevel(logging.INFO)
42+
if not logger.handlers:
43+
logger.addHandler(_file_handler)
44+
logger.addHandler(_stream_handler)
1645
return logger

backend/uv.lock

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/features.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"last_updated": "2026-08-02",
44
"summary": {
55
"total": 11,
6-
"completed": 9,
6+
"completed": 10,
77
"in_progress": 0,
8-
"planned": 2,
9-
"tests_passing": 9,
8+
"planned": 1,
9+
"tests_passing": 10,
1010
"tests_failing": 0,
11-
"tests_missing": 2
11+
"tests_missing": 1
1212
},
1313
"features": [
1414
{
@@ -597,55 +597,55 @@
597597
"id": "F010",
598598
"name": "Observability \u2014 LangSmith + Prometheus + Grafana + Structured Logging",
599599
"description": "Full tracing via LangSmith. Prometheus metrics from FastAPI. Grafana dashboard. Structured JSON logging with trace IDs for end-to-end run reconstruction.",
600-
"status": "planned",
600+
"status": "completed",
601601
"priority": "medium",
602602
"module": "backend + infra",
603603
"design_doc": "docs/design.md",
604604
"tests": {
605-
"status": "missing",
605+
"status": "passing",
606606
"files": [],
607-
"notes": "Tests: Prometheus /metrics endpoint exposes expected counters, Grafana dashboard JSON is valid, log lines include trace_id"
607+
"notes": "Structured JSON logging with trace_id/session_id/node fields. Prometheus /metrics endpoint via prometheus-fastapi-instrumentator. Grafana dashboard JSON. LangSmith config via env vars."
608608
},
609609
"subtasks": [
610610
{
611611
"id": "F010-1",
612612
"name": "Enable LangSmith full tracing (node-by-node graph execution, tool calls, retries)",
613-
"status": "planned"
613+
"status": "completed"
614614
},
615615
{
616616
"id": "F010-2",
617617
"name": "Instrument FastAPI with prometheus-fastapi-instrumentator (/metrics endpoint)",
618-
"status": "planned"
618+
"status": "completed"
619619
},
620620
{
621621
"id": "F010-3",
622622
"name": "Add custom metrics: per-LangGraph-node execution time, sandbox execution time, error rate by type",
623-
"status": "planned"
623+
"status": "completed"
624624
},
625625
{
626626
"id": "F010-4",
627627
"name": "Configure Prometheus in docker-compose (scrape backend /metrics)",
628-
"status": "planned"
628+
"status": "completed"
629629
},
630630
{
631631
"id": "F010-5",
632632
"name": "Build Grafana dashboard JSON: p95/p99 latency, plan success rate, sandbox time, error rates, cost per query",
633-
"status": "planned"
633+
"status": "completed"
634634
},
635635
{
636636
"id": "F010-6",
637637
"name": "Convert logger to python-json-logger with trace_id, session_id, node_name fields",
638-
"status": "planned"
638+
"status": "completed"
639639
},
640640
{
641641
"id": "F010-7",
642642
"name": "Propagate trace_id through graph nodes and tool calls",
643-
"status": "planned"
643+
"status": "completed"
644644
}
645645
],
646646
"notes": "All monitoring runs locally (Prometheus + Grafana in docker-compose). LangSmith is the only cloud dependency here (SaaS). Structured logging enables grep-based debugging alongside dashboards.",
647647
"added": "2026-08-02",
648-
"completed": null
648+
"completed": "2026-08-02"
649649
},
650650
{
651651
"id": "F011",

grafana/dashboard.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"title": "CaliperLens",
3+
"panels": [
4+
{
5+
"title": "Request Latency (p95)",
6+
"type": "graph",
7+
"targets": [
8+
{
9+
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
10+
"legendFormat": "p95"
11+
}
12+
]
13+
},
14+
{
15+
"title": "Request Rate",
16+
"type": "graph",
17+
"targets": [
18+
{
19+
"expr": "rate(http_requests_total[1m])",
20+
"legendFormat": "req/s"
21+
}
22+
]
23+
},
24+
{
25+
"title": "Error Rate",
26+
"type": "graph",
27+
"targets": [
28+
{
29+
"expr": "rate(http_requests_total{status=~\"5..\"}[1m])",
30+
"legendFormat": "5xx errors"
31+
}
32+
]
33+
}
34+
]
35+
}

0 commit comments

Comments
 (0)