rchillyard / flog   1.0.8

MIT License GitHub

This repository is for Flog, the functional logger.

Scala versions: 2.13

CircleCI Maven Central Codacy Badge GitHub Top Languages GitHub GitHub last commit GitHub issues GitHub issues by-label

Flog

This is a set of utilities for functional logging. Flog is not yet released to maven central, but you may copy the jar files, etc. from the releases directory.

Introduction and Usage

Flog is a functional logger: That's to say that Flog is expression-oriented rather than statement-oriented.

In a statement-oriented language such as Java, it is reasonably convenient to add an extra logging line to a method or block. However, when writing functional programs, it's very inconvenient to be forced to break up the flow and perhaps declare a value, then log the value, then continue to use the value. Therefore, in this functional logger, we write loggable expressions which yield a value and, as a side effect-- which the rest of the program doesn't "see" -- we do the logging (footnote 1).

We define an instance of Flog, import its properties, and then use the !! method (actually a method on an implicit inner class of Flog called Floggable which has a String parameter in its constructor).

The basic usage pattern is thus:

val flog = Flog[MyClass]
import flog._
val x: X = msg !! expr

where msg evaluates to a String and expr evaluates to a value of type X which will be assigned to x (footnote 2) while, as a side effect, the value of expr is logged at level INFO. In other words, if you take away the "msg !!" the program will work exactly the same, but without the side effect of logging.

Because we want to control the way a log message looks, we define the trait Loggable[X] which is a type constructor. In particular, we need reasonably brief but informative strings. Specific loggable behaviors are defined, therefore, in implicit objects. Those that are provided by Flog are defined in the Loggable companion object (see below). For example, when logging an iterable, we show the start and end of the list, and simply count the intervening number. A further advantage of this mechanism is that we can define the various methods involving Loggable to be call-by-name (i.e. non-strict). This avoids constructing the string when logging is turned off.

In addition to the !! method, there is also a !? method, which logs at DEBUG level, and a !?? method for TRACE level. Additionally, there are the methods in words: trace (synonym of !??), debug (synonym of !?), info (synonym of !!), warn, and error. Additionally, there is also !| for logging a generic type that isn't necessarily Loggable. In this case, we simply invoke toString on the object to get a rendition for logging. There's also a |! method which ignores the message and does no logging at all. This is useful if you want to temporarily suspend a particular logging construct without removing the instrumentation.

The following signatures are defined for Floggable (the implicit class):

def !![X: Loggable](x: => X): X
def !![X: Loggable](x: => Iterable[X]): Iterable[X]
def !![X: Loggable](x: => Option[X]): Option[X]
def !![K: Loggable, V: Loggable](kVm: => Map[K,V]): Map[K,V]
def !![X: Loggable](xf: => Future[X])(implicit ec: ExecutionContext): Future[X]

These same five signatures (above) are also available for !? (debug) and !!? (trace).

def !|[X](x: => X): X // logs using x.toString
def |![X](x: => X): X // does no logging
def !!![X: Loggable](xy: Try[X]): Try[X]

The signature which takes an Iterable[X] does require some further discussion. If there are sufficient elements, the first three elements and the last element are shown in the log message. Only the number of non-logged elements is shown between them. This method is also invoked by the !!(Map) method, seeing an Iterable[(String, String)].

In the case of non-strict collections, no unnecessary evaluation is performed. Views are left as is and LazyLists are shown as lists only if they have definite size.

The last-named (!!!) method does not return the input exactly as is (as all the other methods do). If xy is a Failure(e) then it logs the exception as an error and returns Failure(LoggedException(e)). This allows for the code to avoid logging the exception twice.

For all these !! logging mechanism to work, there must be (implicit) evidence of Loggable[X] available. The following standard Loggables are provided:

implicit object LoggableBoolean extends Loggable[Boolean]
implicit object LoggableByte extends Loggable[Byte]
implicit object LoggableShort extends Loggable[Short]
implicit object LoggableInt extends Loggable[Int]
implicit object LoggableLong extends Loggable[Long]
implicit object LoggableBigInt extends Loggable[BigInt]
implicit object LoggableString extends Loggable[String]
implicit object LoggableDouble extends Loggable[Double]
implicit object LoggableBigDecimal extends Loggable[BigDecimal]
implicit object LoggableUnit extends Loggable[Unit]

Additionally, for those container types which are not explicitly handled by the !! method signatures, there is support, in Loggables, for various specific types of containers to be logged where, in each case, the parametric types T, L, R, K, or V must provide implicit evidence of type Loggable[T], etc.:

def optionLoggable[T: Loggable]: Loggable[Option[T]]
def iterableLoggable[T: Loggable]: Loggable[Iterable[T]]
def mapLoggable[K, T: Loggable]: Loggable[Map[K, T]] 
def tryLoggable[T: Loggable]: Loggable[Try[T]]
def eitherLoggable[L: Loggable, R: Loggable]: Loggable[Either[L, R]]
def kVLoggable[K: Loggable, V: Loggable]: Loggable[(K, V)]

