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
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
123 lines
4.0 KiB
Python
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)
|