|
| 1 | +# Logging |
| 2 | + |
| 3 | +A batteries-included Structured Logging toolkit for writing to a single logging |
| 4 | +abstraction in CLI apps and production services in Haskell. |
| 5 | + |
| 6 | +## Simple Usage |
| 7 | + |
| 8 | +<!-- |
| 9 | +```haskell |
| 10 | +module Main (module Main) where |
| 11 | +
|
| 12 | +import Prelude |
| 13 | +
|
| 14 | +import Data.Aeson |
| 15 | +import Data.Text (Text) |
| 16 | +import Text.Markdown.Unlit () |
| 17 | +``` |
| 18 | +--> |
| 19 | +
|
| 20 | +```haskell |
| 21 | +import Logging.Simple |
| 22 | +``` |
| 23 | +
|
| 24 | +Throughout your application, you should write against the ubiquitous |
| 25 | +`MonadLogger` interface, but using [`monad-logger-aeson`][monad-logger-aeson]: |
| 26 | +
|
| 27 | +[monad-logger-aeson]: https://jship.github.io/posts/2022-05-17-announcing-monad-logger-aeson/ |
| 28 | +
|
| 29 | +```haskell |
| 30 | +action :: MonadLogger m => m () |
| 31 | +action = do |
| 32 | + logInfo "This is a message sans details" |
| 33 | +
|
| 34 | + logError |
| 35 | + $ "Something went wrong" |
| 36 | + :# [ "error" .= object |
| 37 | + [ "code" .= (100 :: Int) |
| 38 | + , "messages" .= (["x", "y"] :: [Text]) |
| 39 | + ] |
| 40 | + ] |
| 41 | +
|
| 42 | + logDebug "This won't be seen in default settings" |
| 43 | +``` |
| 44 | +
|
| 45 | +When you run your transformer stack, wrap it in `runLoggerLoggingT` providing |
| 46 | +any value with a `HasLogger` instance (such as your main `App`). The `Logger` |
| 47 | +type itself has such an instance, and we provide `runSimpleLoggingT` for the |
| 48 | +simplest case: it creates one configured via environment variables and then |
| 49 | +calls `runLoggerLoggingT` with it. |
| 50 | +
|
| 51 | +You can use `withThreadContext` (from `monad-logger-aeson`) to add details that |
| 52 | +will appear in all the logged messages within that scope. Placing one of these |
| 53 | +at the very top-level adds details to all logged messages. |
| 54 | +
|
| 55 | +```haskell |
| 56 | +runner :: LoggingT IO a -> IO a |
| 57 | +runner = runSimpleLoggingT . withThreadContext ["app" .= ("example" :: Text)] |
| 58 | +
|
| 59 | +main :: IO () |
| 60 | +main = runner action |
| 61 | +``` |
| 62 | +
|
| 63 | +The defaults are good for CLI applications, producing colorful output (if |
| 64 | +connected to a terminal device) suitable for a human: |
| 65 | +
|
| 66 | + |
| 67 | +
|
| 68 | +Under the hood, `Logging.Settings.Env` is using [`envparse`][envparse] to |
| 69 | +configure logging through environment variables. See that module for full |
| 70 | +details. One thing we can adjust is `LOG_LEVEL`: |
| 71 | +
|
| 72 | +[envparse]: https://hackage.haskell.org/package/envparse |
| 73 | +
|
| 74 | + |
| 75 | +
|
| 76 | +In production, you will probably want to set `LOG_FORMAT=json` and ship logs to |
| 77 | +some aggregator like Datadog or Mezmo (formerly LogDNA): |
| 78 | +
|
| 79 | + |
| 80 | +
|
| 81 | +## Advanced Usage |
| 82 | +
|
| 83 | +TODO |
| 84 | +
|
| 85 | +## Integration with RIO |
| 86 | +
|
| 87 | +TODO |
| 88 | +
|
| 89 | +## Integration with Amazonka |
| 90 | +
|
| 91 | +```hs |
| 92 | +data App = App |
| 93 | + { appLogger :: Logger |
| 94 | + , appAWS :: AWS.Env |
| 95 | + } |
| 96 | +
|
| 97 | +instance HasLogger App where |
| 98 | + -- ... |
| 99 | +
|
| 100 | +runApp :: ReaderT App (LoggingT IO) a -> IO a |
| 101 | +runApp f = do |
| 102 | + logger <- newLogger defaultLogSettings |
| 103 | + app <- App logger <$> runLoggerLoggingT logger awsDiscover |
| 104 | + runLoggerLoggingT app $ runReaderT f app |
| 105 | +
|
| 106 | +awsDiscover :: (MonadIO m, MonadLoggerIO m) => m AWS.Env |
| 107 | +awsDiscover = do |
| 108 | + monadLoggerLog <- askLoggerIO |
| 109 | +
|
| 110 | + env <- liftIO $ AWS.newEnv AWS.discover |
| 111 | + pure $ env |
| 112 | + { AWS.envLogger = \level msg -> do |
| 113 | + monadLoggerLog |
| 114 | + defaultLoc -- TODO: there may be a way to get a CallStack/Loc |
| 115 | + "Amazonka" |
| 116 | + (\case |
| 117 | + AWS.Info -> LevelInfo |
| 118 | + AWS.Error -> LevelError |
| 119 | + AWS.Debug -> LevelDebug |
| 120 | + AWS.Trace -> LevelDebug |
| 121 | + ) |
| 122 | + (toLogStr msg) |
| 123 | + } |
| 124 | +``` |
| 125 | +
|
| 126 | +## Integration with WAI |
| 127 | +
|
| 128 | +```hs |
| 129 | +import Network.Wai.Middleware.Logging |
| 130 | +
|
| 131 | +instance HasLogger App where |
| 132 | + -- ... |
| 133 | +
|
| 134 | +waiMiddleware :: App -> Middleware |
| 135 | +waiMiddleware app = requestLogger app . defaultMiddlewaresNoLogging |
| 136 | +``` |
| 137 | +
|
| 138 | +## Integration with Warp |
| 139 | +
|
| 140 | +```hs |
| 141 | +instance HasLogger App where |
| 142 | + -- ... |
| 143 | +
|
| 144 | +warpSettings :: App -> Settings |
| 145 | +warpSettings app = setOnException onEx $ defaultSettings |
| 146 | + where |
| 147 | + onEx _req ex = |
| 148 | + when (defaultShouldDisplayException ex) |
| 149 | + $ runLoggerLoggingT app |
| 150 | + $ logError |
| 151 | + $ "Warp exception" |
| 152 | + :# ["exception" .= displayException ex] |
| 153 | +``` |
| 154 | +
|
| 155 | +## Integration with Yesod |
| 156 | +
|
| 157 | +```hs |
| 158 | +import Logging.Logger (getLoggerLoggerSet) |
| 159 | +
|
| 160 | +instance HasLogger App where |
| 161 | + -- ... |
| 162 | +
|
| 163 | +instance Yesod App where |
| 164 | + -- ... |
| 165 | + makeLogger App {..} = do |
| 166 | + logger <- defaultMakeLogger |
| 167 | + pure $ logger { Y.loggerSet = getLoggerLoggerSet appLogger } |
| 168 | +
|
| 169 | + messageLoggerSource app _logger loc source level msg = |
| 170 | + runLoggerLoggingT app $ monadLoggerLog loc source level msg |
| 171 | +``` |
| 172 | +
|
| 173 | +--- |
| 174 | +
|
| 175 | +[LICENSE](./LICENSE) | [CHANGELOG](./CHANGELOG.md) |
0 commit comments