Skip to main content
Glama
test_structured_logging.py12.3 kB
"""Tests for structured logging functionality.""" import json import logging import time from io import StringIO from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from tests.types_stub import LogRecordExtended from utils.api.request_context import ( RequestContext, clear_current_context, set_current_context, ) from utils.api.structured_logging import ( ContextFilter, StructuredFormatter, get_structured_logger, log_api_request, log_api_response, log_error_with_context, performance_timer, setup_structured_logging, ) def test_context_filter_with_context(): """Test ContextFilter adds context information to log records.""" # Set up context context = ( RequestContext() .with_endpoint("/test/endpoint", "GET") .with_resource("test", "test-id") .with_user("user123") ) set_current_context(context) # Create log record record = logging.LogRecord( name="test", level=logging.INFO, pathname="", lineno=0, msg="Test message", args=(), exc_info=None, ) # Apply filter filter_instance = ContextFilter() result = filter_instance.filter(record) # Check filter returns True assert result is True # Cast to extended type for dynamic attributes extended_record = cast("LogRecordExtended", record) # Check context information was added assert extended_record.correlation_id == context.correlation_id assert extended_record.endpoint == "/test/endpoint" assert extended_record.method == "GET" assert extended_record.resource_type == "test" assert extended_record.resource_id == "test-id" assert extended_record.user_id == "user123" assert extended_record.elapsed_time is not None def test_context_filter_without_context(): """Test ContextFilter handles missing context gracefully.""" # Clear context clear_current_context() # Create log record record = logging.LogRecord( name="test", level=logging.INFO, pathname="", lineno=0, msg="Test message", args=(), exc_info=None, ) # Apply filter filter_instance = ContextFilter() result = filter_instance.filter(record) # Check filter returns True assert result is True # Cast to extended type for dynamic attributes extended_record = cast("LogRecordExtended", record) # Check defaults were set assert extended_record.correlation_id is None assert extended_record.endpoint is None assert extended_record.method is None assert extended_record.resource_type is None assert extended_record.resource_id is None assert extended_record.user_id is None assert extended_record.elapsed_time is None def test_structured_formatter_basic(): """Test StructuredFormatter produces valid JSON.""" # Create log record record = logging.LogRecord( name="test.logger", level=logging.INFO, pathname="", lineno=0, msg="Test message", args=(), exc_info=None, ) # Cast to extended type for dynamic attributes extended_record = cast("LogRecordExtended", record) # Add context fields extended_record.correlation_id = "test-correlation-id" extended_record.endpoint = "/test/endpoint" extended_record.method = "GET" # Format the record (using original record for formatter) formatter = StructuredFormatter() formatted = formatter.format(record) # Parse as JSON log_data = json.loads(formatted) # Check required fields assert "timestamp" in log_data assert log_data["level"] == "INFO" assert log_data["logger"] == "test.logger" assert log_data["message"] == "Test message" assert log_data["correlation_id"] == "test-correlation-id" assert log_data["endpoint"] == "/test/endpoint" assert log_data["method"] == "GET" def test_structured_formatter_with_exception(): """Test StructuredFormatter handles exceptions.""" import sys # Create log record with exception try: raise ValueError("Test error") except ValueError: record = logging.LogRecord( name="test.logger", level=logging.ERROR, pathname="", lineno=0, msg="Error occurred", args=(), exc_info=sys.exc_info(), ) # Format the record formatter = StructuredFormatter() formatted = formatter.format(record) # Parse as JSON log_data = json.loads(formatted) # Check exception information is included assert "exception" in log_data assert "ValueError: Test error" in log_data["exception"] def test_structured_formatter_with_extra_fields(): """Test StructuredFormatter includes extra fields.""" # Create log record record = logging.LogRecord( name="test.logger", level=logging.INFO, pathname="", lineno=0, msg="Test message", args=(), exc_info=None, ) # Cast to extended type for dynamic attributes extended_record = cast("LogRecordExtended", record) # Add extra fields extended_record.custom_field = "custom_value" extended_record.operation = "test_operation" # Format the record formatter = StructuredFormatter() formatted = formatter.format(record) # Parse as JSON log_data = json.loads(formatted) # Check extra fields are included assert log_data["custom_field"] == "custom_value" assert log_data["operation"] == "test_operation" def test_setup_structured_logging(): """Test structured logging setup.""" logger_name = "test.structured" logger = setup_structured_logging(logger_name, logging.DEBUG) # Check logger configuration assert logger.name == logger_name assert logger.level == logging.DEBUG assert logger.propagate is False assert len(logger.handlers) == 1 # Check handler configuration handler = logger.handlers[0] assert isinstance(handler, logging.StreamHandler) assert isinstance(handler.formatter, StructuredFormatter) assert any(isinstance(f, ContextFilter) for f in handler.filters) def test_get_structured_logger_new(): """Test getting a new structured logger.""" logger_name = "test.new.logger" logger = get_structured_logger(logger_name) # Check logger has structured formatting assert len(logger.handlers) >= 1 handler = logger.handlers[0] assert isinstance(handler.formatter, StructuredFormatter) def test_get_structured_logger_existing(): """Test getting an existing structured logger.""" logger_name = "test.existing.logger" # First call sets up structured logging logger1 = get_structured_logger(logger_name) original_handler_count = len(logger1.handlers) # Second call should return same logger without adding handlers logger2 = get_structured_logger(logger_name) assert logger1 is logger2 assert len(logger2.handlers) == original_handler_count def test_performance_timer(): """Test performance timer context manager.""" # Capture log output log_stream = StringIO() handler = logging.StreamHandler(log_stream) handler.setFormatter(StructuredFormatter()) logger = logging.getLogger("test.performance") logger.addHandler(handler) logger.setLevel(logging.INFO) # Use performance timer operation_name = "test_operation" with performance_timer(operation_name, logger): time.sleep(0.01) # Small delay # Check log output log_output = log_stream.getvalue() log_lines = [line for line in log_output.strip().split("\n") if line] # Should have start and complete messages assert len(log_lines) >= 2 # Parse log entries start_log = json.loads(log_lines[0]) complete_log = json.loads(log_lines[-1]) # Check start log assert start_log["operation"] == operation_name assert start_log["event"] == "operation_start" # Check complete log assert complete_log["operation"] == operation_name assert complete_log["event"] == "operation_complete" assert "duration" in complete_log assert "duration_ms" in complete_log assert complete_log["duration"] > 0 def test_log_api_request(): """Test API request logging.""" # Capture log output log_stream = StringIO() handler = logging.StreamHandler(log_stream) handler.setFormatter(StructuredFormatter()) logger = logging.getLogger("test.api.request") logger.addHandler(handler) logger.setLevel(logging.INFO) # Log API request endpoint = "/test/endpoint" method = "POST" parameters = {"param1": "value1", "param2": 42} log_api_request(endpoint, method, parameters, logger) # Check log output log_output = log_stream.getvalue().strip() log_data = json.loads(log_output) # Check log content assert log_data["event"] == "api_request" assert log_data["endpoint"] == endpoint assert log_data["method"] == method assert log_data["parameters"] == parameters assert f"API request: {method} {endpoint}" in log_data["message"] def test_log_api_response(): """Test API response logging.""" # Capture log output log_stream = StringIO() handler = logging.StreamHandler(log_stream) handler.setFormatter(StructuredFormatter()) logger = logging.getLogger("test.api.response") logger.addHandler(handler) logger.setLevel(logging.INFO) # Log API response endpoint = "/test/endpoint" status_code = 200 response_size = 1024 log_api_response(endpoint, status_code, response_size, logger) # Check log output log_output = log_stream.getvalue().strip() log_data = json.loads(log_output) # Check log content assert log_data["event"] == "api_response" assert log_data["endpoint"] == endpoint assert log_data["status_code"] == status_code assert log_data["response_size"] == response_size assert f"API response: {endpoint}" in log_data["message"] def test_log_error_with_context(): """Test error logging with context.""" # Capture log output log_stream = StringIO() handler = logging.StreamHandler(log_stream) handler.setFormatter(StructuredFormatter()) logger = logging.getLogger("test.error") logger.addHandler(handler) logger.setLevel(logging.ERROR) # Log error error_message = "Test error occurred" error = ValueError("Test exception") operation = "test_operation" log_error_with_context(error_message, error, operation, logger) # Check log output log_output = log_stream.getvalue().strip() log_data = json.loads(log_output) # Check log content assert log_data["event"] == "error" assert log_data["error_type"] == "ValueError" assert log_data["error_message"] == "Test exception" assert log_data["operation"] == operation assert log_data["message"] == error_message assert log_data["level"] == "ERROR" def test_performance_timer_with_default_logger(): """Test performance timer with default logger.""" operation_name = "test_default_logger" # Should not raise any exceptions with performance_timer(operation_name): time.sleep(0.001) # Very small delay def test_log_api_request_with_default_logger(): """Test API request logging with default logger.""" endpoint = "/test/default" # Should not raise any exceptions log_api_request(endpoint) def test_log_api_response_with_default_logger(): """Test API response logging with default logger.""" endpoint = "/test/default" # Should not raise any exceptions log_api_response(endpoint) def test_log_error_with_default_logger(): """Test error logging with default logger.""" error = Exception("Test error") # Should not raise any exceptions log_error_with_context("Test message", error) def teardown_function(): """Clean up after each test.""" clear_current_context() # Clean up loggers to avoid interference for name in list(logging.Logger.manager.loggerDict.keys()): if name.startswith("test."): logger = logging.getLogger(name) for handler in logger.handlers[:]: logger.removeHandler(handler)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wwiens/trakt_mcpserver'

If you have feedback or need assistance with the MCP directory API, please join our Discord server