|
6 | 6 | mod logs;
|
7 | 7 |
|
8 | 8 | pub use logs::*;
|
| 9 | + |
| 10 | +#[cfg(test)] |
| 11 | +mod tests { |
| 12 | + |
| 13 | + use super::*; |
| 14 | + use opentelemetry_appender_tracing::layer; |
| 15 | + use opentelemetry_sdk::logs::LoggerProviderBuilder; |
| 16 | + use serde_json::{from_str, Value}; |
| 17 | + use std::process::Command; |
| 18 | + use tracing::error; |
| 19 | + use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Layer}; |
| 20 | + |
| 21 | + // Ignore as this cannot be run in Github CI due to lack of |
| 22 | + // required Kernel. Uncomment to run locally in a supported environment |
| 23 | + |
| 24 | + #[ignore] |
| 25 | + #[test] |
| 26 | + fn integration_test_basic() { |
| 27 | + // Run using the below command |
| 28 | + // sudo -E ~/.cargo/bin/cargo test integration -- --nocapture --ignored |
| 29 | + |
| 30 | + // Basic check if user_events are available |
| 31 | + check_user_events_available().expect("Kernel does not support user_events. Verify your distribution/kernel supports user_events: https://docs.kernel.org/trace/user_events.html."); |
| 32 | + |
| 33 | + let logger_provider = LoggerProviderBuilder::default() |
| 34 | + .with_user_event_exporter("myprovider") |
| 35 | + .build(); |
| 36 | + |
| 37 | + // Once provider with user_event exporter is created, it should create the TracePoints |
| 38 | + // following providername_level_k1 format |
| 39 | + // Validate that the TracePoints are created. |
| 40 | + let user_event_status = check_user_events_available().expect("Kernel does not support user_events. Verify your distribution/kernel supports user_events: https://docs.kernel.org/trace/user_events.html."); |
| 41 | + assert!(user_event_status.contains("myprovider_L1K1")); |
| 42 | + assert!(user_event_status.contains("myprovider_L2K1")); |
| 43 | + assert!(user_event_status.contains("myprovider_L3K1")); |
| 44 | + assert!(user_event_status.contains("myprovider_L4K1")); |
| 45 | + assert!(user_event_status.contains("myprovider_L5K1")); |
| 46 | + |
| 47 | + let filter_otel = |
| 48 | + EnvFilter::new("info").add_directive("opentelemetry=off".parse().unwrap()); |
| 49 | + let otel_layer = layer::OpenTelemetryTracingBridge::new(&logger_provider); |
| 50 | + let otel_layer = otel_layer.with_filter(filter_otel); |
| 51 | + let subscriber = tracing_subscriber::registry().with(otel_layer); |
| 52 | + let _guard = tracing::subscriber::set_default(subscriber); |
| 53 | + |
| 54 | + // Start perf recording in a separate thread and emit logs in parallel. |
| 55 | + let perf_thread = |
| 56 | + std::thread::spawn(|| run_perf_and_decode(5, "user_events:myprovider_L2K1")); |
| 57 | + |
| 58 | + // Give a little time for perf to start recording |
| 59 | + std::thread::sleep(std::time::Duration::from_millis(1000)); |
| 60 | + |
| 61 | + // ACT |
| 62 | + error!( |
| 63 | + name: "my-event-name", |
| 64 | + target: "my-target", |
| 65 | + event_id = 20, |
| 66 | + user_name = "otel user", |
| 67 | + user_email = "otel.user@opentelemetry.com" |
| 68 | + ); |
| 69 | + |
| 70 | + // Wait for the perf thread to complete and get the results |
| 71 | + let result = perf_thread.join().expect("Perf thread panicked"); |
| 72 | + |
| 73 | + assert!(result.is_ok()); |
| 74 | + let json_content = result.unwrap(); |
| 75 | + assert!(!json_content.is_empty()); |
| 76 | + |
| 77 | + let formatted_output = json_content.trim().to_string(); |
| 78 | + /* |
| 79 | + // Sample output from perf-decode |
| 80 | + { |
| 81 | + "./perf.data": [ |
| 82 | + { "n": "myprovider:my-event-name", "__csver__": 1024, "PartA": { "time": "2025-03-07T16:31:28.279214367+00:00" }, "PartC": { "user_name": "otel user", "user_email": "otel.user@opentelemetry.com" }, "PartB": { "_typeName": "Log", "severityNumber": 2, "severityText": "ERROR", "eventId": 20, "name": "my-event-name" }, "meta": { "time": 81252.403220286, "cpu": 4, "pid": 21084, "tid": 21085, "level": 2, "keyword": "0x1" } } ] |
| 83 | + } |
| 84 | + */ |
| 85 | + |
| 86 | + let json_value: Value = from_str(&formatted_output).expect("Failed to parse JSON"); |
| 87 | + let perf_data_key = json_value |
| 88 | + .as_object() |
| 89 | + .expect("JSON is not an object") |
| 90 | + .keys() |
| 91 | + .find(|k| k.contains("perf.data")) |
| 92 | + .expect("No perf.data key found in JSON"); |
| 93 | + |
| 94 | + let events = json_value[perf_data_key] |
| 95 | + .as_array() |
| 96 | + .expect("Events for perf.data is not an array"); |
| 97 | + |
| 98 | + // Find the specific event. Its named providername:eventname format. |
| 99 | + let event = events |
| 100 | + .iter() |
| 101 | + .find(|e| { |
| 102 | + if let Some(name) = e.get("n") { |
| 103 | + name.as_str().unwrap_or("") == "myprovider:my-event-name" |
| 104 | + } else { |
| 105 | + false |
| 106 | + } |
| 107 | + }) |
| 108 | + .expect("Event 'myprovider:my-event-name' not found"); |
| 109 | + |
| 110 | + // Validate event structure and fields |
| 111 | + assert_eq!(event["n"].as_str().unwrap(), "myprovider:my-event-name"); |
| 112 | + assert_eq!(event["__csver__"].as_i64().unwrap(), 1024); |
| 113 | + |
| 114 | + // Validate PartA |
| 115 | + let part_a = &event["PartA"]; |
| 116 | + // Only check if the time field exists, not the actual value |
| 117 | + assert!(part_a.get("time").is_some(), "PartA.time is missing"); |
| 118 | + |
| 119 | + // Validate PartB |
| 120 | + let part_b = &event["PartB"]; |
| 121 | + assert_eq!(part_b["_typeName"].as_str().unwrap(), "Log"); |
| 122 | + assert_eq!(part_b["severityNumber"].as_i64().unwrap(), 2); |
| 123 | + assert_eq!(part_b["severityText"].as_str().unwrap(), "ERROR"); |
| 124 | + assert_eq!(part_b["eventId"].as_i64().unwrap(), 20); |
| 125 | + assert_eq!(part_b["name"].as_str().unwrap(), "my-event-name"); |
| 126 | + |
| 127 | + // Validate PartC |
| 128 | + let part_c = &event["PartC"]; |
| 129 | + assert_eq!(part_c["user_name"].as_str().unwrap(), "otel user"); |
| 130 | + assert_eq!( |
| 131 | + part_c["user_email"].as_str().unwrap(), |
| 132 | + "otel.user@opentelemetry.com" |
| 133 | + ); |
| 134 | + } |
| 135 | + |
| 136 | + fn check_user_events_available() -> Result<String, String> { |
| 137 | + let output = Command::new("sudo") |
| 138 | + .arg("cat") |
| 139 | + .arg("/sys/kernel/tracing/user_events_status") |
| 140 | + .output() |
| 141 | + .map_err(|e| format!("Failed to execute command: {}", e))?; |
| 142 | + |
| 143 | + if output.status.success() { |
| 144 | + let status = String::from_utf8_lossy(&output.stdout); |
| 145 | + Ok(status.to_string()) |
| 146 | + } else { |
| 147 | + Err(format!( |
| 148 | + "Command executed with failing error code: {}", |
| 149 | + String::from_utf8_lossy(&output.stderr) |
| 150 | + )) |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + pub fn run_perf_and_decode(duration_secs: u64, event: &str) -> std::io::Result<String> { |
| 155 | + // Run perf record for duration_secs seconds |
| 156 | + let perf_status = Command::new("sudo") |
| 157 | + .args([ |
| 158 | + "timeout", |
| 159 | + "-s", |
| 160 | + "SIGINT", |
| 161 | + &duration_secs.to_string(), |
| 162 | + "perf", |
| 163 | + "record", |
| 164 | + "-e", |
| 165 | + event, |
| 166 | + ]) |
| 167 | + .status()?; |
| 168 | + |
| 169 | + if !perf_status.success() { |
| 170 | + // Check if it's the expected signal termination (SIGINT from timeout) |
| 171 | + // timeout sends SIGINT, which will cause a non-zero exit code (130 typically) |
| 172 | + if !matches!(perf_status.code(), Some(124) | Some(130) | Some(143)) { |
| 173 | + panic!( |
| 174 | + "perf record failed with exit code: {:?}", |
| 175 | + perf_status.code() |
| 176 | + ); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + // Change permissions on perf.data (which is the default file perf records to) to allow reading |
| 181 | + let chmod_status = Command::new("sudo") |
| 182 | + .args(["chmod", "uog+r", "./perf.data"]) |
| 183 | + .status()?; |
| 184 | + |
| 185 | + if !chmod_status.success() { |
| 186 | + panic!("chmod failed with exit code: {:?}", chmod_status.code()); |
| 187 | + } |
| 188 | + |
| 189 | + // Decode the performance data and return it directly |
| 190 | + // Note: This tool must be installed on the machine |
| 191 | + // git clone https://github.com/microsoft/LinuxTracepoints && |
| 192 | + // cd LinuxTracepoints && mkdir build && cd build && cmake .. && make && |
| 193 | + // sudo cp bin/perf-decode /usr/local/bin && |
| 194 | + let decode_output = Command::new("perf-decode").args(["./perf.data"]).output()?; |
| 195 | + |
| 196 | + if !decode_output.status.success() { |
| 197 | + panic!( |
| 198 | + "perf-decode failed with exit code: {:?}", |
| 199 | + decode_output.status.code() |
| 200 | + ); |
| 201 | + } |
| 202 | + |
| 203 | + // Convert the output to a String |
| 204 | + let raw_output = String::from_utf8_lossy(&decode_output.stdout).to_string(); |
| 205 | + |
| 206 | + // Remove any Byte Order Mark (BOM) characters |
| 207 | + // UTF-8 BOM is EF BB BF (in hex) |
| 208 | + let cleaned_output = if let Some(stripped) = raw_output.strip_prefix('\u{FEFF}') { |
| 209 | + // Skip the BOM character |
| 210 | + stripped.to_string() |
| 211 | + } else { |
| 212 | + raw_output |
| 213 | + }; |
| 214 | + |
| 215 | + // Trim the output to remove any leading/trailing whitespace |
| 216 | + let trimmed_output = cleaned_output.trim().to_string(); |
| 217 | + |
| 218 | + Ok(trimmed_output) |
| 219 | + } |
| 220 | +} |
0 commit comments