In modern Scala applications, logging plays a crucial role in tracking events, monitoring performance, and debugging issues. However, traditional logging libraries can introduce side effects and complexity that don’t align with functional programming principles. Odin addresses this challenge by offering a purely functional approach to logging, with a focus on performance, composability, and structured logs. In this detailed guide, we’ll walk through Odin’s key features, explain its concepts, and show how to use it with Cats-Effect.
Why Use Odin?
Odin provides a functional interface to logging with several key benefits:
- Pure Functional API: Logging is wrapped in effect types, ensuring side-effect-free code.
- Structured and Contextual Logging: Logs are emitted as structured messages with key-value pairs.
- Performance-Oriented: Asynchronous logging and buffered output ensure minimal performance impact.
- Composable: Supports advanced features like loggers composition, contextual effects, and conditional logging.
- Extensible: Includes multiple logging implementations like console, file, and rolling file loggers.
Getting Started with Odin
Logger Interface
trait Logger[F[_]] {
def trace(msg: String): F[Unit]
def debug(msg: String): F[Unit]
def info(msg: String): F[Unit]
def warn(msg: String): F[Unit]
def error(msg: String, t: Throwable = null): F[Unit]
}
ScalaThe core abstraction in Odin is the Logger[F]
interface, where F
is an effect type, such as IO
from Cats-Effect. This interface provides methods to log messages at different levels:
Console Logger
The simplest way to start using Odin is with a console logger.
import cats.effect.{IO, IOApp}
import io.odin._
val logger: Logger[IO] = consoleLogger[IO]()
object SimpleApp extends IOApp.Simple {
def run: IO[Unit] = logger.info("Application started")
}
ScalaFile and Rolling File Logger
You can log messages to a file or use a rolling file logger to rotate logs.
import java.nio.file.Paths
import io.odin._
val fileLogger: Logger[IO] = fileLogger[IO](Paths.get("logs/app.log"))
val rollingLogger: Logger[IO] = rollingFileLogger[IO](
path = Paths.get("logs/rolling.log"),
maxSizeInBytes = 1024 * 1024, // 1MB
maxFiles = 5
)
ScalaFormatting Logs
Odin allows you to format logs with built-in and custom formatters.
1. Default Formatter
val logger: Logger[IO] = consoleLogger[IO](
formatter = Formatter.default
)
Scala2. JSON Formatter
val jsonLogger: Logger[IO] = consoleLogger[IO](
formatter = Formatter.json
)
Scala3. Customized Formatter
val customLogger: Logger[IO] = consoleLogger[IO](
formatter = Formatter.default.withoutColors.copy(timestampFormat = Some("HH:mm:ss"))
)
ScalaMinimal Log Level
You can control which logs are emitted by setting the minimal log level.
val logger: Logger[IO] = consoleLogger[IO](minLevel = Level.Warn)
ScalaComposing Loggers
You can combine multiple loggers, such as console and file loggers, into a single composite logger.
val combinedLogger: Logger[IO] = logger[IO](consoleLogger[IO](), fileLogger[IO](Paths.get("logs/app.log")))
ScalaAsync Logger
Asynchronous logging ensures that logging doesn’t block your application’s main flow.
val asyncLogger: Logger[IO] = asyncLogger[IO](consoleLogger[IO]())
ScalaClass and Enclosure Routing
You can route logs based on the enclosing class or function.
def getLoggerForClass[T](cls: Class[T]): Logger[IO] =
consoleLogger[IO]().withMinimalLevel(Level.Info)
ScalaConstant Context and Contextual Effects
Odin supports adding constant context to logs, such as service names or instance IDs.
val loggerWithContext: Logger[IO] = logger.withConstantContext(Map("service" -> "user-service"))
loggerWithContext.info("Starting the service")
ScalaYou can also add dynamic context using contextual effects.
def logUserAction(userId: String): IO[Unit] =
logger.contextual(Map("userId" -> userId)).info("User action processed")
ScalaSecret Context
Odin allows you to mask sensitive data using secret contexts.
val loggerWithSecret: Logger[IO] = logger.withSecretContext(Map("apiKey" -> "12345"))
loggerWithSecret.info("API call executed")
ScalaContramap and Filter
You can transform logs with contramap
or filter logs based on conditions.
val filteredLogger: Logger[IO] = logger.filter(msg => !msg.message.contains("DEBUG"))
ScalaLogging Exceptions with ToThrowable
Odin provides the toThrowable
method for logging exceptions as structured messages.
val loggerWithThrowable: Logger[IO] = logger.toThrowable
loggerWithThrowable.error("Exception occurred", new RuntimeException("Boom!"))
ScalaTesting Logger
Odin also provides a TestingLogger
to validate logs in unit tests.
import io.odin.loggers.TestingLogger
val testingLogger = TestingLogger[IO]()
for {
_ <- testingLogger.info("Test message")
logs <- testingLogger.get
} yield assert(logs.exists(_.message == "Test message"))
ScalaConditional Logging
Odin allows you to log conditionally, based on runtime conditions.
def logIf(condition: Boolean): IO[Unit] =
logger.ifEnabled(Level.Debug)(logger.debug("Conditional log"))
ScalaExtras and Derivation
You can enrich logs with additional context using extras.
val loggerWithExtras: Logger[IO] = logger.withExtras(Map("env" -> "production"))
ScalaBenchmarks
Odin is optimized for performance with minimal impact on your application. Asynchronous logging and buffered outputs ensure that logs are emitted efficiently. Benchmarks available in the repository demonstrate Odin’s performance compared to other logging libraries.
Conclusion
Odin provides a functional and performant approach to logging for Scala applications, perfectly aligning with Cats-Effect. With structured logging, context support, and minimal configuration, Odin empowers developers to build robust, observable systems. Whether you need console logs for local debugging or rolling file logs for production, Odin offers a flexible and composable solution.
If you’re building functional applications in Scala, Odin is a perfect fit for your logging needs. Try it out and experience the power of functional logging!