From eb769fa89c68c661641ff10000af98846fb728e3 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Wed, 18 Feb 2026 17:31:30 -0500 Subject: [PATCH 1/2] Port logging tests to `@on_viceroy`. --- Makefile | 2 +- examples/logging/log_app.py | 247 ------------------------------ examples/logging/pyproject.toml | 15 -- examples/logging/uv.lock | 53 ------- fastly_compute/tests/test_log.py | 250 ++++++++++++++++++++++--------- 5 files changed, 183 insertions(+), 384 deletions(-) delete mode 100644 examples/logging/log_app.py delete mode 100644 examples/logging/pyproject.toml delete mode 100644 examples/logging/uv.lock diff --git a/Makefile b/Makefile index cb8c0f0..00d5407 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ EXAMPLES_DIR := examples COMPUTE_WIT := wit/deps/fastly/compute.wit # Define all available examples (add new ones here) -EXAMPLES := bottle-app flask-app backend-requests game-of-life logging +EXAMPLES := bottle-app flask-app backend-requests game-of-life # Default example for serve target EXAMPLE ?= bottle-app diff --git a/examples/logging/log_app.py b/examples/logging/log_app.py deleted file mode 100644 index 620fcd1..0000000 --- a/examples/logging/log_app.py +++ /dev/null @@ -1,247 +0,0 @@ -"""Logging example application. - -Demonstrates Fastly Logging API usage with test endpoints. -""" - -import json -import logging -import traceback -from typing import Any - -from bottle import Bottle, response - -from fastly_compute.log import FastlyLogHandler, LogEndpoint -from fastly_compute.wsgi import WsgiHttpIncoming - -app = Bottle() - - -def json_response(data: dict[str, Any], status_code: int = 200) -> str: - """Create a JSON response.""" - response.content_type = "application/json" - response.status = status_code - return json.dumps(data, indent=2) - - -def handle_request(handler): - """Decorator to handle common request/response patterns.""" - - def wrapper(*args, **kwargs): - try: - result = handler(*args, **kwargs) - return json_response(result) - except Exception as e: - return json_response( - { - "error": repr(e), - "error_type": type(e).__name__, - "traceback": traceback.format_exc(), - }, - status_code=500, - ) - - return wrapper - - -# Direct API tests -@app.route("/test/write//") -@handle_request -def test_write(endpoint_name, message): - """Test writing a string message.""" - endpoint = LogEndpoint.open(endpoint_name) - endpoint.write(message) - return {"written": True} - - -@app.route("/test/write-bytes/") -@handle_request -def test_write_bytes(endpoint_name): - """Test writing bytes directly.""" - endpoint = LogEndpoint.open(endpoint_name) - endpoint.write(b"Binary log data: \x00\x01\x02\x03") - return {"written": True} - - -@app.route("/test/write-empty/") -@handle_request -def test_write_empty(endpoint_name): - """Test writing an empty string.""" - endpoint = LogEndpoint.open(endpoint_name) - endpoint.write("") - return {"written": True} - - -@app.route("/test/context-manager/") -@handle_request -def test_context_manager(endpoint_name): - """Test using endpoint as a context manager.""" - with LogEndpoint.open(endpoint_name) as endpoint: - endpoint.write("Message from context manager") - return {"success": True} - - -# Python logging integration tests -@app.route("/test/logging///") -@handle_request -def test_logging(endpoint_name, level, message): - """Test standard Python logging integration.""" - logger = logging.getLogger(f"test_{endpoint_name}") - logger.setLevel(logging.DEBUG) - - # Clear any existing handlers - logger.handlers.clear() - - # Add Fastly handler with new API - handler = FastlyLogHandler(default_endpoint=endpoint_name) - handler.setFormatter( - logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - ) - logger.addHandler(handler) - - # Log at the requested level - log_func = getattr(logger, level.lower()) - log_func(message) - - # Clean up - handler.close() - - return {"logged": True} - - -@app.route("/test/logging-extra/") -@handle_request -def test_logging_extra(endpoint_name): - """Test logging with extra fields.""" - logger = logging.getLogger(f"test_extra_{endpoint_name}") - logger.setLevel(logging.INFO) - logger.handlers.clear() - - handler = FastlyLogHandler(default_endpoint=endpoint_name) - handler.setFormatter( - logging.Formatter( - "%(asctime)s - %(levelname)s - %(message)s - user=%(user_id)s" - ) - ) - logger.addHandler(handler) - - logger.info("User action", extra={"user_id": 12345}) - - handler.close() - - return {"logged": True} - - -@app.route("/test/logging-multiple/") -@handle_request -def test_logging_multiple(endpoint_name): - """Test logging multiple messages.""" - logger = logging.getLogger(f"test_multiple_{endpoint_name}") - logger.setLevel(logging.INFO) - logger.handlers.clear() - - handler = FastlyLogHandler(default_endpoint=endpoint_name) - logger.addHandler(handler) - - count = 5 - for i in range(count): - logger.info(f"Log message {i + 1}") - - handler.close() - - return {"count": count} - - -@app.route("/test/json-log/") -@handle_request -def test_json_log(endpoint_name): - """Test structured JSON logging.""" - logger = logging.getLogger(f"test_json_{endpoint_name}") - logger.setLevel(logging.INFO) - logger.handlers.clear() - - handler = FastlyLogHandler(default_endpoint=endpoint_name) - logger.addHandler(handler) - - # Create structured log message - log_data = { - "event": "request_processed", - "status": 200, - "duration_ms": 42, - "user_id": 12345, - "path": "/api/data", - } - logger.info(json.dumps(log_data)) - - handler.close() - - return {"logged": True} - - -# Endpoint routing tests -@app.route("/test/logging-with-mapper///") -@handle_request -def test_logging_with_mapper(logger_name, level, message): - """Test logging with endpoint mapper function.""" - logger = logging.getLogger(logger_name) - logger.setLevel(logging.DEBUG) - logger.handlers.clear() - - # Define a mapper that routes based on logger name - def endpoint_mapper(name: str) -> str | None: - if name.startswith("api"): - return "api-logs" - # Return None to use default - return None - - handler = FastlyLogHandler( - default_endpoint="default-logs", endpoint_mapper=endpoint_mapper - ) - handler.setFormatter( - logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - ) - logger.addHandler(handler) - - # Log at the requested level - log_func = getattr(logger, level.lower()) - log_func(message) - - handler.close() - - return {"logged": True} - - -@app.route("/test/logging-with-dict///") -@handle_request -def test_logging_with_dict(logger_name, level, message): - """Test logging with dict-based endpoint mapper.""" - logger = logging.getLogger(logger_name) - logger.setLevel(logging.DEBUG) - logger.handlers.clear() - - # Use a dict for simple mapping - endpoint_map = { - "api": "api-logs", - "worker": "worker-logs", - "background": "worker-logs", - } - - handler = FastlyLogHandler( - default_endpoint="default-logs", - endpoint_mapper=lambda name: endpoint_map.get(name.split(".")[0]), - ) - handler.setFormatter( - logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") - ) - logger.addHandler(handler) - - # Log at the requested level - log_func = getattr(logger, level.lower()) - log_func(message) - - handler.close() - - return {"logged": True} - - -# Create the HTTP handler for Fastly Compute -HttpIncoming = WsgiHttpIncoming(app) diff --git a/examples/logging/pyproject.toml b/examples/logging/pyproject.toml deleted file mode 100644 index 0f8504f..0000000 --- a/examples/logging/pyproject.toml +++ /dev/null @@ -1,15 +0,0 @@ -[project] -name = "logging" -version = "0.1.0" -description = "Fastly Compute example demonstrating Logging API usage" -requires-python = ">=3.12" -dependencies = [ - "bottle>=0.12.25", - "fastly-compute", -] - -[tool.uv.sources] -fastly-compute = { path = "../../", editable = true } - -[tool.fastly-compute] -entry = "log_app" diff --git a/examples/logging/uv.lock b/examples/logging/uv.lock deleted file mode 100644 index 9c083d1..0000000 --- a/examples/logging/uv.lock +++ /dev/null @@ -1,53 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[[package]] -name = "bottle" -version = "0.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" }, -] - -[[package]] -name = "fastly-compute" -version = "0.1.0" -source = { editable = "../../" } - -[package.metadata] -requires-dist = [ - { name = "bottle", marker = "extra == 'examples'", specifier = ">=0.12.25" }, - { name = "bottle", marker = "extra == 'test'", specifier = ">=0.12.25" }, - { name = "componentize-py", marker = "extra == 'dev'", specifier = ">=0.19.3,<0.20" }, - { name = "flask", marker = "extra == 'examples'", specifier = ">=3.1.2,<4.0" }, - { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.49.0,<0.50" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=8.4.0,<9.0.0" }, - { name = "requests", marker = "extra == 'test'", specifier = ">=2.32.5,<3.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.11,<0.13.0" }, - { name = "syrupy", marker = "extra == 'test'", specifier = "==5.0.0" }, - { name = "tomli-w", marker = "extra == 'test'", specifier = ">=1.0.0,<2.0.0" }, -] -provides-extras = ["test", "dev", "examples"] - -[package.metadata.requires-dev] -dev = [ - { name = "jinja2", specifier = ">=3.1.6" }, - { name = "maturin", specifier = ">=1.11.5" }, -] - -[[package]] -name = "logging" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "bottle" }, - { name = "fastly-compute" }, -] - -[package.metadata] -requires-dist = [ - { name = "bottle", specifier = ">=0.12.25" }, - { name = "fastly-compute", editable = "../../" }, -] diff --git a/fastly_compute/tests/test_log.py b/fastly_compute/tests/test_log.py index bb9305c..784ff9f 100644 --- a/fastly_compute/tests/test_log.py +++ b/fastly_compute/tests/test_log.py @@ -1,15 +1,16 @@ """Integration tests for Logging functionality.""" +import json +import logging import re -from fastly_compute.testing import ViceroyTestBase +from fastly_compute.log import FastlyLogHandler, LogEndpoint +from fastly_compute.testing import AutoViceroyTestBase, on_viceroy -class TestLogging(ViceroyTestBase): +class TestLogging(AutoViceroyTestBase): """Logging integration tests.""" - WASM_FILE = "build/logging.composed.wasm" - VICEROY_CONFIG = { "local_server": { "log_endpoints": { @@ -22,31 +23,6 @@ class TestLogging(ViceroyTestBase): } } - def assert_success(self, response, expected_data=None): - """Assert that the response was successful. - - Args: - response: The HTTP response - expected_data: Optional dict of expected response data - """ - assert response.status_code == 200 - data = response.json() - if expected_data: - for key, value in expected_data.items(): - assert data[key] == value - return data - - def assert_error(self, response, error_type): - """Assert that the response contains an error of the expected type. - - Args: - response: The HTTP response - error_type: Expected error type name (e.g., "LogEndpointInvalidNameError") - """ - assert response.status_code == 500 - data = response.json() - assert data["error_type"] == error_type - def _get_logs_for_endpoint(self, endpoint_name): """Get all log messages for a specific endpoint from viceroy output. @@ -145,100 +121,238 @@ def assert_log_count(self, endpoint_name, expected_count, pattern=None): # Writing messages + @on_viceroy + def log(cls, endpoint_name, message): + """Log a message to a named endpoint.""" + endpoint = LogEndpoint.open(endpoint_name) + endpoint.write(message) + def test_write_string(self): """Test writing a string message.""" - response = self.get("/test/write/test-logs/Hello%20World") - self.assert_success(response, {"written": True}) - self.assert_log_message("test-logs", "Hello%20World") + self.log("test-logs", "Hello World") + self.assert_log_message("test-logs", "Hello World") + + @on_viceroy + def log_bytes(cls, endpoint_name): + """Log a message to a named endpoint.""" + endpoint = LogEndpoint.open(endpoint_name) + endpoint.write(b"Binary log data: \x00\x01\x02\x03") def test_write_bytes(self): - """Test writing bytes directly.""" - response = self.get("/test/write-bytes/test-logs") - self.assert_success(response, {"written": True}) - # Binary data with null bytes - verify at least one log was written + """Test writing raw bytes, including null ones.""" + self.log_bytes("test-logs") self.assert_log_matches("test-logs", r"Binary log data") def test_write_unicode(self): """Test writing unicode characters.""" - response = self.get( - "/test/write/test-logs/Hello%20%E4%B8%96%E7%95%8C%20%F0%9F%8C%8D" - ) - self.assert_success(response, {"written": True}) - self.assert_log_message( - "test-logs", "Hello%20%E4%B8%96%E7%95%8C%20%F0%9F%8C%8D" - ) + message = "Hello δΈ–η•Œ 🌍" + self.log("test-logs", message) + self.assert_log_message("test-logs", message) def test_write_empty_string(self): """Test writing an empty string (produces no log event per spec).""" - response = self.get("/test/write-empty/test-logs") - self.assert_success(response, {"written": True}) + self.log("test-logs", "") # Per spec: "Each call to write with a non-empty message produces a single log event" # Empty string shouldn't produce a log, but we can't easily verify this # since we share the endpoint with other tests. Just verify the API succeeds. + @on_viceroy + def log_with_context_manager(cls, endpoint_name): + """Test using endpoint as a context manager.""" + with LogEndpoint.open(endpoint_name) as endpoint: + endpoint.write("Message from context manager") + def test_context_manager(self): """Test using endpoint as a context manager.""" - response = self.get("/test/context-manager/test-logs") - self.assert_success(response, {"success": True}) + self.log_with_context_manager("test-logs") self.assert_log_message("test-logs", "Message from context manager") # Python logging integration + @on_viceroy + def log_at_level(cls, endpoint_name: str, level: str, message: str): + """Log a message using Python logging stdlib integration.""" + logger = logging.getLogger(f"test_{endpoint_name}") + logger.setLevel(logging.DEBUG) + + # Clear any existing handlers + logger.handlers.clear() + + # Add Fastly handler with new API + handler = FastlyLogHandler(default_endpoint=endpoint_name) + handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ) + logger.addHandler(handler) + + # Log at the requested level + log_func = getattr(logger, level.lower()) + log_func(message) + + # Clean up + handler.close() + def test_standard_logging_integration(self): """Test integration with Python standard logging.""" - response = self.get("/test/logging/test-logs/INFO/Test%20message") - self.assert_success(response, {"logged": True}) + self.log_at_level("test-logs", "INFO", "Test message") # The log should include timestamp, logger name, level, and message - self.assert_log_matches("test-logs", r"INFO.*Test%20message") + self.assert_log_matches("test-logs", r"INFO.*Test message") + + @on_viceroy + def log_with_extra(cls, endpoint_name): + """Test logging with extra fields.""" + logger = logging.getLogger(f"test_extra_{endpoint_name}") + logger.setLevel(logging.INFO) + logger.handlers.clear() + + handler = FastlyLogHandler(default_endpoint=endpoint_name) + handler.setFormatter( + logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s - user=%(user_id)s" + ) + ) + logger.addHandler(handler) + + logger.info("User action", extra={"user_id": 12345}) + + handler.close() def test_logging_with_extra_fields(self): """Test logging with extra fields.""" - response = self.get("/test/logging-extra/test-logs") - self.assert_success(response, {"logged": True}) + self.log_with_extra("test-logs") # Should include the user_id in the formatted message self.assert_log_matches("test-logs", r"user=12345") + @on_viceroy + def log_multiple(cls, endpoint_name): + """Test logging multiple messages.""" + logger = logging.getLogger(f"test_multiple_{endpoint_name}") + logger.setLevel(logging.INFO) + logger.handlers.clear() + + handler = FastlyLogHandler(default_endpoint=endpoint_name) + logger.addHandler(handler) + + count = 5 + for i in range(count): + logger.info(f"Log message {i + 1}") + + handler.close() + def test_logging_multiple_messages(self): """Test logging multiple messages in sequence.""" - response = self.get("/test/logging-multiple/test-logs") - self.assert_success(response, {"count": 5}) + self.log_multiple("test-logs") # Should have exactly 5 log messages with "Log message" in them self.assert_log_count("test-logs", 5, pattern=r"Log message") def test_logging_all_levels(self): """Test logging at all standard levels.""" for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: - response = self.get(f"/test/logging/test-logs/{level}/Test%20at%20{level}") - self.assert_success(response, {"logged": True}) - self.assert_log_matches("test-logs", rf"{level}.*Test%20at%20{level}") + self.log_at_level("test-logs", level, f"Test at {level}") + self.assert_log_matches("test-logs", rf"{level}.*Test at {level}") + + @on_viceroy + def log_json(cls, endpoint_name): + """Test structured JSON logging.""" + logger = logging.getLogger(f"test_json_{endpoint_name}") + logger.setLevel(logging.INFO) + logger.handlers.clear() + + handler = FastlyLogHandler(default_endpoint=endpoint_name) + logger.addHandler(handler) + + # Create structured log message + log_data = { + "event": "request_processed", + "status": 200, + "duration_ms": 42, + "user_id": 12345, + "path": "/api/data", + } + logger.info(json.dumps(log_data)) + + handler.close() def test_json_structured_logging(self): """Test structured logging with JSON format.""" - response = self.get("/test/json-log/json-logs") - self.assert_success(response, {"logged": True}) + self.log_json("json-logs") # Should contain JSON with the expected fields self.assert_log_matches("json-logs", r'"event":\s*"request_processed"') self.assert_log_matches("json-logs", r'"user_id":\s*12345') # Endpoint routing tests + @on_viceroy + def log_with_mapper(cls, logger_name, level, message): + """Test logging with endpoint mapper function.""" + logger = logging.getLogger(logger_name) + logger.setLevel(logging.DEBUG) + logger.handlers.clear() + + # Define a mapper that routes based on logger name + def endpoint_mapper(name: str) -> str | None: + if name.startswith("api"): + return "api-logs" + # Return None to use default + return None + + handler = FastlyLogHandler( + default_endpoint="default-logs", endpoint_mapper=endpoint_mapper + ) + handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ) + logger.addHandler(handler) + + # Log at the requested level + log_func = getattr(logger, level.lower()) + log_func(message) + + handler.close() + def test_logging_with_mapper_function(self): """Test endpoint routing using a mapper function.""" - response = self.get("/test/logging-with-mapper/api.requests/INFO/API%20request") - self.assert_success(response, {"logged": True}) + self.log_with_mapper("api.requests", "INFO", "API request") # Should route to api-logs based on logger name - self.assert_log_matches("api-logs", r"INFO.*API%20request") + self.assert_log_matches("api-logs", r"INFO.*API request") def test_logging_with_mapper_fallback(self): """Test endpoint routing with mapper fallback to default.""" - response = self.get("/test/logging-with-mapper/unknown/INFO/Unknown%20logger") - self.assert_success(response, {"logged": True}) + self.log_with_mapper("unknown", "INFO", "Unknown logger") # Should fall back to default-logs - self.assert_log_matches("default-logs", r"INFO.*Unknown%20logger") + self.assert_log_matches("default-logs", r"INFO.*Unknown logger") + + @on_viceroy + def log_with_dict(cls, logger_name, level, message): + """Test logging with dict-based endpoint mapper.""" + logger = logging.getLogger(logger_name) + logger.setLevel(logging.DEBUG) + logger.handlers.clear() + + # Use a dict for simple mapping + endpoint_map = { + "api": "api-logs", + "worker": "worker-logs", + "background": "worker-logs", + } + + handler = FastlyLogHandler( + default_endpoint="default-logs", + endpoint_mapper=lambda name: endpoint_map.get(name.split(".")[0]), + ) + handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ) + logger.addHandler(handler) + + # Log at the requested level + log_func = getattr(logger, level.lower()) + log_func(message) + + handler.close() def test_logging_with_dict_mapper(self): """Test endpoint routing using a dict-based mapper.""" - response = self.get("/test/logging-with-dict/worker/INFO/Worker%20task") - self.assert_success(response, {"logged": True}) + self.log_with_dict("worker", "INFO", "Worker task") # Should route to worker-logs via dict lookup - self.assert_log_matches("worker-logs", r"INFO.*Worker%20task") + self.assert_log_matches("worker-logs", r"INFO.*Worker task") From b1b06ed842731e8dc7d47c26960139410a017948 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Wed, 18 Feb 2026 17:33:01 -0500 Subject: [PATCH 2/2] Typo --- scripts/generate_patches/wit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate_patches/wit.py b/scripts/generate_patches/wit.py index ce17075..4a4bb31 100644 --- a/scripts/generate_patches/wit.py +++ b/scripts/generate_patches/wit.py @@ -345,7 +345,7 @@ def __init__(self, package_json: dict, wit_json: dict[str, list[dict]]): self._wit = wit_json def interfaces(self) -> Iterable[Interface]: - """Return the iterfaces defined in this package.""" + """Return the interfaces defined in this package.""" for interface_num in self._package["interfaces"].values(): yield Interface(self._wit["interfaces"][interface_num], self._wit)