Try with exception logging

后端 未结 4 1501
鱼传尺愫
鱼传尺愫 2020-12-28 15:35

Scala\'s Try is very useful.

I\'d like to use that pattern, but log all exceptions.

How can I do this?

相关标签:
4条回答
  • 2020-12-28 15:41

    You used the term "exceptions" which is ambiguous. (java.lang.)Throwable is the root of anything that can be placed behind the throw term. java.lang.Exception is one of the two descendants of Throwable (the other being java.lang.Error). Further making this ambiguous is java.lang.RuntimeException, a descendant of Exception, which is probably where you mostly want to spend your logging time (unless you are doing lower level application framework or hardware driver implementations).

    Assuming you are wanting to log literally ALL instances of Throwable, then you would need something like this (NOT RECOMMENDED):

    def logAtThrowable(f: => A): Try[A] =
      try
        Try(f) match {
          case failure @ Failure(throwable) =>
            log(s"Failure: {throwable.getMessage}")
            failure
          case success @ _ =>
            //uncomment out the next line if you want to also log Success-es
            //log(s"Success: {throwable.getMessage}")
            success
        }
      catch throwable: Throwable => {
        //!NonFatal pathway
        log(s"Failure: {throwable.getMessage}")
        throw throwable
      }
    

    The external try/catch is required to capture all the Throwable instances which are filtered away by scala.util.control.NonFatal within the Try's try/catch block.

    That said...there is a Java/JVM rule: you should never define a catch clause at the resolution of Throwable (again, unless you are doing lower level application framework or hardware driver implementations).

    Following the intention of this rule, you would need to narrow the Throwable to you only emitted logging at the finer grained level, say something more refined, like java.lang.RuntimeException. If so, the code would look like this (recommended):

    def logAtRuntimeException(f: => A): Try[A] =
      Try(f) match {
        case failure @ Failure(throwable) =>
          throwable match {
            case runtimeException: RuntimeException =>
              log(s"Failure: {runtimeException.getMessage}")
          }
          failure
        case success @ _ =>
          success
      }
    

    In both code snippets above, you will notice that I used match as opposed to .recoverWith. This is to facilitate easily adding a rethrow that works. It turns out that all the methods on Try are themselves also wrapped with try/catch blocks. This means that if you want to log the Throwable and then rethrow it, if you are using one of the Try methods like recoverWith, the rethrow is immediately recaught and placed into a Failure thereby completely undermining the value of the intentional rethrow. By using match, the rethrow is guaranteed to succeed as it remains outside any of the Try methods.

    If you would like to see more of the rabbit holes around this particular area, I created a blog post of my own exploration.

    0 讨论(0)
  • 2020-12-28 15:58

    You can tweak it even further using implicit class

    def someMethod[A](f: => A): Try[A] = Try(f)
    
    implicit class LogTry[A](res: Try[A]) {
      def log() = res match {
        case Success(s) => println("Success :) " + s); res
        case Failure(f) => println("Failure :( " + f); res
      }
    }
    

    Now you can call someMethod and on its result call log like this:

    scala> someMethod(1/0).log
    Failure :( java.lang.ArithmeticException: / by zero
    

    and

    scala> someMethod(1).log
    Success :) 1
    

    Of course println method inside implicit class can be substituted with any logging you want.

    0 讨论(0)
  • 2020-12-28 16:05

    Starting Scala 2.13, the chaining operation tap can be used to apply a side effect (in this case some logging) on any value while returning the original value:

    import util.chaining._
    
    val x = Try("aa".toInt).tap(_.failed.foreach(println))
    // java.lang.NumberFormatException: For input string: "aa"
    // x: Try[Int] = Failure(java.lang.NumberFormatException: For input string: "aa")
    

    Or an equivalent pattern matching version:

    val x = Try("aa".toInt).tap { case Failure(e) => println(e) case _ => }
    // java.lang.NumberFormatException: For input string: "aa"
    // x: Try[Int] = Failure(java.lang.NumberFormatException: For input string: "aa")
    

    The tap chaining operation applies a side effect (in this case println or some logging) on a value (in this case a Try) while returning the original unmodified value on which tap is applied (the Try):

    def tap[U](f: (A) => U): A

    0 讨论(0)
  • 2020-12-28 16:07

    Define the following helper:

    import scala.util.{Try, Failure}
    
    def LogTry[A](computation: => A): Try[A] = {
      Try(computation) recoverWith {
        case e: Throwable =>
          log(e)
          Failure(e)
      }
    }
    

    Then you can use it as you would use Try, but any exception will be logged through log(e).

    0 讨论(0)
提交回复
热议问题