-
Notifications
You must be signed in to change notification settings - Fork 42
getting started
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"
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.
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.
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.
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!")
}
All configuration of Scribe is managed in Scala itself to avoid messy configuration files.
All loggers can be configured to define a parent logger that subscribes to all logging events fired by that 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.
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.
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 toSome(Logger.rootId)
. During configuration this can be disconnected withlogger.orphan()
or set to have a different parent name withlogger.withParent(logger | name | id)
. -
modifiers: List[LogModifier]
: ALogModifier
has a priority (to determine the order in which they will be invoked) and take aLogRecord
and return anOption[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 returningNone
. There are many pre-definedLogModifier
helper classes, but it's trivial to define your own as needed. -
handlers: List[LogHandler]
: ALogHandler
receivesLogRecord
s after they have been processed sequentially bymodifiers
and if the result of themodifiers
is non empty, will be passed along to eachLogHandler
. The primary use-case ofLogHandler
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 aLogRecord
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 thisLogger
. This is a retained value to allowcopy
andreplace
to occur with the immutable case class ofLogger
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.