Skip to content

getting started

Matt Hicks edited this page Dec 30, 2021 · 14 revisions

SBT Configuration

Latest version

Scribe is published to Sonatype OSS and Maven Central and supports JVM and Scala.js with 2.11, 2.12, 2.13, and Dotty and Scala Native with 2.11:

libraryDependencies += "com.outr" %% "scribe" % "3.6.6"   // Scala
libraryDependencies += "com.outr" %%% "scribe" % "3.6.6"  // Scala.js / Scala Native / Cross-project

Add the following if you want file logging support (JVM and Native):

libraryDependencies += "com.outr" %% "scribe-file" % "3.6.6"

Using Scribe

Scribe supports a zero import and zero mix-in logging feature to make it far faster and easier to use logging in your application:

class MyClass {
  scribe.info("Hello, World!")
  doSomething()
  
  def doSomething(): Unit = {
    scribe.info("I did something!")
  }
}

The output will look something like the following:

2017.01.02 19:05:47:342 [main] INFO MyClass:2 - Hello, World!
2017.01.02 19:05:47.342 [main] INFO MyClass.doSomething:6 - I did something!

It is worth noting that Scribe, by default, logs class name, method name, and line numbers. This is a compile-time feature and does not add any extra performance impact to your application.

Logger by class

You can utilize the implicit class to log on a specific instance without touching the code of that class:

import scribe._

class MyClass {
  val myString = "Nothing Special About Me"
  myString.logger.info("Logging on a String!")
}

Though at first glance this might not seem useful, you can configure logging on an instance and it will utilize the class name to retain configuration:

import scribe._
import scribe.file._

class MyClass {
  val myString = "Nothing Special About Me"
  myString.logger.withHandler(writer = FileWriter("logs" / ("app-" % year % "-" % month % "-" % day % ".log")).replace()
  myString.logger.info("Logging on a String!")
  
  "Another String".logger.info("Written to a file...")
}

The second logging call will share the same Logger instance as it is derived from the same class name. This makes it very easy to configure and log explicitly to types without a lot of extra boilerplate or hassle.

Classic Mix-In Logging

Scribe also supports a more classic style of logging via mix-in of the Logging trait:

import scribe.Logging

class MyClass extends Logging {
  logger.info("Hello, World!")
}

The default logging configuration will output to the console and includes Info and above.

Logger by name

If you come from Java then this may all seem a little bit foreign to you. You can always go old-school and simply get a logger by name and use it:

import scribe.Logger

class MyClass {
  val logger = Logger("MyClass")
  logger.info("I'm old-school!")
}

Configuring Scribe

All configuration of Scribe is managed in Scala itself to avoid messy configuration files.

Parents

All loggers can be configured to define a parent logger that subscribes to all logging events fired by that logger.

The Root Logger

By default, all loggers refer to their parent via dot-separation ("com.example.SomeClass" will have a parent of "com.example") to the top-level, and the top-level logger will have a parent as Logger.root. It is the root logger that has the default console writer.

Logging to a File

The following will add a new LogHandler to the specified logger to append to a daily file.

Logger(loggerName).withHandler(writer = FileWriter("logs" / ("app-" % year % "-" % month % "-" % day % ".log"))).replace()

All future references to loggerName will include the new handler.

Configuring the Logger

A Logger is actually just a case class with some additional functionality added on. The Logger contains parentId, modifiers, handlers, overrideClassName and id.

  • parentId: Option[Long]: The parent logger that will receive all logging events this logger receives. Defaults to Some(Logger.rootId). During configuration this can be disconnected with logger.orphan() or set to have a different parent name with logger.withParent(logger | name | id).

  • modifiers: List[LogModifier]: A LogModifier has a priority (to determine the order in which they will be invoked) and take a LogRecord and return an Option[LogRecord]. This gives the modifier the ability to change the record (for example, to boost the value or modify the message), or even to filter out records altogether by returning None. There are many pre-defined LogModifier helper classes, but it's trivial to define your own as needed.

  • handlers: List[LogHandler]: A LogHandler receives LogRecords after they have been processed sequentially by modifiers and if the result of the modifiers is non empty, will be passed along to each LogHandler. The primary use-case of LogHandler is to output the records (to the console, a file, etc.), but it is a flexible and simple interface that can be extended to fulfill any need.

  • overrideClassName: Option[String]: The className is derived automatically when a LogRecord is created, but if this value is set, the name can be explicitly defined for records created by this logger.

  • id: Long: The unique identifier to this Logger. This is a retained value to allow copy and replace to occur with the immutable case class of Logger to "modify configuration" at runtime.

If you want to simply update my logger removing the Logger.root parent reference you can do so like this:

import scribe.Logging

class MyClass extends Logging {
  logger.orphan().replace()
}

This will update the logger being used for this class going forward.

To change the default global log level, use:

scribe.Logger.root
  .clearHandlers()
  .clearModifiers()
  .withHandler(minimumLevel = Some(Level.Error))
  .replace()

You can configure the output (how the log will look like) when adding a LogHandler. The Formatter companion currently has two pre-defined scenarios (simple and default). Building your own Formatter instance is easy and efficient with the formatter interpolator:

import scribe.format._

val myFormatter: Formatter = formatter"[$threadName] $positionAbbreviated - $message$newLine"
Logger.root
  .clearHandlers()
  .withHandler(formatter = myFormatter)
  .replace()

This builds an efficient formatter at compile-time with the blocks you specify. This is both clean and readable. Finally, this fully extensible as additional custom blocks can be defined by simply implementing the FormatBlock interface.