Blammo

Batteries-included Structured Logging library

https://github.com/freckle/blammo#readme

Version on this page:1.2.1.0
LTS Haskell 23.1:2.1.1.0
Stackage Nightly 2024-12-21:2.1.1.0
Latest on Hackage:2.1.1.0

See all snapshots Blammo appears in

MIT licensed
Maintained by Freckle Education
This version can be pinned in stack with:Blammo-1.2.1.0@sha256:d319b91109e14762f06c340bbf955efcfccd6cc853a182e4c384113e236ddcb5,4836

Blammo

Hackage Stackage Nightly Stackage LTS CI

Blammo is a Structured Logging library that’s

  • Easy to use: one import and go!
  • Easy to configure: environment variable parsing out of the box!
  • Easy to integrate: see below for Amazonka, Yesod, and more!
  • Produces beautiful, colorful output in development
  • Produces fast-fast JSON in production

All built on the well-known MonadLogger interface and using an efficient fast-logger implementation.

It’s better than bad, it’s good!

Simple Usage

import Blammo.Logging.Simple

Throughout your application, you should write against the ubiquitous MonadLogger interface:

action1 :: MonadLogger m => m ()
action1 = do
  logInfo "This is a message sans details"

And make use of monad-logger-aeson for structured details:

data MyError = MyError
  { code :: Int
  , messages :: [Text]
  }
  deriving stock Generic
  deriving anyclass ToJSON

action2 :: MonadLogger m => m ()
action2 = do
  logError $ "Something went wrong" :# ["error" .= MyError 100 ["x", "y"]]
  logDebug "This won't be seen in default settings"

When you run your transformer stack, wrap it in runLoggerLoggingT providing any value with a HasLogger instance (such as your main App). The Logger type itself has such an instance, and we provide runSimpleLoggingT for the simplest case: it creates one configured via environment variables and then calls runLoggerLoggingT with it.

You can use withThreadContext (from monad-logger-aeson) to add details that will appear in all the logged messages within that scope. Placing one of these at the very top-level adds details to all logged messages.

runner :: LoggingT IO a -> IO a
runner = runSimpleLoggingT . withThreadContext ["app" .= ("example" :: Text)]

main :: IO ()
main = runner $ do
  action1
  action2

The defaults are good for CLI applications, producing colorful output (if connected to a terminal device) suitable for a human:

Under the hood, Logging.Settings.Env is using envparse to configure logging through environment variables. See that module for full details. One thing we can adjust is LOG_LEVEL:

In production, you will probably want to set LOG_FORMAT=json and ship logs to some aggregator like Datadog or Mezmo (formerly LogDNA):

Multiline Format

With the terminal formatter, a log message that is more than 120 visible characters will break into multi-line format:

This breakpoint can be controlled with LOG_BREAKPOINT. Set an unreasonably large number to disable this feature.

Out of Order Messages

Blammo is built on fast-logger, which offers concurrent logging through multiple buffers. This concurrent logging is fast, but may deliver messages out of order. You want this on production: your aggregator should be inspecting the message’s time-stamp to re-order as necessary on the other side. However, this can be problematic in a CLI, where there is both little need for such high performance and a lower tolerance for the confusion of out of order messages.

For this reason, the default behavior is to not use concurrent logging, but setting the format to json will automatically enable it (with {number-of-cores} as the value). To handle this explicitly, set LOG_CONCURRENCY.

Configuration

Setting Setter Environment variable and format
Format setLogSettingsFormat LOG_FORMAT=tty|json
Level(s) setLogSettingsLevels LOG_LEVEL=<level>[,<source:level>,...]
Destination setLogSettingsDestination LOG_DESTINATION=stdout|stderr|@<path>
Color setLogSettingsColor LOG_COLOR=auto|always|never
Breakpoint setLogSettingsBreakpoint LOG_BREAKPOINT=<number>
Concurrency setLogSettingsConcurrency LOG_CONCURRENCY=<number>

Advanced Usage

Add our environment variable parser to your own,

data AppSettings = AppSettings
  { appDryRun :: Bool
  , appLogSettings :: LogSettings
  , -- ...
  }

loadAppSettings :: IO AppSettings
loadAppSettings = Env.parse id $ AppSettings
  <$> var switch "DRY_RUN" mempty
  <*> LogSettingsEnv.parser
  <*> -- ...

Load a Logger into your App type and define HasLogger,

data App = App
  { appSettings :: AppSettings
  , appLogger :: Logger
  , -- ...
  }

instance HasLogger App where
  loggerL = lens appLogger $ \x y -> x { appLogger = y }

loadApp :: IO App
loadApp = do
  appSettings <- loadAppSettings
  appLogger <- newLogger $ appLogSettings appSettings
  -- ...
  pure App {..}

Use runLoggerLoggingT,

runAppT :: App -> ReaderT App (LoggingT IO) a -> IO a
runAppT app f = runLoggerLoggingT app $ runReaderT f app

Use without LoggingT

If your app monad is not a transformer stack containing LoggingT (ex: the ReaderT pattern), you can derive MonadLogger via WithLogger:

data AppEnv = AppEnv
  { appLogger :: Logger
  -- ...
  }

instance HasLogger AppEnv where
  loggerL = lens appLogger $ \x y -> x {appLogger = y}

newtype App a = App
  { unApp :: ReaderT AppEnv IO a }
  deriving newtype
    ( Functor
    , Applicative
    , Monad
    , MonadIO
    , MonadReader AppEnv
    )
  deriving (MonadLogger, MonadLoggerIO)
    via (WithLogger AppEnv IO)

