Blammo
Batteries-included Structured Logging library
https://github.com/freckle/blammo#readme
LTS Haskell 23.1: | 2.1.1.0 |
Stackage Nightly 2024-12-27: | 2.1.1.0 |
Latest on Hackage: | 2.1.1.0 |
Blammo-2.1.1.0@sha256:29fa7c1411fc3dc87504e3c2d1e903a0077208a51228d779eaa02abf025219c0,4755
Module documentation for 2.1.1.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.1.0
- Accept special value
null
forLOG_DESTINATION
as a synonym for the null device (/dev/null
or\\.\NUL
on windows).
v2.1.0.0
Removes less frequently used definitions from the main Blammo.Logging
module
into other modules.
- Moved from
Blammo.Logging
to 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.Logging
to new moduleBlammo.Logging.Setup
:HasLogger (..)
,withLogger
,newLogger
,runLoggerLoggingT
,LoggingT
,WithLogger (..)
,runWithLogger
Blammo.Logging.Simple
has been expanded to include reëxports of:
Blammo.Logging.LogSettings
Blammo.Logging.Setup
Blammo.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
withLoggerLoggingT
orwithLogger
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 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.