Blammo
Batteries-included Structured Logging library
https://github.com/freckle/blammo#readme
| LTS Haskell 24.16: | 2.1.3.0 |
| Stackage Nightly 2025-10-24: | 2.1.3.0 |
| Latest on Hackage: | 2.1.3.0 |
Blammo-2.1.3.0@sha256:1ebab378041b1786fffbc06ebdbd31483175d4531a27859f4ef6a63dd14b5cd2,4794Module documentation for 2.1.3.0
- Blammo
- Data
- Data.Aeson
- System
- System.Log
- System.Log.FastLogger
- System.Log
Blammo

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|null|@<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)
}
Changes
Unreleased
v2.1.3.0
- Add
setLogSettingsColorsto support customizing colors
v2.1.2.0
- Add
setLoggerReformat
v2.1.1.0
- Accept special value
nullforLOG_DESTINATIONas a synonym for the null device (/dev/nullor\\.\NULon windows).
v2.1.0.0
Removes less frequently used definitions from the main Blammo.Logging module
into other modules.
- Moved from
Blammo.Loggingto new moduleBlammo.Logging.ThreadContext:MonadMask,withThreadContext,myThreadContext,Pair. - Removed from
Blammo.Logging(still available inBlammo.Logging.LogSettings):LogSettings,LogDestination (..),LogFormat (..),defaultLogSettings,LogColor (..),setLogSettingsLevels,setLogSettingsDestination,setLogSettingsFormat,setLogSettingsColor,setLogSettingsBreakpoint,setLogSettingsConcurrency. - Moved from
Blammo.Loggingto new moduleBlammo.Logging.Setup:HasLogger (..),withLogger,newLogger,runLoggerLoggingT,LoggingT,WithLogger (..),runWithLogger
Blammo.Logging.Simple has been expanded to include reëxports of:
Blammo.Logging.LogSettingsBlammo.Logging.SetupBlammo.Logging.ThreadContext
v2.0.0.0
- Remove module
Network.Wai.Middleware.Logging. It is moved to a new package,Blammo-wai.
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
withLoggerLoggingTorwithLoggerto 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
flushLoggerto work around fast-logger bug
v1.1.2.2
-
Don’t automatically colorize if
TERM=dumbis 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 forjson. SettingLOG_CONCURRENCYwill 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_CONCURRENCYparser
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}LogSettingsBreakpointandLOG_BREAKPOINTparsing
v1.1.0.0
- Add
flushLogger - Ensure log is flushed even on exceptions.
v1.0.3.0
- Add
Env.{parse,parser}Withfunctions for parsing ‘LogSettings’ from environment variables with custom defaults.
v1.0.2.3
- Fix for localhost
clientIpvalue inrequestLogger(#18)
v1.0.2.2
- Support down to LTS 12.26 / GHC 8.4
v1.0.2.1
- Add configurability to
requestLogger, setLogSourceby default - Add ability to capture and retrieve logged messages, for testing
v1.0.1.1
- Add
addThreadContextFromRequest, a waiMiddlewarefor adding context using information from theRequest.
v1.0.0.1
- Relax lower bounds, support GHC 8.8
v1.0.0.0
First tagged release.