forked from cleveragents/cleveragents-core
a6242c1e38
Implements comprehensive observability infrastructure per issue #940: - Add PrometheusRegistry with counters/histograms/gauges for all 14 operational metric keys; supports /metrics HTTP endpoint (server mode) and file-based export (local mode) - Add AuditBridge event subscriber that automatically writes security-relevant domain events to audit.jsonl with structured AuditRecord schema (timestamp, event_type, actor, action, resource, outcome, metadata, correlation_id, plan_id, session_id) - Add configure_structlog() for JSON or text log output with inject_correlation_id processor for log correlation across plan executions - Add observability config fields to Settings: metrics_prometheus_port, metrics_file_path, audit_jsonl_path, log_format - Add prometheus-client>=0.20.0 to project dependencies - Add 61 Behave BDD scenarios covering metrics registry, audit bridge routing, structured log output, and settings integration ISSUES CLOSED: #940
171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
"""Step definitions for JSON structlog configuration feature (Forgejo #940)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.config.settings import Settings
|
|
from cleveragents.infrastructure.observability.structlog_config import (
|
|
configure_structlog,
|
|
get_correlation_id,
|
|
inject_correlation_id,
|
|
set_correlation_id,
|
|
)
|
|
|
|
__all__: list[str] = []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — configure_structlog
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I call configure_structlog with log_format "{log_format}"')
|
|
def step_configure_structlog_format(context: Context, log_format: str) -> None:
|
|
context.configure_error = None
|
|
try:
|
|
configure_structlog(log_format=log_format)
|
|
except Exception as exc:
|
|
context.configure_error = exc
|
|
|
|
|
|
@when('I call configure_structlog with log_level "{log_level}"')
|
|
def step_configure_structlog_level(context: Context, log_level: str) -> None:
|
|
context.configure_error = None
|
|
try:
|
|
configure_structlog(log_level=log_level)
|
|
except Exception as exc:
|
|
context.configure_error = exc
|
|
|
|
|
|
@then("no exception should be raised from configure_structlog")
|
|
def step_no_exception_raised(context: Context) -> None:
|
|
error = getattr(context, "configure_error", None)
|
|
assert error is None, f"Unexpected exception: {error}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When / Then — correlation ID context variable
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I set the correlation ID to "{cid}"')
|
|
def step_set_correlation_id(context: Context, cid: str) -> None:
|
|
set_correlation_id(cid)
|
|
|
|
|
|
@when("I set the correlation ID to None")
|
|
def step_set_correlation_id_none(context: Context) -> None:
|
|
set_correlation_id(None)
|
|
|
|
|
|
@given("the correlation ID is cleared")
|
|
def step_clear_correlation_id(context: Context) -> None:
|
|
set_correlation_id(None)
|
|
|
|
|
|
@given('the correlation ID is set to "{cid}"')
|
|
def step_given_correlation_id_set(context: Context, cid: str) -> None:
|
|
set_correlation_id(cid)
|
|
|
|
|
|
@then('get_correlation_id should return "{expected}"')
|
|
def step_get_correlation_id_returns(context: Context, expected: str) -> None:
|
|
actual = get_correlation_id()
|
|
assert actual == expected, f"Expected '{expected}', got '{actual}'"
|
|
|
|
|
|
@then("get_correlation_id should return None")
|
|
def step_get_correlation_id_returns_none(context: Context) -> None:
|
|
assert get_correlation_id() is None, f"Expected None, got '{get_correlation_id()}'"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When / Then — inject_correlation_id processor
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I run the inject_correlation_id processor on an empty event dict")
|
|
def step_run_processor_empty_dict(context: Context) -> None:
|
|
context.event_dict: dict[str, Any] = {}
|
|
context.event_dict = inject_correlation_id(None, "info", context.event_dict)
|
|
|
|
|
|
@when(
|
|
'I run the inject_correlation_id processor on an event dict with correlation_id "{existing}"'
|
|
)
|
|
def step_run_processor_existing_cid(context: Context, existing: str) -> None:
|
|
context.event_dict = {"correlation_id": existing}
|
|
context.event_dict = inject_correlation_id(None, "info", context.event_dict)
|
|
|
|
|
|
@then('the event dict should contain key "{key}" with value "{value}"')
|
|
def step_event_dict_has_key_value(context: Context, key: str, value: str) -> None:
|
|
assert key in context.event_dict, (
|
|
f"Key '{key}' not found in event dict: {context.event_dict}"
|
|
)
|
|
assert context.event_dict[key] == value, (
|
|
f"Expected event_dict['{key}'] == '{value}', got '{context.event_dict[key]}'"
|
|
)
|
|
|
|
|
|
@then('the event dict should not contain key "{key}"')
|
|
def step_event_dict_missing_key(context: Context, key: str) -> None:
|
|
assert key not in context.event_dict, (
|
|
f"Key '{key}' unexpectedly found in event dict: {context.event_dict}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Settings integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I create default settings for structlog")
|
|
def step_create_default_settings(context: Context) -> None:
|
|
Settings._instance = None
|
|
context.settings = Settings(database_url="sqlite:///:memory:")
|
|
|
|
|
|
@when('I create settings with log_format "{log_format}"')
|
|
def step_create_settings_with_log_format(context: Context, log_format: str) -> None:
|
|
import os
|
|
|
|
Settings._instance = None
|
|
os.environ["CLEVERAGENTS_LOG_FORMAT"] = log_format
|
|
try:
|
|
context.settings = Settings(database_url="sqlite:///:memory:")
|
|
finally:
|
|
del os.environ["CLEVERAGENTS_LOG_FORMAT"]
|
|
|
|
|
|
@then('settings.log_format should equal "{expected}"')
|
|
def step_settings_log_format_equals(context: Context, expected: str) -> None:
|
|
assert context.settings.log_format == expected, (
|
|
f"Expected log_format '{expected}', got '{context.settings.log_format}'"
|
|
)
|
|
|
|
|
|
@then("settings.metrics_prometheus_port should be an integer")
|
|
def step_settings_prometheus_port_is_int(context: Context) -> None:
|
|
assert isinstance(context.settings.metrics_prometheus_port, int), (
|
|
f"Expected int, got {type(context.settings.metrics_prometheus_port).__name__}"
|
|
)
|
|
|
|
|
|
@then("settings.audit_jsonl_path should be a string")
|
|
def step_settings_audit_jsonl_path_is_str(context: Context) -> None:
|
|
assert isinstance(context.settings.audit_jsonl_path, str), (
|
|
f"Expected str, got {type(context.settings.audit_jsonl_path).__name__}"
|
|
)
|
|
|
|
|
|
@then("settings.metrics_file_path should be a string")
|
|
def step_settings_metrics_file_path_is_str(context: Context) -> None:
|
|
assert isinstance(context.settings.metrics_file_path, str), (
|
|
f"Expected str, got {type(context.settings.metrics_file_path).__name__}"
|
|
)
|