Files
CoreRasurae 3e3e9b4b5d
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 18s
CI / e2e_tests (pull_request) Successful in 30s
CI / security (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 40s
CI / unit_tests (pull_request) Successful in 3m19s
CI / integration_tests (pull_request) Successful in 3m36s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 5m38s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 19s
CI / build (push) Successful in 15s
CI / security (push) Successful in 38s
CI / e2e_tests (push) Successful in 28s
CI / typecheck (push) Successful in 42s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 3m14s
CI / docker (push) Successful in 10s
CI / integration_tests (push) Successful in 3m33s
CI / coverage (push) Successful in 6m3s
CI / benchmark-publish (push) Successful in 19m14s
CI / benchmark-regression (pull_request) Successful in 36m42s
feat(observability): wire AuditService.record() into domain services via EventBus auto-dispatch
Implements AuditEventSubscriber that subscribes to all 9 security-relevant
EventType members and persists redacted audit entries via AuditService.record().

Key components:
- AuditEventSubscriber: bridges EventBus and AuditService (SEC7)
- SECURITY_EVENT_MAP: maps EventType enum to audit type strings
- Redaction via redact_dict() on event details before persistence
- Graceful error handling: failures logged, never propagated

Post-review fixes applied:
- BUG-1: Remove dead correlation_id null-check guard (DomainEvent.correlation_id
  is always non-None via ULID default_factory)
- SEC-2: Redact exception messages in warning logs via redact_value() to prevent
  potential leakage of sensitive internal state (e.g. DB connection strings)
- PERF-3: Pre-generate unique DomainEvent instances in ASV benchmark setup to
  avoid skew from reusing a single frozen object
- Wire event_bus from the DI container into CorrectionService (plan.py),
  ConfigService (config.py, skill.py x2, server.py), and
  PersistentSessionService (session.py) at their CLI construction sites.

Closes #581
2026-03-12 23:32:48 +00:00

123 lines
4.0 KiB
Python

"""ASV benchmarks for audit service write throughput.
Measures the overhead of the AuditEventSubscriber pipeline:
EventBus.emit() -> redact_dict() -> AuditService.record() -> SQLite.
"""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
from typing import ClassVar
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from sqlalchemy import create_engine # noqa: E402
from sqlalchemy.orm import sessionmaker # noqa: E402
from cleveragents.application.services.audit_event_subscriber import ( # noqa: E402
AuditEventSubscriber,
)
from cleveragents.application.services.audit_service import AuditService # noqa: E402
from cleveragents.config.settings import Settings # noqa: E402
from cleveragents.infrastructure.database.models import Base # noqa: E402
from cleveragents.infrastructure.events.models import DomainEvent # noqa: E402
from cleveragents.infrastructure.events.reactive import ( # noqa: E402
ReactiveEventBus,
)
from cleveragents.infrastructure.events.types import EventType # noqa: E402
def _make_audit_service() -> AuditService:
"""Create an in-memory AuditService for benchmarking."""
Settings._instance = None
settings = Settings(database_url="sqlite:///:memory:")
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine)()
return AuditService(settings=settings, session=session)
class AuditServiceBenchmarks:
"""Benchmarks for audit event recording throughput."""
timeout = 120
params: ClassVar[list[int]] = [1, 10, 100, 1000]
param_names: ClassVar[list[str]] = ["num_events"]
def setup(self, num_events: int) -> None:
# Recreate fresh DB/bus per ASV timing round to prevent row
# accumulation across repetitions (PERF-4).
self.svc = _make_audit_service()
self.bus = ReactiveEventBus()
self.subscriber = AuditEventSubscriber(
audit_service=self.svc,
event_bus=self.bus,
)
# Pre-generate unique events so each iteration has a distinct
# timestamp and correlation_id, avoiding benchmark skew from
# reusing a single frozen object (PERF-3).
self.events = [
DomainEvent(
event_type=EventType.PLAN_APPLIED,
plan_id=f"BENCH-{i:04d}",
details={"action_name": "local/benchmark"},
)
for i in range(num_events)
]
def time_record_events(self, num_events: int) -> None:
for event in self.events:
self.bus.emit(event)
class AuditDirectRecordBenchmarks:
"""Benchmarks for direct AuditService.record() calls."""
timeout = 120
params: ClassVar[list[int]] = [1, 10, 100, 1000]
param_names: ClassVar[list[str]] = ["num_events"]
def setup(self, num_events: int) -> None:
self.svc = _make_audit_service()
def time_direct_record(self, num_events: int) -> None:
for _ in range(num_events):
self.svc.record(
event_type="plan_applied",
plan_id="BENCH-001",
details={"action_name": "local/benchmark"},
)
class AuditRedactionBenchmarks:
"""Benchmarks for the redaction overhead in audit recording."""
timeout = 120
def setup(self) -> None:
self.svc = _make_audit_service()
self.bus = ReactiveEventBus()
self.subscriber = AuditEventSubscriber(
audit_service=self.svc,
event_bus=self.bus,
)
self.event_with_secrets = DomainEvent(
event_type=EventType.CONFIG_CHANGED,
details={
"api_key": "sk-proj-ABCDEFGHIJ1234567890",
"token": "tok_ABCDEF1234567890",
"normal_field": "safe-value",
},
)
def time_record_with_redaction(self) -> None:
self.bus.emit(self.event_with_secrets)