@@ -23,14 +23,41 @@ use tracing_subscriber::{layer::Layer, registry::LookupSpan};
2323use crate :: command_kind:: CommandKind ;
2424use trident_api:: error:: TridentError ;
2525
26+ /// Reads `CLOCK_BOOTTIME` (nanosecond-resolution time since boot,
27+ /// including any suspended time -- unlike `sysinfo::System::uptime()` or a
28+ /// naive `/proc/uptime` parse, which only expose whole-second resolution)
29+ /// and converts it into the `trident_start` uptime metric value. Used by
30+ /// `Trident::new`'s startup telemetry (see the `trident_start` call site
31+ /// in `lib.rs`), which passes it the live `nix::time::clock_gettime`
32+ /// result.
33+ ///
34+ /// Best-effort: `clock_gettime` with a valid clock ID essentially never
35+ /// fails on Linux, but falls back to `f64::NAN` -- which `json!()`
36+ /// serializes as JSON `null`, a genuine "not available" rather than a
37+ /// misleading literal zero (see
38+ /// `test_clock_boottime_uptime_secs_nan_fallback_serializes_as_json_null`
39+ /// below) -- rather than failing startup if it somehow does.
40+ pub ( crate ) fn clock_boottime_uptime_secs (
41+ clock_read : Result < nix:: sys:: time:: TimeSpec , nix:: errno:: Errno > ,
42+ ) -> f64 {
43+ clock_read
44+ . map ( |ts| std:: time:: Duration :: from ( ts) . as_secs_f64 ( ) )
45+ . unwrap_or_else ( |e| {
46+ warn ! ( "Failed to read CLOCK_BOOTTIME: {e}" ) ;
47+ f64:: NAN
48+ } )
49+ }
50+
2651use osutils:: {
2752 dmi, files,
2853 osrelease:: { OsRelease , OS_RELEASE_PATH } ,
29- uname,
54+ uname, virt ,
3055} ;
56+ use sysdefs:: arch:: SystemArchitecture ;
3157
3258use crate :: {
33- datastore:: DataStore , logging:: operation_context, TRIDENT_METRICS_FILE_PATH , TRIDENT_VERSION ,
59+ datastore:: DataStore , init:: acl, logging:: operation_context, TRIDENT_METRICS_FILE_PATH ,
60+ TRIDENT_VERSION ,
3461} ;
3562
3663lazy_static:: lazy_static! {
@@ -802,6 +829,30 @@ fn populate_platform_info() -> BTreeMap<String, Value> {
802829 "unknown" . to_string ( )
803830 } ) ;
804831 platform_info. insert ( "kernel_version" . to_string ( ) , json ! ( kernel_release. trim( ) ) ) ;
832+
833+ // Whether this host is virtualized (see `osutils::virt` for the
834+ // detection heuristic and its caveats).
835+ platform_info. insert ( "vm" . to_string ( ) , json ! ( virt:: is_virtual( ) ) ) ;
836+
837+ let arch: & ' static str = SystemArchitecture :: current ( ) . into ( ) ;
838+ platform_info. insert ( "arch" . to_string ( ) , json ! ( arch) ) ;
839+ // Best-effort: a failure to determine whether this is an ACL (Azure
840+ // Container Linux) host must never fail startup, it only means this
841+ // one field is missing from every telemetry event for this
842+ // invocation. A detection failure is reported as "unknown", not
843+ // "false" -- conflating "known non-ACL" with "couldn't tell" would
844+ // misclassify a host whose ACL check simply failed to run as
845+ // definitively non-ACL.
846+ let acl = match acl:: is_acl ( ) {
847+ Ok ( true ) => "true" ,
848+ Ok ( false ) => "false" ,
849+ Err ( e) => {
850+ warn ! ( "Failed to determine if host is running ACL: {e:?}" ) ;
851+ "unknown"
852+ }
853+ } ;
854+ platform_info. insert ( "acl" . to_string ( ) , json ! ( acl) ) ;
855+
805856 platform_info
806857}
807858
@@ -937,6 +988,63 @@ mod tests {
937988 ) ;
938989 }
939990
991+ #[ test]
992+ /// Regression test for a Copilot review finding on PR #778: claimed
993+ /// that `clock_boottime_uptime_secs`'s `f64::NAN` fallback, passed
994+ /// through `tracing::info!(value = ...)` to
995+ /// `TraceEntryVisitor::record_f64`'s `json!(value)`, would make
996+ /// `serde_json` unable to serialize the entry, panicking instead of
997+ /// staying best-effort. Calls the exact function `lib.rs`'s
998+ /// `trident_start` metric uses (see its call site), forcing the
999+ /// failure branch with a synthetic `Errno`, then sends the resulting
1000+ /// value through the real tracing -> visitor -> file-write pipeline
1001+ /// (not just a standalone `serde_json` snippet): `json!(f64)` goes
1002+ /// through `Value::from(f64)`, which maps non-finite floats to
1003+ /// `Value::Null` rather than erroring, so the written line contains a
1004+ /// valid JSON `"value":null` and the file write completes normally.
1005+ fn test_clock_boottime_uptime_secs_nan_fallback_serializes_as_json_null ( ) {
1006+ let uptime_secs = clock_boottime_uptime_secs ( Err ( nix:: errno:: Errno :: EINVAL ) ) ;
1007+ assert ! (
1008+ uptime_secs. is_nan( ) ,
1009+ "a failed clock read must fall back to NaN, not a misleading literal value"
1010+ ) ;
1011+
1012+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1013+ let metrics_path = temp_dir. path ( ) . join ( "metrics.jsonl" ) ;
1014+ let tracestream = TraceStream :: default ( ) ;
1015+ let trace_sender = tracestream
1016+ . make_trace_sender_with_metrics_path ( metrics_path. to_str ( ) . unwrap ( ) )
1017+ . with_filter ( filter:: LevelFilter :: INFO ) ;
1018+
1019+ let _guard = tracing:: subscriber:: set_default (
1020+ tracing_subscriber:: Registry :: default ( ) . with ( trace_sender) ,
1021+ ) ;
1022+
1023+ tracing:: info!( metric_name = "trident_start" , value = uptime_secs) ;
1024+
1025+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
1026+
1027+ let file = File :: open ( & metrics_path) . unwrap ( ) ;
1028+ let reader = BufReader :: new ( file) ;
1029+ let lines: Vec < String > = reader. lines ( ) . map ( |l| l. unwrap ( ) ) . collect ( ) ;
1030+
1031+ let entry_line = lines
1032+ . iter ( )
1033+ . find ( |line| line. contains ( r#""metric_name":"trident_start""# ) )
1034+ . expect ( "expected trident_start metric not found in the local metrics file" ) ;
1035+
1036+ // Confirm the line is valid, parseable JSON (i.e. writing a NaN
1037+ // value never broke serialization) and that the value field landed
1038+ // as JSON null rather than a literal 0 or a serialization failure.
1039+ let parsed: serde_json:: Value =
1040+ serde_json:: from_str ( entry_line) . expect ( "trace entry line must be valid JSON" ) ;
1041+ assert_eq ! (
1042+ parsed. get( "value" ) ,
1043+ Some ( & Value :: Null ) ,
1044+ "NaN metric value should serialize as JSON null, not a literal 0 or an error"
1045+ ) ;
1046+ }
1047+
9401048 #[ test]
9411049 /// Regression test: `TraceStream::set_installation_id` must actually
9421050 /// reach the serialized trace entry's `additional_fields.installation_id`
@@ -1309,13 +1417,31 @@ mod functional_test {
13091417 "kernel_version" . to_string ( ) ,
13101418 json ! ( uname:: kernel_release( ) . unwrap( ) . trim( ) ) ,
13111419 ) ;
1420+ expected_platform_info. insert ( "vm" . to_string ( ) , json ! ( virt:: is_virtual( ) ) ) ;
13121421
13131422 // Call the function to get the actual result.
13141423 let platform_info = populate_platform_info ( ) ;
13151424
1316- // Assert that the actual result matches the expected result.
1425+ let expected_arch: & ' static str = SystemArchitecture :: current ( ) . into ( ) ;
1426+ assert_eq ! ( platform_info. get( "arch" ) . unwrap( ) , & json!( expected_arch) ) ;
1427+ // Host-dependent (like the fields above): just assert the field is
1428+ // present and one of the values `acl::is_acl()` can actually
1429+ // produce, rather than a fixed expectation, since whether the VM
1430+ // running this test is an ACL host isn't controlled by the test.
1431+ let acl = platform_info. get ( "acl" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
1432+ assert ! (
1433+ [ "true" , "false" , "unknown" ] . contains( & acl) ,
1434+ "unexpected acl value: {acl}"
1435+ ) ;
1436+
1437+ // Assert that the actual result matches the expected result for
1438+ // every field with a fixed expectation (arch/acl are asserted
1439+ // separately above since they are host-dependent).
1440+ let mut actual_platform_info = platform_info;
1441+ actual_platform_info. remove ( "arch" ) ;
1442+ actual_platform_info. remove ( "acl" ) ;
13171443 assert_eq ! (
1318- platform_info , expected_platform_info,
1444+ actual_platform_info , expected_platform_info,
13191445 "Platform info does not match the expected result"
13201446 ) ;
13211447 }
0 commit comments