Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dcacbbc
feat(telemetry): Implement database instrumentation suppression for L…
vprashrex Aug 14, 2026
d020fa5
feat(telemetry): Implement DB span filtering for Sentry in SQLAlchemy…
vprashrex Aug 14, 2026
48f47e4
feat(tests): Add unit tests for LLM/response Celery task wrappers and…
vprashrex Aug 14, 2026
b8ee39d
feat(tests): Simplify patching of SQLAlchemyInstrumentor in DB engine…
vprashrex Aug 14, 2026
86efee9
Merge branch 'main' into feat/http-db-observability-metrics
vprashrex Aug 23, 2026
c0d1171
feat(telemetry): Enhance database observability with slow query and c…
vprashrex Aug 23, 2026
71ad9e4
Enhance observability with OTel and Sentry integration
vprashrex Aug 24, 2026
760b0bb
Merge branch 'main' into feat/http-db-observability-metrics
vprashrex Aug 24, 2026
1f8c56c
Merge branch 'main' into feat/http-db-observability-metrics
vprashrex Aug 25, 2026
9f16b94
Merge remote-tracking branch 'origin/main' into feat/http-db-observab…
vprashrex Sep 8, 2026
0ccf189
refactor(telemetry): split log context from Sentry user binding
vprashrex Sep 8, 2026
8633a44
feat(observability): keep genai content out of Sentry
vprashrex Sep 8, 2026
9ff8108
Merge branch 'main' into feat/http-db-observability-metrics
vprashrex Oct 6, 2026
465ffd1
refactor(telemetry): split telemetry into a package
vprashrex Oct 6, 2026
43b0b05
Refactor telemetry metrics and tracing setup
vprashrex Oct 6, 2026
b250cbe
feat(telemetry): use FastAPI instrumentation fully, forward root-span…
vprashrex Oct 6, 2026
bd2df34
chore(finetune): drop unrelated log tweak in unmaintained evaluation …
vprashrex Oct 6, 2026
a18dc8c
chore(sentry): pin only the GenAI integrations for SDKs we use
vprashrex Oct 6, 2026
6bffe53
feat(sentry): refine before_send filters and update SENTRY_RELEASE ha…
vprashrex Oct 6, 2026
c60802b
fix(callback): change log level from warning to error for callback fa…
vprashrex Oct 6, 2026
56d553e
feat(telemetry): update span attributes and Sentry tags for improved …
vprashrex Oct 7, 2026
6ab5445
feat(telemetry): enhance observability metrics and logging for Celery…
vprashrex Oct 7, 2026
f78064a
refactor(metrics): remove unused process RSS monitoring functions and…
vprashrex Oct 9, 2026
d0f5a22
feat(telemetry): reorder and refine observability metrics and logging…
vprashrex Oct 9, 2026
16de284
feat(telemetry): enable observability metrics based on OTEL_ENABLED s…
vprashrex Oct 10, 2026
22e5220
feat(telemetry): update span attributes and Sentry tags for improved …
vprashrex Oct 10, 2026
17da0b9
feat(telemetry): enhance LLM task kwargs redaction and improve callba…
vprashrex Oct 10, 2026
3bb76e7
feat(telemetry): enhance logging context with module names and improv…
vprashrex Oct 11, 2026
e705054
feat(observability): add architecture overview documentation for trac…
vprashrex Oct 12, 2026
a87e283
feat(observability): integrate Sentry with Celery for enhanced task m…
vprashrex Oct 12, 2026
84f8187
Merge branch 'main' into feat/http-db-observability-metrics
vprashrex Oct 12, 2026
3c78303
feat(observability): remove unnecessary logging statements to enhance…
vprashrex Oct 12, 2026
b7fb5bb
feat(observability): update logging fields for improved Sentry integr…
vprashrex Oct 13, 2026
0603117
Merge branch 'main' into feat/http-db-observability-metrics
vprashrex Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions backend/app/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from app.core.config import settings
from app.core.db import engine
from app.core.security import api_key_manager
from app.core.telemetry import set_request_log_context
from app.core.telemetry import bind_sentry_user, set_request_log_context
from app.crud.organization import validate_organization
from app.crud.project import validate_project
from app.models import (
Expand Down Expand Up @@ -45,13 +45,7 @@ def get_db() -> Generator[Session, None, None]:


def _set_tenant_span_attributes(auth_context: AuthContext) -> None:
"""Tag the active OTel span and log context with tenant info after auth.

Sets org/project on:
- OTel span → Sentry traces filterable by tenant
- log context → every log record in this request carries org_id/project_id
- Sentry scope → tags on all events for this request
"""
"""Stamp org/project on the span, log context and Sentry scope after auth."""
span = trace.get_current_span()
if span.is_recording():
span.set_attribute("user.id", str(auth_context.user.id))
Expand All @@ -60,10 +54,10 @@ def _set_tenant_span_attributes(auth_context: AuthContext) -> None:
if auth_context.project:
span.set_attribute("tenant.project_id", auth_context.project.id)

set_request_log_context(
org_id=auth_context.organization.id if auth_context.organization else None,
project_id=auth_context.project.id if auth_context.project else None,
)
org_id = auth_context.organization.id if auth_context.organization else None
project_id = auth_context.project.id if auth_context.project else None
set_request_log_context(org_id=org_id, project_id=project_id)
bind_sentry_user(user_id=auth_context.user.id, org_id=org_id, project_id=project_id)


def _authenticate_with_jwt(session: Session, token: str) -> AuthContext:
Expand Down
50 changes: 20 additions & 30 deletions backend/app/celery/celery_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@

from celery import Celery
from celery.signals import (
worker_init,
setup_logging,
task_failure,
task_postrun,
task_prerun,
setup_logging,
worker_init,
worker_process_init,
)
from kombu import Exchange, Queue

from app.core.config import settings
from app.core.logger import configure_logging
from app.core.sentry_filters import before_send_filter, before_send_transaction_filter

logger = logging.getLogger(__name__)
_telemetry_initialized = False
Expand All @@ -35,37 +34,25 @@ def _initialize_worker_observability() -> None:
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.httpx import HttpxIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration

sentry_sdk.init(
dsn=str(settings.SENTRY_DSN),
environment=settings.ENVIRONMENT,
release=settings.API_VERSION,
instrumenter="otel",
traces_sample_rate=1.0,
max_request_body_size="never",
enable_logs=True,
before_send=before_send_filter,
before_send_transaction=before_send_transaction_filter,
from app.core.telemetry import init_sentry

init_sentry(
integrations=[
LoggingIntegration(
level=logging.INFO,
sentry_logs_level=logging.INFO,
),
CeleryIntegration(
propagate_traces=False,
monitor_beat_tasks=False,
),
],
disabled_integrations=[
SqlalchemyIntegration(),
HttpxIntegration(),
CeleryIntegration(propagate_traces=True, monitor_beat_tasks=False)
],
disabled_integrations=[SqlalchemyIntegration(), HttpxIntegration()],
)

@task_prerun.connect(weak=False)
def _tag_sentry_task(task: "celery.Task", **_: object) -> None: # type: ignore[name-defined]
"""Tag the task's Sentry scope with its name so Issues can be filtered by task."""
sentry_sdk.set_tag("celery.task_name", task.name)

_sentry_initialized = True

if not _telemetry_initialized:
if settings.OTEL_ENABLED and not _telemetry_initialized:
from app.core.telemetry import flush_telemetry, setup_telemetry

setup_telemetry(service_name="kaapi-celery")
Expand All @@ -88,9 +75,10 @@ def log_pool_status(task: "celery.Task", **_: object) -> None: # type: ignore[n
If checked_out equals pool size right when a task starts, connections
are being held across tasks (likely across LLM API calls) and leaking.
"""
from app.core.db import engine
from sqlalchemy.pool import QueuePool

from app.core.db import engine

pool = engine.pool
if isinstance(pool, QueuePool):
logger.info(
Expand All @@ -106,9 +94,10 @@ def log_pool_status_post(task: "celery.Task", **_: object) -> None: # type: ign
Compare with task_prerun log — if checked_out is the same or higher after
the task, connections were not returned and are leaking.
"""
from app.core.db import engine
from sqlalchemy.pool import QueuePool

from app.core.db import engine

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see, this initialise multiple time so why do you not directly import this at top level.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any circular dependency issue?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No circular import. It stays inside the function on purpose: importing app.core.db at module level would create the DB engine in the Celery main process before fork, and every forked worker would share that connection pool.


pool = engine.pool
if isinstance(pool, QueuePool):
logger.info(
Expand All @@ -126,9 +115,10 @@ def log_pool_status_failure(
Failures are the most likely path for sessions to leak — exceptions can
bypass session cleanup if not guarded by a context manager.
"""
from app.core.db import engine
from sqlalchemy.pool import QueuePool

from app.core.db import engine

pool = engine.pool
if isinstance(pool, QueuePool):
logger.warning(
Expand Down
26 changes: 16 additions & 10 deletions backend/app/celery/tasks/job_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from app.celery.celery_app import celery_app
from app.celery.utils import gevent_timeout
from app.core.config import settings
from app.core.telemetry import LogModuleEnum, log_context

if TYPE_CHECKING:
from app.services.notifications.eval_completion import (
Expand All @@ -41,6 +42,8 @@
# app/core/logger.py and app/celery/utils.py).
DEFAULT_TRACE_ID = "N/A"

SENTRY_NO_PROPAGATE_HEADERS: dict[str, bool] = {"sentry-propagate-traces": False}


def _set_trace(trace_id: str) -> None:
correlation_id.set(trace_id)
Expand Down Expand Up @@ -403,6 +406,7 @@ def run_assessment_api_batch(
"trace_id": trace_id,
},
countdown=POLL_COUNTDOWN_SECONDS,
headers=SENTRY_NO_PROPAGATE_HEADERS.copy(),
)
return result

Expand Down Expand Up @@ -451,12 +455,13 @@ def run_evaluation_fast_chunk(
f"task_id={current_task.request.id}"
)

return _run_with_otel_parent(
self,
lambda: execute_fast_evaluation_chunk(
eval_run_id=eval_run_id, chunk_index=chunk_index
),
)
with log_context(module=LogModuleEnum.FASTEVALS):
return _run_with_otel_parent(
self,
lambda: execute_fast_evaluation_chunk(
eval_run_id=eval_run_id, chunk_index=chunk_index
),
)


@celery_app.task(bind=True, queue="default", priority=6)
Expand All @@ -478,10 +483,11 @@ def run_evaluation_fast_aggregate(
f"eval_run_id={eval_run_id} | task_id={current_task.request.id}"
)

return _run_with_otel_parent(
self,
lambda: execute_fast_evaluation_aggregate(eval_run_id=eval_run_id),
)
with log_context(module=LogModuleEnum.FASTEVALS):
return _run_with_otel_parent(
self,
lambda: execute_fast_evaluation_aggregate(eval_run_id=eval_run_id),
)


# Priority 6 (fast-eval tier): same band as run_prompt_improvement — one graph
Expand Down
7 changes: 7 additions & 0 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from pydantic import (
EmailStr,
Field,
HttpUrl,
PostgresDsn,
computed_field,
Expand Down Expand Up @@ -158,6 +159,12 @@ def AWS_S3_BUCKET(self) -> str:
BACKEND_SERVICE_NAME: str = "kaapi-backend"
CRON_SERVICE_NAME: str = "kaapi-cron"

SENTRY_TRACES_SAMPLE_RATE: float = Field(default=1.0, ge=0.0, le=1.0)
SENTRY_SEND_DEFAULT_PII: bool = False
SENTRY_ERROR_SAMPLE_RATE: float = Field(default=1.0, ge=0.0, le=1.0)
SENTRY_PROFILE_SESSION_SAMPLE_RATE: float = Field(default=1.0, ge=0.0, le=1.0)
SENTRY_PROFILE_LIFECYCLE: Literal["manual", "trace"] = "trace"

# Threshold Request Rate per minute
THRESHOLD_LLM_CALL_RATE: int = 15
THRESHOLD_COLLECTIONS_RATE: int = 3
Expand Down
4 changes: 3 additions & 1 deletion backend/app/core/db.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlmodel import Session, create_engine, select

from app import crud
from app.core.config import settings
from app.core.telemetry import instrument_db_engine
from app.models import User, UserCreate

Expand Down Expand Up @@ -33,7 +34,8 @@ def get_engine():

# Create a default engine for backward compatibility
engine = get_engine()
instrument_db_engine(engine)
if settings.OTEL_ENABLED:
instrument_db_engine(engine)


# make sure all SQLModel models are imported (app.models) before initializing DB
Expand Down
2 changes: 2 additions & 0 deletions backend/app/core/exception_handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from collections import defaultdict

import sentry_sdk
from fastapi import FastAPI, Request, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
Expand Down Expand Up @@ -109,6 +110,7 @@ async def http_exception_handler(

@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
sentry_sdk.capture_exception(exc)
return JSONResponse(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
content=APIResponse.failure_response(
Expand Down
3 changes: 0 additions & 3 deletions backend/app/core/langfuse/langfuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,6 @@ def wrapper(
completion_config: NativeCompletionConfig, query: QueryParams, **kwargs
):
if not credentials:
logger.info(
"[observe_llm_execution] No credentials - skipping observability"
)
Comment thread
vprashrex marked this conversation as resolved.
return func(completion_config, query, **kwargs)

try:
Expand Down
6 changes: 5 additions & 1 deletion backend/app/core/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from asgi_correlation_id import correlation_id

from app.core.config import settings
from app.core.telemetry.context import LogContextFilter

LOG_DIR = settings.LOG_DIR
os.makedirs(LOG_DIR, exist_ok=True)
Expand All @@ -17,7 +18,7 @@
LOGGING_LEVEL = logging.INFO
LOGGING_FORMAT = (
"%(asctime)s - [%(service_name)s] - [%(correlation_id)s] - "
"%(levelname)s - %(name)s - %(message)s"
"%(levelname)s - %(_log_location)s - %(message)s"
)
_service_name_context: ContextVar[str | None] = ContextVar(
"kaapi_service_name", default=None
Expand Down Expand Up @@ -102,14 +103,17 @@ def configure_logging(service_name: str | None = None) -> None:
stream_handler.setFormatter(formatter)
stream_handler.addFilter(CorrelationIdFilter())
stream_handler.addFilter(ServiceNameFilter(resolved_service_name))
stream_handler.addFilter(LogContextFilter())

file_handler = RotatingFileHandler(
LOG_FILE_PATH, maxBytes=10 * 1024 * 1024, backupCount=5
)
file_handler.setFormatter(formatter)
file_handler.addFilter(CorrelationIdFilter())
file_handler.addFilter(ServiceNameFilter(resolved_service_name))
file_handler.addFilter(LogContextFilter())

root_logger.addFilter(LogContextFilter())
root_logger.handlers.clear()
root_logger.addHandler(stream_handler)
root_logger.addHandler(file_handler)
Expand Down
Loading
Loading