runApp :: AppEnv -> App a -> IO a
runApp env action =
  runReaderT (unApp action) env

In your app you can use code written against the MonadLogger interface, like the actions defined earlier:

app :: App ()
app = do
  action1
  action2

Initialize the app with withLogger.

main2 :: IO ()
main2 =
  withLogger defaultLogSettings $ \logger -> do
    let appEnv =
          AppEnv
            { appLogger = logger
            -- ...
            }
    runApp appEnv app

Integration with RIO

data App = App
  { appLogFunc :: LogFunc
  , -- ...
  }

instance HasLogFuncApp where
  logFuncL = lens appLogFunc $ \x y -> x { logFunc = y }

runApp :: MonadIO m => RIO App a -> m a
runApp f = runSimpleLoggingT $ do
  loggerIO <- askLoggerIO

  let
    logFunc = mkLogFunc $ \cs source level msg -> loggerIO
      (callStackLoc cs)
      source
      (fromRIOLevel level)
      (getUtf8Builder msg)

  app <- App logFunc
    <$> -- ...
    <*> -- ...

  runRIO app $ f

callStackLoc :: CallStack -> Loc
callStackLoc = undefined

fromRIOLevel :: RIO.LogLevel -> LogLevel
fromRIOLevel = undefined

Integration with Amazonka

data App = App
  { appLogger :: Logger
  , appAWS :: AWS.Env
  }

instance HasLogger App where
  -- ...

runApp :: MonadUnliftIO m => ReaderT App m a -> m a
runApp f =
  withLogger defaultLogSettings $ \logger -> do
    aws <- runWithLogger logger awsDiscover
    runReaderT f $ App logger aws

awsDiscover :: (MonadIO m, MonadLoggerIO m) => m AWS.Env
awsDiscover = do
  loggerIO <- askLoggerIO
  env <- liftIO $ AWS.newEnv AWS.discover
  pure $ env
    { AWS.envLogger = \level msg -> do
      loggerIO
        defaultLoc -- TODO: there may be a way to get a CallStack/Loc
        "Amazonka"
        (case level of
          AWS.Info -> LevelInfo
          AWS.Error -> LevelError
          AWS.Debug -> LevelDebug
          AWS.Trace -> LevelOther "trace"
        )
        (toLogStr msg)
    }

Integration with WAI

import Network.Wai.Middleware.Logging

instance HasLogger App where
  -- ...

waiMiddleware :: App -> Middleware
waiMiddleware app =
  addThreadContext ["app" .= ("my-app" :: Text)]
    $ requestLogger app
    $ defaultMiddlewaresNoLogging

Integration with Warp

import qualified Network.Wai.Handler.Warp as Warp

instance HasLogger App where
  -- ...

warpSettings :: App -> Settings
warpSettings app = setOnException onEx $ defaultSettings
 where
  onEx _req ex =
    when (Warp.defaultShouldDisplayException ex)
      $ runWithLogger app
      $ logError
      $ "Warp exception" :# ["exception" .= displayException ex]

Integration with Yesod

instance HasLogger App where
  -- ...

instance Yesod App where
  -- ...

  messageLoggerSource app _logger loc source level msg =
    runWithLogger app $ monadLoggerLog loc source level msg

LICENSE | CHANGELOG

Changes

Unreleased

v1.2.1.0

  • Add Blammo.Logging.Simple.withLoggerEnv

v1.2.0.0

  • New in Blammo.Logging: withLogger, WithLogger(..), runWithLogger
  • New in Blammo.Logging.Logger: runLogAction
  • WAI middleware no longer performs a log flush. Wrap your entire application in either withLoggerLoggingT or withLogger to ensure a log flush at application shutdown.

v1.1.3.0

  • Update fast-logger to fix log flushing bug, and remove 0.1s delay that was introduced as a workaround.

v1.1.2.3

  • Add small delay (0.1s) in flushLogger to work around fast-logger bug

v1.1.2.2

  • Don’t automatically colorize if TERM=dumb is found in ENV

  • Respect NO_COLOR

  • Automatically adjust log concurrency based on LOG_FORMAT:

    Disable concurrency for tty (making that the new default) and enable it for json. Setting LOG_CONCURRENCY will still be respected.

v1.1.2.1

  • Add various getColors* helper functions

v1.1.2.0

  • Add Blammo.Logging.LogSettings.LogLevels

v1.1.1.2

  • Fix bug in LOG_CONCURRENCY parser

v1.1.1.1

  • Add getLogSettingsConcurrency
  • Add getLoggerShouldColor
  • Add pushLoggerStr & pushLoggerStrLn
  • Add getLoggerLogSettings

v1.1.1.0

  • Terminal formatter: align attributes vertically if the message goes over a certain number of characters (default 120).
  • Adds {get,set}LogSettingsBreakpoint and LOG_BREAKPOINT parsing

v1.1.0.0

  • Add flushLogger
  • Ensure log is flushed even on exceptions.

v1.0.3.0

  • Add Env.{parse,parser}With functions for parsing ‘LogSettings’ from environment variables with custom defaults.

v1.0.2.3

  • Fix for localhost clientIp value in requestLogger (#18)

v1.0.2.2

  • Support down to LTS 12.26 / GHC 8.4

v1.0.2.1

  • Add configurability to requestLogger, set LogSource by default
  • Add ability to capture and retrieve logged messages, for testing

v1.0.1.1

  • Add addThreadContextFromRequest, a wai Middleware for adding context using information from the Request.

v1.0.0.1

  • Relax lower bounds, support GHC 8.8

v1.0.0.0

First tagged release.