Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
728743c
fixes
HardMax71 Jan 8, 2026
e1fcdd9
fixes
HardMax71 Jan 8, 2026
9c2b6e8
fixes
HardMax71 Jan 8, 2026
884fb04
fixes
HardMax71 Jan 8, 2026
118bd6d
fixes
HardMax71 Jan 9, 2026
277cabf
fixes
HardMax71 Jan 9, 2026
6027ac4
fixes
HardMax71 Jan 9, 2026
7cc31fe
fixes (removed duplicate logins)
HardMax71 Jan 9, 2026
4abcb71
flaky test fix
HardMax71 Jan 9, 2026
aa3c8ac
xdist-group
HardMax71 Jan 9, 2026
5489e39
pyproject fix
HardMax71 Jan 9, 2026
93a79e6
durations=0 for tests: checking which ones take most time
HardMax71 Jan 9, 2026
bc944e1
optimizations of sse routes tests
HardMax71 Jan 9, 2026
48aa71f
optimizations of sse routes tests
HardMax71 Jan 9, 2026
824d686
security service added to DI, simplified settings passing
HardMax71 Jan 9, 2026
e88e606
more DI for settings obj, also removed random bootstrap_servers being…
HardMax71 Jan 9, 2026
72a7733
mypy
HardMax71 Jan 9, 2026
2cb4d4d
removed env block from toml file, deps updated accordingly
HardMax71 Jan 9, 2026
9a22a4c
Otel transient error fix
HardMax71 Jan 9, 2026
b3cdef8
Otel transient error fix + DI for metrics
HardMax71 Jan 9, 2026
4d78cc1
other fixes
HardMax71 Jan 9, 2026
57fd0b1
other fixes
HardMax71 Jan 9, 2026
8120957
mypy fixes part 1
HardMax71 Jan 9, 2026
a3907bb
mypy fixes part 2
HardMax71 Jan 9, 2026
47d1215
mypy fixes part 3
HardMax71 Jan 9, 2026
8cde784
mypy fixes part 5; also added csrf middleware
HardMax71 Jan 9, 2026
5796a27
less fixtures, passing directly xx_user fixture as logged in user of …
HardMax71 Jan 9, 2026
726e2f9
settings fixes (lifespan now reads from DI)
HardMax71 Jan 9, 2026
f1109d5
new sse event (subscribed), updated tests, dlq retry msgs moved to ma…
HardMax71 Jan 10, 2026
4452603
fixes
HardMax71 Jan 10, 2026
a202287
fixes
HardMax71 Jan 10, 2026
c6837fc
fixes
HardMax71 Jan 10, 2026
e67ef8a
fixes
HardMax71 Jan 10, 2026
1016bf7
single source of truth regarding loading Settings - DI (not get_setti…
HardMax71 Jan 10, 2026
d375e5e
fixes + parallel execution
HardMax71 Jan 10, 2026
2f1a020
non-flaky tests
HardMax71 Jan 10, 2026
225c737
fixes
HardMax71 Jan 10, 2026
a5a1f99
fixes
HardMax71 Jan 10, 2026
9ff0f12
fixes
HardMax71 Jan 10, 2026
2f0ee35
fixes
HardMax71 Jan 10, 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
3 changes: 3 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
run: |
cd backend
uv run pytest tests/unit -v -rs \
--durations=0 \
--cov=app \
--cov-report=xml --cov-report=term

Expand Down Expand Up @@ -116,6 +117,7 @@ jobs:
run: |
cd backend
uv run pytest tests/integration -v -rs \
--durations=0 \
--cov=app \
--cov-report=xml --cov-report=term

Expand Down Expand Up @@ -210,6 +212,7 @@ jobs:
run: |
cd backend
uv run pytest tests/e2e -v -rs \
--durations=0 \
--cov=app \
--cov-report=xml --cov-report=term

Expand Down
3 changes: 3 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ WEB_BACKLOG=2048
# When running uvicorn locally (outside Docker), bind to IPv4 loopback to avoid
# IPv6-only localhost resolution on some Linux distros.
SERVER_HOST=127.0.0.1

# Security
BCRYPT_ROUNDS=12
9 changes: 3 additions & 6 deletions backend/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ PROJECT_NAME=integr8scode
DATABASE_NAME=integr8scode_test
API_V1_STR=/api/v1
SECRET_KEY=test-secret-key-for-testing-only-32chars!!
ENVIRONMENT=testing
TESTING=true

# MongoDB - use localhost for tests
Expand All @@ -27,18 +26,16 @@ SCHEMA_REGISTRY_URL=http://localhost:8081

# Security
SECURE_COOKIES=true
CORS_ALLOWED_ORIGINS=["http://localhost:3000","https://localhost:3000"]
BCRYPT_ROUNDS=4

# Features
RATE_LIMIT_ENABLED=true
ENABLE_TRACING=false

# OpenTelemetry - explicitly disabled for tests (no endpoint = NoOp meter)
# OpenTelemetry - explicitly disabled for tests
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_METRICS_EXPORTER=none
OTEL_TRACES_EXPORTER=none
OTEL_LOGS_EXPORTER=none

# Development
DEVELOPMENT_MODE=false
LOG_LEVEL=INFO
ENVIRONMENT=test
3 changes: 2 additions & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ done
echo "Starting application..."
[ -f /app/kubeconfig.yaml ] && export KUBECONFIG=/app/kubeconfig.yaml

exec gunicorn app.main:app \
exec gunicorn 'app.main:create_app' \
--factory \
-k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:443 \
--workers ${WEB_CONCURRENCY:-4} \
Expand Down
9 changes: 5 additions & 4 deletions backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from fastapi.security import OAuth2PasswordRequestForm
from pymongo.errors import DuplicateKeyError

from app.core.security import security_service
from app.core.security import SecurityService
from app.core.utils import get_client_ip
from app.db.repositories import UserRepository
from app.domain.user import DomainUserCreate
Expand All @@ -19,7 +19,7 @@
UserResponse,
)
from app.services.auth_service import AuthService
from app.settings import get_settings
from app.settings import Settings

