Rust Instrumentation: Custom Metric Collection
Building robust software requires understanding its performance and behavior under various conditions. Instrumentation is the process of embedding code within your application to collect metrics that provide insights into its execution. This challenge will guide you through creating a basic instrumentation system in Rust to track function call counts and execution durations.
Problem Description
Your task is to implement a simple instrumentation library in Rust that allows developers to automatically track two key metrics for their functions:
- Call Count: The number of times a specific function has been invoked.
- Execution Duration: The total time spent executing a specific function.
You need to design a system that can be easily applied to existing Rust functions with minimal code modification. The system should be thread-safe and capable of reporting the collected metrics.
Key Requirements:
- Macro-based Instrumentation: Use Rust macros to automatically wrap functions with instrumentation logic. This should be the primary way users will instrument their code.
- Metric Storage: Maintain a thread-safe collection of metrics. Each metric should be associated with a unique identifier (e.g., the function name).
- Timing Mechanism: Accurately measure the execution time of instrumented functions.
- Reporting: Provide a mechanism to retrieve and display the collected metrics.
- Thread Safety: Ensure that concurrent calls to instrumented functions do not lead to data races or incorrect metric collection.
Expected Behavior:
When a function is decorated with your instrumentation macro, each invocation should:
- Increment a counter for that function.
- Record the start time before the function's original code executes.
- Record the end time after the function's original code completes.
- Calculate the duration and add it to a running total for that function.
The reporting mechanism should output a summary of all collected metrics.
Edge Cases:
- Functions that panic: The instrumentation should still capture the end time and report the duration up to the point of panic.
- Very short execution times: Ensure the timing mechanism can handle sub-millisecond durations accurately.
- Concurrent execution: The system must handle multiple threads calling the same instrumented function simultaneously.
Examples
Example 1: Basic Function Instrumentation
Input:
use instrumentation_lib::{instrument, report_metrics};
use std::thread;
use std::time::Duration;
#[instrument("slow_operation")]
fn slow_operation(n: u32) -> u32 {
thread::sleep(Duration::from_millis(50));
n * 2
}
fn main() {
slow_operation(10);
slow_operation(20);
thread::sleep(Duration::from_millis(100)); // Simulate some other work
slow_operation(30);
report_metrics();
}
Output:
Metrics Report:
--------------------
Function: slow_operation
Calls: 3
Total Duration: XXX ms (approximately, will vary slightly)
--------------------
Explanation: The slow_operation function is decorated with #[instrument("slow_operation")]. It is called three times. The output shows that it was called 3 times and the total time spent executing it across all calls.
Example 2: Multiple Instrumented Functions
Input:
use instrumentation_lib::{instrument, report_metrics};
use std::thread;
use std::time::Duration;
#[instrument("fetch_data")]
fn fetch_data() {
thread::sleep(Duration::from_millis(75));
}
#[instrument("process_data")]
fn process_data() {
thread::sleep(Duration::from_millis(25));
}
fn main() {
fetch_data();
process_data();
fetch_data();
report_metrics();
}
Output:
Metrics Report:
--------------------
Function: fetch_data
Calls: 2
Total Duration: XXX ms (approximately)
--------------------
Function: process_data
Calls: 1
Total Duration: XXX ms (approximately)
--------------------
Explanation: Both fetch_data and process_data are instrumented. The report correctly aggregates metrics for each function independently.
Example 3: Concurrent Calls
Input:
use instrumentation_lib::{instrument, report_metrics};
use std::thread;
use std::time::Duration;
#[instrument("concurrent_task")]
fn concurrent_task(id: u32) {
let sleep_duration = if id % 2 == 0 { 100 } else { 50 };
thread::sleep(Duration::from_millis(sleep_duration));
}
fn main() {
let mut handles = vec![];
for i in 0..5 {
let handle = thread::spawn(move || {
concurrent_task(i);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
report_metrics();
}
Output:
Metrics Report:
--------------------
Function: concurrent_task
Calls: 5
Total Duration: XXX ms (approximately, sum of all sleep durations)
--------------------
Explanation: Five threads concurrently call concurrent_task. The instrumentation library correctly handles concurrent access to update the metrics for concurrent_task without issues. The total duration will reflect the sum of all individual execution times.
Constraints
- The instrumentation macro should be named
instrument. It should accept a string literal for the metric identifier. - The reporting function should be named
report_metrics. - The solution must use
std::time::Instantfor timing. - The metric storage must be thread-safe, using mechanisms like
MutexorRwLockfromstd::sync. - The solution should aim for minimal overhead when functions are not being instrumented (though this is less of a concern for this basic challenge).
- The reported durations should ideally be in milliseconds, with reasonable precision.
Notes
- Consider how to define and share the metric storage globally. A static mutable variable protected by a mutex is a common pattern.
- The
proc_macrocrate will be essential for implementing the#[instrument]attribute macro. - Think about how to capture the function name or a user-provided identifier to distinguish metrics from different functions.
- The
Instant::now()method gives you a monotonic clock, which is suitable for measuring durations. - For reporting, you can iterate over your collected metrics and print them to standard output.
- Handling panics within instrumented functions is crucial for robust instrumentation. Consider using
std::panic::catch_unwindif necessary, though for this challenge, simply ensuring the end time is recorded is a good start.