Blammo-1.1.2.2: README.lhs
# Blammo
[](https://hackage.haskell.org/package/Blammo)
[](http://stackage.org/nightly/package/Blammo)
[](http://stackage.org/lts/package/Blammo)
[](https://github.com/freckle/blammo/actions/workflows/ci.yml)

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
<!--
```haskell
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Main (module Main) where
import Prelude
import Data.Aeson
import Data.Text (Text)
import GHC.Generics (Generic)
import Text.Markdown.Unlit ()
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Logger (Loc, LogStr, ToLogStr (toLogStr))
import Control.Monad.Reader (asks, MonadReader, ReaderT (runReaderT))
```
-->
```haskell
import Blammo.Logging.Simple
```
Throughout your application, you should write against the ubiquitous
`MonadLogger` interface:
```haskell
action1 :: MonadLogger m => m ()
action1 = do
logInfo "This is a message sans details"
```
And make use of [`monad-logger-aeson`][monad-logger-aeson] for structured
details:
[monad-logger-aeson]: https://jship.github.io/posts/2022-05-17-announcing-monad-logger-aeson/
```haskell
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.
```haskell
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`][envparse] to
configure logging through environment variables. See that module for full
details. One thing we can adjust is `LOG_LEVEL`:
[envparse]: https://hackage.haskell.org/package/envparse

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`.
[fast-logger]: https://hackage.haskell.org/package/fast-logger
## 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,
```hs
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`,
```hs
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`,
```hs
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](https://www.fpcomplete.com/blog/readert-design-pattern/)), you
can implement a custom instance of `MonadLogger`:
```haskell
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:
```haskell
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:
```haskell
main2 :: IO ()
main2 = do
logFunc <- runSimpleLoggingT askLoggerIO
let appEnv =
AppEnv
{ appLogFunc = logFunc
-- ...
}
runApp appEnv app
```
## Integration with RIO
```hs
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
```hs
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
```hs
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
```hs
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
```hs
instance HasLogger App where
-- ...
instance Yesod App where
-- ...
messageLoggerSource app _logger loc source level msg =
runLoggerLoggingT app $ monadLoggerLog loc source level msg
```
---
[LICENSE](./LICENSE) | [CHANGELOG](./CHANGELOG.md)