Skip to content

Commit 035e8fc

Browse files
authored
UserEvents - Integration Test (#196)
1 parent 1c7f22e commit 035e8fc

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed

opentelemetry-user-events-logs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ tracing-core = "0.1.31"
2626
tracing-subscriber = { version = "0.3.0", default-features = false, features = ["env-filter", "fmt", "registry", "std"] }
2727
ctrlc = "3.4"
2828
criterion = "0.5"
29+
serde_json = "1.0.140"
2930

3031
[features]
3132
spec_unstable_logs_enabled = ["opentelemetry/spec_unstable_logs_enabled", "opentelemetry_sdk/spec_unstable_logs_enabled", "opentelemetry-appender-tracing/spec_unstable_logs_enabled"]

opentelemetry-user-events-logs/src/lib.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,215 @@
66
mod logs;
77

88
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

Comments
 (0)