Blammo
Batteries-included Structured Logging library
https://github.com/freckle/blammo#readme
Version on this page: | 1.1.2.1@rev:1 |
LTS Haskell 22.33: | 1.1.3.0 |
Stackage Nightly 2024-09-07: | 2.1.1.0 |
Latest on Hackage: | 2.1.1.0 |
Blammo-1.1.2.1@sha256:d27e58bedabb513e4beb4ae36b1938c4b43e16878da34db841a3a1c3f6dc6e8a,4886
Module documentation for 1.1.2.1
- 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. By default, it uses {number-of-processors} 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 such cases, you can set LOG_CONCURRENCY=1
to use a single buffer.
Configuration
Setting | Setter | Environment variable and format |
---|---|---|
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
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.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.