agent-governance/tests/governance/test_phase12_observability.py
profit 8c6e7831e9 Add Phase 10-12 implementation: multi-tenant, marketplace, observability
Major additions:
- marketplace/: Agent template registry with FTS5 search, ratings, versioning
- observability/: Prometheus metrics, distributed tracing, structured logging
- ledger/migrations/: Database migration scripts for multi-tenant support
- tests/governance/: 15 new test files for phases 6-12 (295 total tests)
- bin/validate-phases: Full 12-phase validation script

New features:
- Multi-tenant support with tenant isolation and quota enforcement
- Agent marketplace with semantic versioning and search
- Observability with metrics, tracing, and log correlation
- Tier-1 agent bootstrap scripts

Updated components:
- ledger/api.py: Extended API for tenants, marketplace, observability
- ledger/schema.sql: Added tenant, project, marketplace tables
- testing/framework.ts: Enhanced test framework
- checkpoint/checkpoint.py: Improved checkpoint management

Archived:
- External integrations (Slack/GitHub/PagerDuty) moved to .archive/
- Old checkpoint files cleaned up

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 18:39:47 -05:00

425 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Phase 12: Observability Tests
=============================
Validates metrics, tracing, and logging systems.
"""
import json
import sqlite3
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
DB_PATH = Path("/opt/agent-governance/ledger/governance.db")
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
# =============================================================================
# Metrics Tests
# =============================================================================
def test_metrics_module_exists():
"""Verify metrics module exists"""
metrics_path = Path("/opt/agent-governance/observability/metrics.py")
assert metrics_path.exists(), "Metrics module not found"
content = metrics_path.read_text()
assert "Counter" in content, "Counter class not found"
assert "Gauge" in content, "Gauge class not found"
assert "Histogram" in content, "Histogram class not found"
assert "to_prometheus" in content, "Prometheus export not found"
print("[PASS] metrics module exists with Counter, Gauge, Histogram")
def test_metrics_registry_exists():
"""Verify metrics registry can be imported"""
try:
from observability.metrics import registry
assert registry is not None, "Registry is None"
print("[PASS] metrics registry can be imported")
except ImportError as e:
raise AssertionError(f"Failed to import metrics registry: {e}")
def test_counter_increment():
"""Test counter increment functionality"""
from observability.metrics import Counter
counter = Counter(name="test_counter", help="Test counter", labels=["status"])
counter.inc({"status": "success"})
counter.inc({"status": "success"})
counter.inc({"status": "error"})
assert counter.values[("success",)] == 2, "Counter increment failed"
assert counter.values[("error",)] == 1, "Counter increment failed"
print("[PASS] counter increment works")
def test_gauge_set():
"""Test gauge set functionality"""
from observability.metrics import Gauge
gauge = Gauge(name="test_gauge", help="Test gauge", labels=["component"])
gauge.set(42, {"component": "database"})
gauge.set(100, {"component": "cache"})
assert gauge.values[("database",)] == 42, "Gauge set failed"
assert gauge.values[("cache",)] == 100, "Gauge set failed"
print("[PASS] gauge set works")
def test_histogram_observe():
"""Test histogram observe functionality"""
from observability.metrics import Histogram
hist = Histogram(
name="test_histogram",
help="Test histogram",
labels=["endpoint"],
buckets=[0.1, 0.5, 1.0, 5.0]
)
hist.observe(0.05, {"endpoint": "/api"})
hist.observe(0.3, {"endpoint": "/api"})
hist.observe(2.0, {"endpoint": "/api"})
observations = hist.observations[("/api",)]
assert len(observations) == 3, "Histogram observe failed"
assert 0.05 in observations, "Missing observation"
print("[PASS] histogram observe works")
def test_prometheus_format():
"""Test Prometheus format output"""
from observability.metrics import Counter
counter = Counter(name="http_requests_total", help="Total HTTP requests", labels=["method", "status"])
counter.inc({"method": "GET", "status": "200"}, 10)
output = counter.to_prometheus()
assert "# HELP http_requests_total" in output, "Missing HELP line"
assert "# TYPE http_requests_total counter" in output, "Missing TYPE line"
assert 'method="GET"' in output, "Missing label"
assert "10" in output, "Missing value"
print("[PASS] Prometheus format output works")
# =============================================================================
# Tracing Tests
# =============================================================================
def test_tracing_module_exists():
"""Verify tracing module exists"""
tracing_path = Path("/opt/agent-governance/observability/tracing.py")
assert tracing_path.exists(), "Tracing module not found"
content = tracing_path.read_text()
assert "Span" in content, "Span class not found"
assert "Trace" in content, "Trace class not found"
assert "Tracer" in content, "Tracer class not found"
print("[PASS] tracing module exists with Span, Trace, Tracer")
def test_tracer_creation():
"""Test tracer creation"""
from observability.tracing import Tracer
tracer = Tracer(service_name="test-service")
assert tracer.service_name == "test-service", "Service name not set"
print("[PASS] tracer creation works")
def test_span_creation():
"""Test span creation and timing"""
from observability.tracing import Tracer
tracer = Tracer()
with tracer.trace("test_operation") as span:
time.sleep(0.01) # Small delay
span.set_attribute("test_key", "test_value")
assert span.trace_id is not None, "Trace ID not set"
assert span.span_id is not None, "Span ID not set"
assert span.end_time is not None, "End time not set"
assert span.duration_ms > 0, "Duration should be positive"
assert span.attributes.get("test_key") == "test_value", "Attribute not set"
print("[PASS] span creation and timing works")
def test_nested_spans():
"""Test nested span relationships"""
from observability.tracing import Tracer
tracer = Tracer()
with tracer.trace("parent_operation") as parent:
with tracer.span("child_operation") as child:
child.set_attribute("level", "child")
assert child.parent_span_id == parent.span_id, "Parent-child relationship not set"
assert child.trace_id == parent.trace_id, "Trace ID mismatch"
print("[PASS] nested spans with parent-child relationships work")
def test_span_error_handling():
"""Test span error recording"""
from observability.tracing import Tracer
tracer = Tracer()
try:
with tracer.trace("failing_operation") as span:
raise ValueError("Test error")
except ValueError:
pass
assert span.status == "error", "Span status should be error"
assert "ValueError" in span.attributes.get("error.type", ""), "Error type not recorded"
print("[PASS] span error handling works")
def test_trace_context_propagation():
"""Test trace context thread-local storage"""
from observability.tracing import Tracer, get_current_trace_id
tracer = Tracer()
with tracer.trace("context_test"):
trace_id = get_current_trace_id()
assert trace_id is not None, "Trace ID not in context"
# After trace ends, should be None
assert get_current_trace_id() is None, "Trace ID should be cleared"
print("[PASS] trace context propagation works")
def test_traces_table_creation():
"""Test that traces table gets created"""
from observability.tracing import TraceStorage
storage = TraceStorage()
conn = get_db()
cursor = conn.execute("""
SELECT name FROM sqlite_master WHERE type='table' AND name='traces'
""")
row = cursor.fetchone()
conn.close()
assert row is not None, "traces table not created"
print("[PASS] traces table exists")
# =============================================================================
# Logging Tests
# =============================================================================
def test_logging_module_exists():
"""Verify logging module exists"""
logging_path = Path("/opt/agent-governance/observability/logging.py")
assert logging_path.exists(), "Logging module not found"
content = logging_path.read_text()
assert "LogEntry" in content, "LogEntry class not found"
assert "LogStorage" in content, "LogStorage class not found"
assert "get_logger" in content, "get_logger function not found"
print("[PASS] logging module exists with LogEntry, LogStorage, get_logger")
def test_logger_creation():
"""Test logger creation"""
from observability.logging import get_logger
logger = get_logger("test_module")
assert logger is not None, "Logger not created"
assert logger.name == "test_module", "Logger name not set"
print("[PASS] logger creation works")
def test_log_entry_structure():
"""Test log entry data structure"""
from observability.logging import LogEntry
entry = LogEntry(
timestamp="2026-01-24T12:00:00Z",
level="INFO",
logger="test",
message="Test message",
trace_id="trace-123",
attributes={"key": "value"}
)
d = entry.to_dict()
assert d["level"] == "INFO", "Level not set"
assert d["message"] == "Test message", "Message not set"
assert d["trace_id"] == "trace-123", "Trace ID not set"
assert d["key"] == "value", "Attributes not merged"
print("[PASS] log entry structure works")
def test_log_entry_json_serialization():
"""Test log entry JSON serialization"""
from observability.logging import LogEntry
entry = LogEntry(
timestamp="2026-01-24T12:00:00Z",
level="ERROR",
logger="test",
message="Error occurred"
)
json_str = entry.to_json()
parsed = json.loads(json_str)
assert parsed["level"] == "ERROR", "JSON serialization failed"
print("[PASS] log entry JSON serialization works")
def test_logs_table_creation():
"""Test that logs table gets created"""
from observability.logging import LogStorage
storage = LogStorage()
conn = get_db()
cursor = conn.execute("""
SELECT name FROM sqlite_master WHERE type='table' AND name='logs'
""")
row = cursor.fetchone()
conn.close()
assert row is not None, "logs table not created"
print("[PASS] logs table exists")
def test_log_storage_save_and_search():
"""Test log storage save and search"""
from observability.logging import LogStorage, LogEntry
storage = LogStorage()
# Save a test log
entry = LogEntry(
timestamp="2026-01-24T12:00:00Z",
level="INFO",
logger="test_storage",
message="Test log entry for search",
tenant_id="default"
)
storage.save(entry)
# Search for it
logs = storage.search(logger="test_storage", limit=10)
# Should find at least one log
found = any("Test log entry for search" in log.get("message", "") for log in logs)
assert found, "Log entry not found in search"
print("[PASS] log storage save and search works")
# =============================================================================
# Integration Tests
# =============================================================================
def test_tracing_with_logging():
"""Test trace correlation in logs"""
from observability.tracing import Tracer, get_current_trace_id
tracer = Tracer()
with tracer.trace("correlated_operation") as span:
trace_id = get_current_trace_id()
assert trace_id is not None, "No trace ID in context"
assert trace_id == span.trace_id, "Trace ID mismatch"
print("[PASS] tracing and logging correlation works")
def test_observability_package_import():
"""Test that observability package imports cleanly"""
try:
from observability import (
registry,
get_tracer,
get_logger,
record_agent_execution,
record_violation
)
assert registry is not None
print("[PASS] observability package imports cleanly")
except ImportError as e:
raise AssertionError(f"Failed to import observability package: {e}")
def run_all_tests():
"""Run all Phase 12 tests"""
print("=" * 60)
print("PHASE 12: OBSERVABILITY TESTS")
print("=" * 60)
print()
tests = [
# Metrics tests
test_metrics_module_exists,
test_metrics_registry_exists,
test_counter_increment,
test_gauge_set,
test_histogram_observe,
test_prometheus_format,
# Tracing tests
test_tracing_module_exists,
test_tracer_creation,
test_span_creation,
test_nested_spans,
test_span_error_handling,
test_trace_context_propagation,
test_traces_table_creation,
# Logging tests
test_logging_module_exists,
test_logger_creation,
test_log_entry_structure,
test_log_entry_json_serialization,
test_logs_table_creation,
test_log_storage_save_and_search,
# Integration tests
test_tracing_with_logging,
test_observability_package_import,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except AssertionError as e:
print(f"[FAIL] {test.__name__}: {e}")
failed += 1
except Exception as e:
print(f"[ERROR] {test.__name__}: {e}")
failed += 1
print()
print("=" * 60)
print(f"RESULTS: {passed} passed, {failed} failed")
print("=" * 60)
return failed == 0
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)