11use bestool_psql:: history:: History ;
22use clap:: { Parser , Subcommand } ;
3+ use jiff:: Timestamp ;
34use lloggs:: { LoggingArgs , PreArgs , WorkerGuard } ;
45use miette:: { miette, IntoDiagnostic , Result } ;
6+ use serde:: Serialize ;
57use std:: path:: PathBuf ;
68use tracing:: debug;
79
@@ -122,7 +124,7 @@ fn main() -> Result<()> {
122124 }
123125
124126 for ( timestamp, entry) in entries {
125- let datetime = timestamp_to_datetime ( timestamp) ;
127+ let datetime = timestamp_to_rfc3339 ( timestamp) ;
126128 let mode = if entry. writemode { "WRITE" } else { "READ" } ;
127129 println ! (
128130 "[{}] {} - db={} sys={}" ,
@@ -149,7 +151,7 @@ fn main() -> Result<()> {
149151 }
150152
151153 for ( timestamp, entry) in entries {
152- let datetime = timestamp_to_datetime ( timestamp) ;
154+ let datetime = timestamp_to_rfc3339 ( timestamp) ;
153155 let mode = if entry. writemode { "WRITE" } else { "READ" } ;
154156 println ! (
155157 "[{}] {} - db:{} sys:{}" ,
@@ -211,8 +213,8 @@ fn main() -> Result<()> {
211213 }
212214 }
213215
214- let oldest = timestamp_to_datetime ( entries. first ( ) . unwrap ( ) . 0 ) ;
215- let newest = timestamp_to_datetime ( entries. last ( ) . unwrap ( ) . 0 ) ;
216+ let oldest = timestamp_to_rfc3339 ( entries. first ( ) . unwrap ( ) . 0 ) ;
217+ let newest = timestamp_to_rfc3339 ( entries. last ( ) . unwrap ( ) . 0 ) ;
216218
217219 println ! ( "History Statistics" ) ;
218220 println ! ( "==================" ) ;
@@ -247,53 +249,125 @@ fn main() -> Result<()> {
247249 Commands :: Export { output } => {
248250 let entries = history. list ( ) ?;
249251
250- let json = serde_json:: to_string_pretty ( & entries) . into_diagnostic ( ) ?;
252+ // Convert entries to export format with RFC3339 timestamps
253+ let export_entries: Vec < ExportEntry > = entries
254+ . into_iter ( )
255+ . map ( |( timestamp, entry) | ExportEntry {
256+ ts : timestamp_to_rfc3339 ( timestamp) ,
257+ query : entry. query ,
258+ db_user : entry. db_user ,
259+ sys_user : entry. sys_user ,
260+ writemode : entry. writemode ,
261+ tailscale : entry. tailscale ,
262+ ots : entry. ots ,
263+ } )
264+ . collect ( ) ;
251265
252266 if let Some ( path) = output {
253- std:: fs:: write ( path, json) . into_diagnostic ( ) ?;
267+ use std:: io:: Write ;
268+ let mut file = std:: fs:: File :: create ( path) . into_diagnostic ( ) ?;
269+ for entry in export_entries {
270+ let json = serde_json:: to_string ( & entry) . into_diagnostic ( ) ?;
271+ writeln ! ( file, "{}" , json) . into_diagnostic ( ) ?;
272+ }
254273 println ! ( "History exported." ) ;
255274 } else {
256- println ! ( "{}" , json) ;
275+ for entry in export_entries {
276+ let json = serde_json:: to_string ( & entry) . into_diagnostic ( ) ?;
277+ println ! ( "{}" , json) ;
278+ }
257279 }
258280 }
259281 }
260282
261283 Ok ( ( ) )
262284}
263285
264- fn timestamp_to_datetime ( micros : u64 ) -> String {
265- use std:: time:: { Duration , UNIX_EPOCH } ;
266-
267- let duration = Duration :: from_micros ( micros) ;
268- let system_time = UNIX_EPOCH + duration;
269-
270- if let Ok ( duration_since_epoch) = system_time. duration_since ( UNIX_EPOCH ) {
271- let secs = duration_since_epoch. as_secs ( ) ;
272- let nanos = duration_since_epoch. subsec_nanos ( ) ;
273-
274- let days_since_epoch = secs / 86400 ;
275- let seconds_today = secs % 86400 ;
276- let hours = seconds_today / 3600 ;
277- let minutes = ( seconds_today % 3600 ) / 60 ;
278- let seconds = seconds_today % 60 ;
279-
280- let year = 1970 + ( days_since_epoch / 365 ) as i32 ;
281- let day_of_year = ( days_since_epoch % 365 ) as u32 ;
282-
283- let month = 1 + ( day_of_year / 30 ) . min ( 11 ) ;
284- let day = 1 + ( day_of_year % 30 ) . min ( 30 ) ;
285-
286- format ! (
287- "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:06}" ,
288- year,
289- month,
290- day,
291- hours,
292- minutes,
293- seconds,
294- nanos / 1000
295- )
296- } else {
297- format ! ( "{}" , micros)
286+ /// Export entry format with RFC3339 timestamp
287+ #[ derive( Debug , Serialize ) ]
288+ struct ExportEntry {
289+ ts : String ,
290+ query : String ,
291+ db_user : String ,
292+ sys_user : String ,
293+ writemode : bool ,
294+ #[ serde( skip_serializing_if = "Vec::is_empty" ) ]
295+ tailscale : Vec < bestool_psql:: history:: TailscalePeer > ,
296+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
297+ ots : Option < String > ,
298+ }
299+
300+ fn timestamp_to_rfc3339 ( micros : u64 ) -> String {
301+ // Convert microseconds to seconds and nanoseconds
302+ let secs = ( micros / 1_000_000 ) as i64 ;
303+ let nanos = ( ( micros % 1_000_000 ) * 1_000 ) as i32 ;
304+
305+ // Create timestamp and format as RFC3339
306+ Timestamp :: new ( secs, nanos)
307+ . map ( |ts| ts. to_string ( ) )
308+ . unwrap_or_else ( |_| format ! ( "invalid-timestamp-{}" , micros) )
309+ }
310+
311+ #[ cfg( test) ]
312+ mod tests {
313+ use super :: * ;
314+
315+ #[ test]
316+ fn test_timestamp_to_rfc3339 ( ) {
317+ // Test a known timestamp: 2024-01-15 13:10:45.123456 UTC
318+ // = 1705324245123456 microseconds since epoch
319+ let micros = 1705324245123456 ;
320+ let rfc3339 = timestamp_to_rfc3339 ( micros) ;
321+
322+ // Should be in RFC3339 format with T separator and Z timezone
323+ assert_eq ! ( rfc3339, "2024-01-15T13:10:45.123456Z" ) ;
324+ }
325+
326+ #[ test]
327+ fn test_export_entry_serialization ( ) {
328+ let entry = ExportEntry {
329+ ts : "2024-01-15T12:30:45.123456Z" . to_string ( ) ,
330+ query : "SELECT * FROM users;" . to_string ( ) ,
331+ db_user : "postgres" . to_string ( ) ,
332+ sys_user : "alice" . to_string ( ) ,
333+ writemode : false ,
334+ tailscale : vec ! [ ] ,
335+ ots : None ,
336+ } ;
337+
338+ let json = serde_json:: to_string ( & entry) . unwrap ( ) ;
339+
340+ // Should be compact (single line)
341+ assert ! ( !json. contains( '\n' ) ) ;
342+
343+ // Should contain expected fields
344+ assert ! ( json. contains( "\" ts\" " ) ) ;
345+ assert ! ( json. contains( "\" query\" " ) ) ;
346+ assert ! ( json. contains( "\" db_user\" " ) ) ;
347+ assert ! ( json. contains( "\" sys_user\" " ) ) ;
348+ assert ! ( json. contains( "\" writemode\" " ) ) ;
349+
350+ // Should NOT contain empty tailscale or null ots (due to skip_serializing_if)
351+ assert ! ( !json. contains( "\" tailscale\" " ) ) ;
352+ assert ! ( !json. contains( "\" ots\" " ) ) ;
353+ }
354+
355+ #[ test]
356+ fn test_export_entry_with_ots ( ) {
357+ let entry = ExportEntry {
358+ ts : "2024-01-15T12:30:45.123456Z" . to_string ( ) ,
359+ query : "INSERT INTO logs VALUES (1);" . to_string ( ) ,
360+ db_user : "postgres" . to_string ( ) ,
361+ sys_user : "alice" . to_string ( ) ,
362+ writemode : true ,
363+ tailscale : vec ! [ ] ,
364+ ots : Some ( "bob-watching" . to_string ( ) ) ,
365+ } ;
366+
367+ let json = serde_json:: to_string ( & entry) . unwrap ( ) ;
368+
369+ // Should contain ots when present
370+ assert ! ( json. contains( "\" ots\" " ) ) ;
371+ assert ! ( json. contains( "bob-watching" ) ) ;
298372 }
299373}
0 commit comments