Blammo
Batteries-included Structured Logging library
https://github.com/freckle/blammo#readme
Version on this page: | 1.1.3.0 |
LTS Haskell 23.1: | 2.1.1.0 |
Stackage Nightly 2024-12-22: | 2.1.1.0 |
Latest on Hackage: | 2.1.1.0 |
Blammo-1.1.3.0@sha256:c9292aeb708c9d2fa9697643b5c2b272dbd46112cb17460c3f2cc7772bfff2d5,4709
Module documentation for 1.1.3.0
- Blammo
- Data
- Data.Aeson
- Network
- Network.Wai
- Network.Wai.Middleware
- Network.Wai
- 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|@<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 implement a custom instance of MonadLogger
:
data AppEnv = AppEnv
{ appLogFunc :: Loc -> LogSource -> LogLevel -> LogStr -> IO ()
-- ...
}
newtype App a = App
{ unApp :: ReaderT AppEnv IO a }
deriving newtype
( Functor
, Applicative
, Monad
, MonadIO
, MonadReader AppEnv
)
instance MonadLogger App where
monadLoggerLog loc logSource logLevel msg = do
logFunc <- asks appLogFunc
liftIO $ logFunc loc logSource logLevel (toLogStr msg)
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
To retrieve the log function from Blammo, use askLoggerIO
(from
MonadLoggerIO
) with runSimpleLoggingT
(or runLoggerLoggingT
if you need
more customization options), when you initialize the app:
main2 :: IO ()
main2 = do
logFunc <- runSimpleLoggingT askLoggerIO
let appEnv =
AppEnv
{ appLogFunc = logFunc
-- ...
}
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 :: ReaderT App (LoggingT IO) a -> IO a
runApp f = do
logger <- newLogger defaultLogSettings
app <- App logger <$> runLoggerLoggingT logger awsDiscover
runLoggerLoggingT app $ runReaderT f app
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
instance HasLogger App where
-- ...
warpSettings :: App -> Settings
warpSettings app = setOnException onEx $ defaultSettings
where
onEx _req ex =
when (defaultShouldDisplayException ex)
$ runLoggerLoggingT 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 =
runLoggerLoggingT app $ monadLoggerLog loc source level msg
Changes
Unreleased
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 forjson
. SettingLOG_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
andLOG_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 inrequestLogger
(#18)
v1.0.2.2
- Support down to LTS 12.26 / GHC 8.4
v1.0.2.1
- Add configurability to
requestLogger
, setLogSource
by default - Add ability to capture and retrieve logged messages, for testing
v1.0.1.1
- Add
addThreadContextFromRequest
, a waiMiddleware
for adding context using information from theRequest
.
v1.0.0.1
- Relax lower bounds, support GHC 8.8
v1.0.0.0
First tagged release.