There is also a method which can be used for any underlying type that for which there is no explicit loggable method:

def anyLoggable[T]: Loggable[T]

Additionally, Loggables defines a set of methods for creating Loggable[P] where P is a Product, that's to say a case class, or a tuple, with a particular number of members (fields). Each member type must itself be Loggable ("standard" types such as Int and String--see above for definition--will implicitly find the appropriate instance). If a member is of a non-standard type, you will need to define an implicit val with the appropriate method from Loggables (see above). For these methods, all you have to do is include a reference to the apply method of the case class (you can skip the ".apply" if you haven't defined an explicit companion object). If you are familiar with, for instance, reading/writing Json, you should be comfortable with this idea.

Each of these "loggableN" methods has a signature thus (using loggable3 as an exemplar):

def loggable3[P0: Loggable, P1: Loggable, P2: Loggable, T <: Product : ClassTag]
    (construct: (P0, P1, P2) => T, fields: Seq[String] = Nil): Loggable[T]

There are currently 9 such methods defined (loggable1 thru loggable9). Here is the specification used to test this particular method:

case class Threesy(x: Int, y: Boolean, z: Double)
val target = loggable3(Threesy)
target.toLog(Threesy(42, y = true, 3.1415927)) shouldBe "Threesy(x:42,y:true,z:3.1415927)"

In some situations, the reflection code is unable to get the field names in order (for example when there are public lazy vals). In such a case, add the second parameter (after the function) to explicitly give the field names in order. Normally, of course, you can leave this parameter unset.

Please see worksheets/FlogExamples.sc for examples of usage. Additionally, see any of the spec files, especially FlogSpec for more definition on how to use the package.

If you wish to make a class loggable which is not a case class (or other Product), then you can do it something like the following (basically you must define the toLog method):

class Complex(val real: Double, val imag: Double)
object Complex {
  trait LoggableComplex extends Loggable[Complex] {
    def toLog(t: Complex): String = s"${t.real} + i${t.imag}"
  }
  implicit object LoggableComplex extends LoggableComplex
}

Variations

Flog is a case class which has one member: logger which is of type Logger. In normal usage, the logger will be of type Slf4jLogger and will be derived from the org.slf4j.Logger for the particular class specified. However, you can also provide other loggers, particularly of the type GenericLogger or AppendableLogger. This is a case class with a member of type LogFunction, a trait with the following method definition:

def apply(w: => String): Unit

There is a GenericLogFunction type which implements this trait. However, if you do want to provide your own, then you need to understand their type, another case class:

case class GenericLogFunction(f: String => Any, enabled: Boolean = true) extends LogFunction

Additionally, there is a type of Logger called AppendableLogger:

case class AppendableLogger(appendable: Appendable with AutoCloseable with Flushable) extends Logger

If you use this type, you should run invoke it something like the following:

Using(Flog(System.out)) {
  f =>
    import f._
    message info x
}

Furthermore, there is also (primarily for unit testing) a type of Logger called StringBuilderLogger:

case class StringBuilderLogger(sb: StringBuilder) extends Logger

It is also possible to change the behavior of the Flog instance by invoking one of the methods:

def disabled: Flog
def withLogger(logger: Logger): Flog

Dependencies

For the default logging function, we include the following dependencies:

"org.slf4j" % "slf4j-api" % "1.7.30",
"ch.qos.logback" % "logback-classic" % "1.2.3" % "runtime"

If you choose to use a different logger function, you may need to change these dependencies.

Please Note

Currently, the synonyms info, debug, trace are only valid for simple types of X. For Iterable[X]. Option[X], Future[X], Map[K, V], use the operators !!, !?, and !?? respectively. Note also that if you want to use warn or error, you can only log simple types and that there are no operator-type synonyms.

When using the !! operator for Iterable[X], you need to take care that the context is Iterable, rather than a more specific type such as Seq or List. Thus,

"Hello" !! List(1, 2, 3)

or

val xs = "Hello" !! List(1, 2, 3)
xs shouldBe List(1, 2, 3)

See also unit tests $bang$bang 1 and $bang$bang 1a for more detail.

Footnotes

  • (1) At present, this mechanism is not truly referentially transparent. In the future, we may provide an actor mechanism to allow for pure functional logging which is RT.
  • (2) "assigned to x:" I don't mean to suggest assignment in the classic sense any more than I mean that "variables" are mutable. The construct "val x = expr" means that x and (the evaluated) expr mean the same in the remainder of the current scope.

Version

1.0.8 Issue #17: Fixed regression in handling of futures; improved the error method;

1.0.7 Issue #14: Implemented level-based logging;

1.0.6 Issue #12: Minor changes to iterableLoggable;

1.0.5 Issue #10: Some changes to implementation of Iterable, including not evaluating non-strict collections.

1.0.4 Issue #7: Provides a more functional way of setting an explicit logger or disabling logging.

1.0.3 General improvements: more consistent functionality, issues with underlying logger hopefully resolved.

1.0.2 Added support for Future, cleaned up non-Flog modules, changed artifact name to "flog."

1.0.1 This project was cloned from DecisionTree.