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):
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)
}
LICENSE | CHANGELOG