Skip to content

Commit ac6cfa0

Browse files
test: Add Firecracker tracing integration test
Verifies Firecracker works with clippy-tracing instrumentation and trace-level logging enabled. Signed-off-by: demoncoder-crypto <dhillonprabhjitsingh@gmail.com>
1 parent e36e774 commit ac6cfa0

File tree

1 file changed

+250
-0
lines changed

1 file changed

+250
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Test that Firecracker works correctly when instrumented with tracing and trace level logs are enabled.
5+
6+
This test verifies that the log-instrument crate can successfully instrument the actual Firecracker
7+
binary and that the instrumented Firecracker works correctly with trace-level logging.
8+
9+
Test Coverage:
10+
1. Uses clippy-tracing tool to add instrumentation to Firecracker source files
11+
2. Builds Firecracker with tracing features enabled
12+
3. Verifies basic functionality (API calls, VM lifecycle) works with instrumentation
13+
4. Confirms trace-level logs are generated and contain meaningful information
14+
5. Checks performance impact is within acceptable bounds
15+
"""
16+
17+
import re
18+
import tempfile
19+
import time
20+
from pathlib import Path
21+
22+
import pytest
23+
24+
from framework import utils
25+
from framework.microvm import MicroVMFactory
26+
from host_tools.cargo_build import cargo, get_binary
27+
28+
29+
def build_instrumented_firecracker():
30+
"""Build Firecracker with tracing instrumentation enabled."""
31+
# First, add instrumentation using clippy-tracing
32+
clippy_tracing = get_binary("clippy-tracing")
33+
34+
# Add instrumentation to a subset of files to avoid performance issues
35+
# We'll instrument just the API server and main entry points for meaningful traces
36+
cargo_args = [
37+
"--action",
38+
"fix",
39+
"--path",
40+
"./src/firecracker/src/main.rs",
41+
"--path",
42+
"./src/firecracker/src/api_server",
43+
"--path",
44+
"./src/vmm/src/lib.rs",
45+
"--path",
46+
"./src/vmm/src/builder.rs",
47+
]
48+
49+
utils.check_output(f"{clippy_tracing} {' '.join(cargo_args)}")
50+
51+
# Build Firecracker with tracing feature enabled
52+
cargo("build", "--features tracing --bin firecracker")
53+
54+
return get_binary("firecracker")
55+
56+
57+
def cleanup_instrumentation():
58+
"""Remove instrumentation from source files."""
59+
clippy_tracing = get_binary("clippy-tracing")
60+
61+
# Strip instrumentation from the files we modified
62+
strip_args = [
63+
"--action",
64+
"strip",
65+
"--path",
66+
"./src/firecracker/src/main.rs",
67+
"--path",
68+
"./src/firecracker/src/api_server",
69+
"--path",
70+
"./src/vmm/src/lib.rs",
71+
"--path",
72+
"./src/vmm/src/builder.rs",
73+
]
74+
75+
utils.check_output(f"{clippy_tracing} {' '.join(strip_args)}")
76+
77+
78+
@pytest.fixture(scope="module")
79+
def instrumented_firecracker_binary():
80+
"""Build instrumented Firecracker binary for testing."""
81+
binary = build_instrumented_firecracker()
82+
yield binary
83+
cleanup_instrumentation()
84+
85+
86+
def test_firecracker_tracing_basic_functionality(instrumented_firecracker_binary):
87+
"""Test that instrumented Firecracker can start and handle basic API calls with trace logging."""
88+
# Create a temporary directory for this test
89+
with tempfile.TemporaryDirectory() as temp_dir:
90+
# Create a MicroVM factory with the instrumented binary
91+
factory = MicroVMFactory(instrumented_firecracker_binary)
92+
93+
# Build a microVM
94+
vm = factory.build()
95+
96+
# Configure basic VM settings
97+
vm.basic_config(vcpu_count=1, mem_size_mib=128)
98+
99+
# Spawn the VM with trace level logging
100+
vm.spawn(log_level="Trace", log_show_level=True, log_show_origin=True)
101+
102+
try:
103+
# Wait for the API socket to be available
104+
vm._wait_for_api_socket() # pylint: disable=protected-access
105+
106+
# Make some basic API calls to generate trace logs
107+
# Get instance info
108+
response = vm.api.describe_instance.get()
109+
assert response.status_code == 200
110+
111+
# Get machine config
112+
response = vm.api.machine_config.get()
113+
assert response.status_code == 200
114+
115+
# Set logger to trace level to ensure we capture instrumentation logs
116+
logger_config = {"level": "Trace"}
117+
response = vm.api.logger.put(**logger_config)
118+
assert response.status_code == 204
119+
120+
# Make another API call after setting trace level
121+
response = vm.api.describe_instance.get()
122+
assert response.status_code == 200
123+
124+
# Verify that the VM is working correctly
125+
assert vm.state == "Not started"
126+
127+
finally:
128+
vm.kill()
129+
130+
# Check the logs for instrumentation traces
131+
log_data = vm.log_data
132+
133+
# Verify that trace level logs are present
134+
assert "TRACE" in log_data, "Expected TRACE level logs in output"
135+
136+
# Look for log-instrument traces (function entry/exit)
137+
# These should have the format: ThreadId(X)>>function_name or ThreadId(X)<<function_name
138+
trace_pattern = r"ThreadId\(\d+\)(?:::[^>]*)?(?:>>|<<)\w+"
139+
trace_matches = re.findall(trace_pattern, log_data)
140+
141+
assert (
142+
len(trace_matches) > 0
143+
), f"Expected to find log-instrument traces in logs, but found none. Log data: {log_data[:1000]}..."
144+
145+
# Verify we see function entry and exit traces
146+
entry_traces = [match for match in trace_matches if ">>" in match]
147+
exit_traces = [match for match in trace_matches if "<<" in match]
148+
149+
assert len(entry_traces) > 0, "Expected to find function entry traces (>>)"
150+
assert len(exit_traces) > 0, "Expected to find function exit traces (<<)"
151+
152+
# Verify that meaningful functions are being traced
153+
# Look for traces from main, API handling, or VM management functions
154+
meaningful_functions = ["main", "api", "vmm", "request", "response"]
155+
found_meaningful = False
156+
157+
for trace in trace_matches:
158+
for func in meaningful_functions:
159+
if func.lower() in trace.lower():
160+
found_meaningful = True
161+
break
162+
if found_meaningful:
163+
break
164+
165+
assert (
166+
found_meaningful
167+
), f"Expected to find traces from meaningful functions, but traces were: {trace_matches[:10]}"
168+
169+
170+
def test_firecracker_tracing_performance_impact():
171+
"""Test that instrumented Firecracker still performs reasonably (basic smoke test)."""
172+
# This is a basic performance smoke test to ensure tracing doesn't break functionality
173+
# We're not doing detailed performance analysis, just ensuring it doesn't hang or crash
174+
175+
# Build instrumented binary
176+
instrumented_binary = build_instrumented_firecracker()
177+
178+
try:
179+
factory = MicroVMFactory(instrumented_binary)
180+
vm = factory.build()
181+
182+
# Time the basic configuration and startup
183+
start_time = time.time()
184+
185+
vm.basic_config(vcpu_count=1, mem_size_mib=128, add_root_device=False)
186+
vm.spawn(log_level="Trace")
187+
188+
# Make several API calls
189+
for _ in range(5):
190+
response = vm.api.describe_instance.get()
191+
assert response.status_code == 200
192+
193+
elapsed = time.time() - start_time
194+
195+
# Should complete within reasonable time (30 seconds is very generous)
196+
# This is just to catch major performance regressions or hangs
197+
assert (
198+
elapsed < 30
199+
), f"Instrumented Firecracker took too long to start and handle API calls: {elapsed}s"
200+
201+
vm.kill()
202+
203+
finally:
204+
cleanup_instrumentation()
205+
206+
207+
def test_trace_log_filtering():
208+
"""Test that trace log filtering works correctly with instrumented Firecracker."""
209+
instrumented_binary = build_instrumented_firecracker()
210+
211+
try:
212+
factory = MicroVMFactory(instrumented_binary)
213+
vm = factory.build()
214+
215+
vm.basic_config(vcpu_count=1, mem_size_mib=128, add_root_device=False)
216+
vm.spawn(log_level="Info") # Start with Info level
217+
218+
try:
219+
# Initially should not have trace logs
220+
initial_log_data = vm.log_data
221+
222+
# Set logger to trace level
223+
logger_config = {"level": "Trace"}
224+
response = vm.api.logger.put(**logger_config)
225+
assert response.status_code == 204
226+
227+
# Make API calls to generate traces
228+
for _ in range(3):
229+
response = vm.api.describe_instance.get()
230+
assert response.status_code == 200
231+
232+
# Now should have trace logs
233+
final_log_data = vm.log_data
234+
235+
# Verify no TRACE logs were present initially
236+
assert (
237+
"TRACE" not in initial_log_data
238+
), "Expected no TRACE logs before setting log level to Trace"
239+
240+
# The new log data should contain trace information
241+
new_log_data = final_log_data[len(initial_log_data) :]
242+
assert (
243+
"TRACE" in new_log_data
244+
), "Expected TRACE logs after setting log level to Trace"
245+
246+
finally:
247+
vm.kill()
248+
249+
finally:
250+
cleanup_instrumentation()

0 commit comments

Comments
 (0)