@@ -23,7 +23,7 @@ mod source_ref;
2323// TODO: doesn't need to be exposed if we can clean up the arguments to do_mapping
2424use crate :: progress:: WorkGuard ;
2525use crate :: source_hier:: { ScanEvent , SourceFileID , SourceHierContent , SourceHierTree } ;
26- use crate :: source_ref:: FormatArgument ;
26+ use crate :: source_ref:: { CallSite , FormatArgument } ;
2727pub use code_source:: CodeSource ;
2828pub use log_format:: LogFormat ;
2929pub use progress:: ProgressTracker ;
@@ -142,15 +142,16 @@ impl LogMatcher {
142142 . next ( )
143143 }
144144
145- pub fn find_source_file_statements ( & self , path : & Path ) -> Option < & StatementsInFile > {
146- if let Some ( ( _root_path, src_tree) ) = self . match_path ( path) {
147- src_tree
148- . tree
149- . find_file ( path)
150- . and_then ( |info| src_tree. files_with_statements . get ( & info. id ) )
151- } else {
152- None
153- }
145+ pub fn find_source_file_statements ( & self , path : & Path ) -> Vec < & StatementsInFile > {
146+ self . roots
147+ . values ( )
148+ . flat_map ( |root| {
149+ root. tree
150+ . find_file ( path)
151+ . into_iter ( )
152+ . filter_map ( |( _actual_path, info) | root. files_with_statements . get ( & info. id ) )
153+ } )
154+ . collect ( )
154155 }
155156
156157 /// Traverse the roots looking for supported source files.
@@ -256,11 +257,22 @@ impl LogMatcher {
256257 . sorted_by ( |lhs, rhs| rhs. quality . cmp ( & lhs. quality ) )
257258 . next ( )
258259 {
260+ let exception_trace = match log_ref {
261+ LogRef {
262+ details :
263+ Some ( LogDetails {
264+ trace : Some ( trace) , ..
265+ } ) ,
266+ ..
267+ } => trace. to_exception_trace ( self ) ,
268+ _ => Vec :: new ( ) ,
269+ } ;
259270 let variables = extract_variables ( log_ref, src_ref) ;
260271 return Some ( LogMapping {
261272 log_ref : log_ref. clone ( ) ,
262273 src_ref : Some ( ( * src_ref) . clone ( ) ) ,
263274 variables,
275+ exception_trace,
264276 } ) ;
265277 }
266278 }
@@ -319,7 +331,7 @@ static BACKTRACE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
319331 # Match all stack frames
320332 (?:
321333 # File line: ' File "path", line N, in function'
322- ^\s{2}File\s+\ "[^\ "]*\ ",\s+line\s+\d+,\s+in\s+\S+\s*$\n?
334+ ^\s{2}File\s+"[^"]*",\s+line\s+\d+,\s+in\s+\S+\s*$\n?
323335
324336 # Code line (optional): ' code_here'
325337 (?:^\s{4}.*$\n?)?
@@ -411,7 +423,7 @@ impl SourceLanguage {
411423 }
412424 SourceLanguage :: Java => {
413425 r#"
414- (method_invocation
426+ (method_invocation
415427 object: (identifier) @object-name
416428 name: (identifier) @method-name
417429 arguments: [
@@ -504,6 +516,9 @@ pub struct LogMapping<'a> {
504516 pub log_ref : LogRef < ' a > ,
505517 #[ serde( rename( serialize = "srcRef" ) ) ]
506518 pub src_ref : Option < SourceRef > ,
519+ #[ serde( skip_serializing_if = "Vec::is_empty" ) ]
520+ #[ serde( rename( serialize = "exceptionTrace" ) ) ]
521+ pub exception_trace : Vec < CallSite > ,
507522 pub variables : Vec < VariablePair > ,
508523}
509524
@@ -526,12 +541,88 @@ fn is_only_body(details: &Option<LogDetails>) -> bool {
526541 }
527542}
528543
544+ static PYTHON_CALLER_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
545+ Regex :: new (
546+ r#"(?smx)
547+ (?:
548+ ^\s+File\s+"(?<path>[^"]+)",\s+line\s+(?<line>\d+),\s+in\s+(?<name>[^\n]+)$\n?
549+ )
550+ "# ,
551+ )
552+ . unwrap ( )
553+ } ) ;
554+
555+ static JAVA_CALLER_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
556+ Regex :: new (
557+ r#"(?smx)
558+ (?:
559+ ^\s+at\s+(?<pkg>(?:[^.\n(]+\.)*)(?<class>[^.$\n(]+)\.(?<name>\S+)\((?<file>[^:]+):(?<line>\d+)\)\s*$\n?
560+ )
561+ "# ,
562+ )
563+ . unwrap ( )
564+ } ) ;
565+
529566#[ derive( Copy , Clone , Debug , PartialEq , Serialize ) ]
530567pub struct StackTrace < ' a > {
531568 pub language : SourceLanguage ,
532569 pub content : & ' a str ,
533570}
534571
572+ impl < ' a > StackTrace < ' a > {
573+ fn to_exception_trace ( & self , log_matcher : & LogMatcher ) -> Vec < CallSite > {
574+ let mut retval = Vec :: new ( ) ;
575+ match self . language {
576+ SourceLanguage :: Rust => { }
577+ SourceLanguage :: Java => {
578+ for cap in JAVA_CALLER_REGEX . captures_iter ( self . content ) {
579+ // The Java stack trace does not contain the full path to the source file.
580+ // So, we need to construct a path from the package and class name. Then,
581+ // we use SourceHierTree::find_file() to find the actual path.
582+ let path_for_pkg = cap
583+ . name ( "pkg" )
584+ . map ( |m| PathBuf :: from ( m. as_str ( ) . replace ( "." , "/" ) ) )
585+ . unwrap_or_default ( ) ;
586+ let path_for_class = path_for_pkg. join ( cap. name ( "file" ) . unwrap ( ) . as_str ( ) ) ;
587+ let full_path = log_matcher
588+ . roots
589+ . values ( )
590+ . filter_map ( |root| {
591+ if let Some ( ( actual_path, _source_info) ) =
592+ root. tree . find_file ( & path_for_class) . iter ( ) . next ( )
593+ {
594+ Some ( actual_path. clone ( ) )
595+ } else {
596+ None
597+ }
598+ } )
599+ . next ( ) ;
600+ if let Some ( full_path) = full_path {
601+ retval. push ( CallSite {
602+ name : cap. name ( "name" ) . unwrap ( ) . as_str ( ) . to_string ( ) ,
603+ source_path : full_path. to_string_lossy ( ) . to_string ( ) ,
604+ language : SourceLanguage :: Java ,
605+ line_no : cap. name ( "line" ) . unwrap ( ) . as_str ( ) . parse :: < usize > ( ) . unwrap ( ) ,
606+ } ) ;
607+ }
608+ }
609+ }
610+ SourceLanguage :: Cpp => { }
611+ SourceLanguage :: Python => {
612+ for cap in PYTHON_CALLER_REGEX . captures_iter ( self . content ) {
613+ retval. push ( CallSite {
614+ name : cap. name ( "name" ) . unwrap ( ) . as_str ( ) . to_string ( ) ,
615+ source_path : cap. name ( "path" ) . unwrap ( ) . as_str ( ) . to_string ( ) ,
616+ language : SourceLanguage :: Python ,
617+ line_no : cap. name ( "line" ) . unwrap ( ) . as_str ( ) . parse :: < usize > ( ) . unwrap ( ) ,
618+ } ) ;
619+ }
620+ }
621+ }
622+ retval
623+ }
624+ }
625+
535626#[ derive( Copy , Clone , Debug , PartialEq , Serialize , Default ) ]
536627pub struct LogDetails < ' a > {
537628 #[ serde( skip_serializing_if = "Option::is_none" ) ]
@@ -1088,4 +1179,27 @@ java.lang.IllegalStateException: simulated failure for demo
10881179 let vars = extract_variables ( & log_ref, & src_refs[ 0 ] ) ;
10891180 assert_yaml_snapshot ! ( vars) ;
10901181 }
1182+
1183+ const PYTHON_TRACE : & str = r#"\
1184+ Traceback (most recent call last):
1185+ File "python-logging-example/python_logging_example/__main__.py", line 26, in main
1186+ helper.fail_now()
1187+ ~~~~~~~~~~~~~~~^^
1188+ File "python-logging-example/python_logging_example/helper.py", line 3, in fail_now
1189+ return 1 / 0
1190+ ~~^~~
1191+ ZeroDivisionError: division by zero
1192+ "# ;
1193+
1194+ #[ test]
1195+ fn test_python_trace ( ) {
1196+ let stacktrace = StackTrace {
1197+ language : SourceLanguage :: Python ,
1198+ content : PYTHON_TRACE ,
1199+ } ;
1200+
1201+ let log_matcher = LogMatcher :: new ( ) ;
1202+ let trace = stacktrace. to_exception_trace ( & log_matcher) ;
1203+ assert_yaml_snapshot ! ( trace) ;
1204+ }
10911205}
0 commit comments