router = APIRouter(prefix="/auth", tags=["authentication"], route_class=DishkaRoute)

Expand All @@ -29,6 +29,8 @@ async def login(
request: Request,
response: Response,
user_repo: FromDishka[UserRepository],
security_service: FromDishka[SecurityService],
settings: FromDishka[Settings],
logger: FromDishka[logging.Logger],
form_data: OAuth2PasswordRequestForm = Depends(),
) -> LoginResponse:
Expand Down Expand Up @@ -74,8 +76,6 @@ async def login(
headers={"WWW-Authenticate": "Bearer"},
)

settings = get_settings()

logger.info(
"Login successful",
extra={
Expand Down Expand Up @@ -127,6 +127,7 @@ async def register(
request: Request,
user: UserCreate,
user_repo: FromDishka[UserRepository],
security_service: FromDishka[SecurityService],
logger: FromDishka[logging.Logger],
) -> UserResponse:
logger.info(
Expand Down
6 changes: 3 additions & 3 deletions backend/app/api/routes/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from app.schemas_pydantic.user import UserResponse
from app.services.event_service import EventService
from app.services.kafka_event_service import KafkaEventService
from app.settings import get_settings
from app.settings import Settings

router = APIRouter(prefix="/events", tags=["events"], route_class=DishkaRoute)

Expand Down Expand Up @@ -229,8 +229,8 @@ async def publish_custom_event(
event_request: PublishEventRequest,
request: Request,
event_service: FromDishka[KafkaEventService],
settings: FromDishka[Settings],
) -> PublishEventResponse:
settings = get_settings()
base_meta = EventMetadata(
service_name=settings.SERVICE_NAME,
service_version=settings.SERVICE_VERSION,
Expand Down Expand Up @@ -311,6 +311,7 @@ async def replay_aggregate_events(
admin: Annotated[UserResponse, Depends(admin_user)],
event_service: FromDishka[EventService],
kafka_event_service: FromDishka[KafkaEventService],
settings: FromDishka[Settings],
logger: FromDishka[logging.Logger],
target_service: str | None = Query(None, description="Service to replay events to"),
dry_run: bool = Query(True, description="If true, only show what would be replayed"),
Expand Down Expand Up @@ -339,7 +340,6 @@ async def replay_aggregate_events(
await asyncio.sleep(0.1)

try:
settings = get_settings()
meta = EventMetadata(
service_name=settings.SERVICE_NAME,
service_version=settings.SERVICE_VERSION,
Expand Down
4 changes: 2 additions & 2 deletions backend/app/api/routes/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from app.services.execution_service import ExecutionService
from app.services.idempotency import IdempotencyManager
from app.services.kafka_event_service import KafkaEventService
from app.settings import get_settings
from app.settings import Settings

router = APIRouter(route_class=DishkaRoute)

Expand Down Expand Up @@ -162,6 +162,7 @@ async def cancel_execution(
current_user: Annotated[UserResponse, Depends(current_user)],
cancel_request: CancelExecutionRequest,
event_service: FromDishka[KafkaEventService],
settings: FromDishka[Settings],
) -> CancelResponse:
# Handle terminal states
terminal_states = [ExecutionStatus.COMPLETED, ExecutionStatus.FAILED, ExecutionStatus.TIMEOUT]
Expand All @@ -178,7 +179,6 @@ async def cancel_execution(
event_id="-1", # exact event_id unknown
)

settings = get_settings()
payload = {
"execution_id": execution.execution_id,
"status": str(ExecutionStatus.CANCELLED),
Expand Down
9 changes: 3 additions & 6 deletions backend/app/core/adaptive_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import threading
import time
from collections import deque
from typing import Any, Sequence, Tuple
from typing import Sequence, Tuple

from opentelemetry.context import Context
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
from opentelemetry.util.types import Attributes

from app.settings import get_settings
from app.settings import Settings


class AdaptiveSampler(Sampler):
Expand Down Expand Up @@ -239,11 +239,8 @@ def shutdown(self) -> None:
self._adjustment_thread.join(timeout=5.0)


def create_adaptive_sampler(settings: Any | None = None) -> AdaptiveSampler:
def create_adaptive_sampler(settings: Settings) -> AdaptiveSampler:
"""Create adaptive sampler with settings"""
if settings is None:
settings = get_settings()

return AdaptiveSampler(
base_rate=settings.TRACING_SAMPLING_RATE,
min_rate=max(0.001, settings.TRACING_SAMPLING_RATE / 100), # 1/100th of base
Expand Down
1 change: 1 addition & 0 deletions backend/app/core/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def create_app_container(settings: Settings) -> AsyncContainer:
RepositoryProvider(),
MessagingProvider(),
EventProvider(),
SagaOrchestratorProvider(),
KafkaServicesProvider(),
SSEProvider(),
AuthProvider(),
Expand Down
39 changes: 23 additions & 16 deletions backend/app/core/dishka_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,32 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
# Metrics setup moved to app creation to allow middleware registration
logger.info("Lifespan start: tracing and services initialization")

# Initialize tracing
instrumentation_report = init_tracing(
service_name=settings.TRACING_SERVICE_NAME,
logger=logger,
service_version=settings.TRACING_SERVICE_VERSION,
sampling_rate=settings.TRACING_SAMPLING_RATE,
enable_console_exporter=settings.TESTING,
adaptive_sampling=settings.TRACING_ADAPTIVE_SAMPLING,
)

if instrumentation_report.has_failures():
logger.warning(
"Some instrumentation libraries failed to initialize",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
# Initialize tracing only when enabled (avoid exporter retries in tests)
if settings.ENABLE_TRACING and not settings.TESTING:
instrumentation_report = init_tracing(
service_name=settings.TRACING_SERVICE_NAME,
settings=settings,
logger=logger,
service_version=settings.TRACING_SERVICE_VERSION,
sampling_rate=settings.TRACING_SAMPLING_RATE,
enable_console_exporter=settings.TESTING,
adaptive_sampling=settings.TRACING_ADAPTIVE_SAMPLING,
)

if instrumentation_report.has_failures():
logger.warning(
"Some instrumentation libraries failed to initialize",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
)
else:
logger.info(
"Distributed tracing initialized successfully",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
)
else:
logger.info(
"Distributed tracing initialized successfully",
extra={"instrumentation_summary": instrumentation_report.get_summary()},
"Distributed tracing disabled",
extra={"testing": settings.TESTING, "enable_tracing": settings.ENABLE_TRACING},
)

# Initialize schema registry once at startup
Expand Down
21 changes: 9 additions & 12 deletions backend/app/core/metrics/base.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,54 @@
from dataclasses import dataclass
from typing import Optional

from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.metrics import Meter, NoOpMeterProvider
from opentelemetry.sdk.metrics import MeterProvider as SdkMeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource

from app.settings import get_settings
from app.settings import Settings


@dataclass
class MetricsConfig:
service_name: str = "integr8scode-backend"
service_version: str = "1.0.0"
otlp_endpoint: Optional[str] = None
otlp_endpoint: str | None = None
export_interval_millis: int = 10000
console_export_interval_millis: int = 60000


class BaseMetrics:
def __init__(self, meter_name: str | None = None):
def __init__(self, settings: Settings, meter_name: str | None = None):
"""Initialize base metrics with its own meter.
Args:
settings: Application settings.
meter_name: Optional name for the meter. Defaults to class name.
"""
# Get settings and create config
settings = get_settings()
config = MetricsConfig(
service_name=settings.TRACING_SERVICE_NAME or "integr8scode-backend",
service_version="1.0.0",
otlp_endpoint=settings.OTEL_EXPORTER_OTLP_ENDPOINT,
)

# Each collector creates its own independent meter
meter_name = meter_name or self.__class__.__name__
self._meter = self._create_meter(config, meter_name)
self._meter = self._create_meter(settings, config, meter_name)
self._create_instruments()

def _create_meter(self, config: MetricsConfig, meter_name: str) -> Meter:
def _create_meter(self, settings: Settings, config: MetricsConfig, meter_name: str) -> Meter:
"""Create a new meter instance for this collector.
Args:
settings: Application settings
config: Metrics configuration
meter_name: Name for this meter
Returns:
A new meter instance
"""
# If tracing/metrics disabled or no OTLP endpoint configured, use NoOp meter to avoid threads/network
settings = get_settings()
if not settings.ENABLE_TRACING or not config.otlp_endpoint:
# If tracing/metrics disabled or no OTLP endpoint configured, use NoOp meter
if not config.otlp_endpoint:
return NoOpMeterProvider().get_meter(meter_name)

resource = Resource.create(
Expand Down
17 changes: 8 additions & 9 deletions backend/app/core/metrics/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,20 @@ def __init__(self, name: str, metric_class: Type[T], logger: logging.Logger) ->

def get(self) -> T:
"""
Get the metric from context, creating it if necessary.

This method implements lazy initialization - if no metric exists
in the current context, it creates one. This is useful for testing
and standalone scripts where the context might not be initialized.
Get the metric from context.

Returns:
The metric instance for the current context

Raises:
RuntimeError: If metrics not initialized via DI
"""
metric = self._context_var.get()
if metric is None:
# Lazy initialization with logging
self.logger.debug(f"Lazy initializing {self._name} metrics in context")
metric = self._metric_class()
self._context_var.set(metric)
raise RuntimeError(
f"{self._name} metrics not initialized. "
"Ensure MetricsContext.initialize_all() is called during app startup."
)
return metric

def set(self, metric: T) -> contextvars.Token[Optional[T]]:
Expand Down
Loading
Loading