Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/jship/monad-logger-aeson

JSON logging using monad-logger interface
https://github.com/jship/monad-logger-aeson

aeson json logging structured-logging

Last synced: 24 days ago
JSON representation

JSON logging using monad-logger interface

Awesome Lists containing this project

README

        

# [monad-logger-aeson][]

[![Build badge][]][build]
[![Version badge][]][version]

## Synopsis

`monad-logger-aeson` provides structured JSON logging using `monad-logger`'s
interface. Specifically, it is intended to be a (largely) drop-in replacement
for `monad-logger`'s `Control.Monad.Logger.CallStack` module.

For additional detail on the library, please see the [Haddocks][], the
[announcement blog post][], and the remainder of this README.

## Crash course

Assuming we have the following `monad-logger`-based code:

```haskell
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE OverloadedStrings #-}
module Main
( main
) where

import Control.Monad.Logger.CallStack
import Data.Text (pack)

doStuff :: (MonadLogger m) => Int -> m ()
doStuff x = do
logDebug $ "Doing stuff: x=" <> pack (show x)

main :: IO ()
main = do
runStdoutLoggingT do
doStuff 42
logInfo "Done"
```

We would get something like this log output:

```text
[Debug] Doing stuff: x=42 @(main:Main app/readme-example.hs:12:3)
[Info] Done @(main:Main app/readme-example.hs:18:5)
```

We can change our import from this:

```haskell
import Control.Monad.Logger.CallStack
```

To this:

```haskell
import Control.Monad.Logger.Aeson
```

In changing the import, we'll have one compiler error to address:

```text
monad-logger-aeson/app/readme-example.hs:12:35: error:
• Couldn't match expected type ‘Message’
with actual type ‘Data.Text.Internal.Text’
• In the second argument of ‘(<>)’, namely ‘pack (show x)’
In the second argument of ‘($)’, namely
‘"Doing stuff: x=" <> pack (show x)’
In a stmt of a 'do' block:
logDebug $ "Doing stuff: x=" <> pack (show x)
|
12 | logDebug $ "Doing stuff: x=" <> pack (show x)
|
```

This indicates that we need to provide the `logDebug` call a `Message` rather
than a `Text` value. This compiler error gives us a choice depending upon our
current time constraints: we can either go ahead and convert this `Text` value
to a "proper" `Message` by moving the metadata it encodes into structured data
(i.e. a `[Series]` value, where `Series` is an `aeson` key and encoded value),
or we can defer doing that for now by tacking on an empty `[Series]` value.
We'll opt for the former here:

```haskell
logDebug $ "Doing stuff" :# ["x" .= x]
```

Note that the `logInfo` call did not give us a compiler error, as `Message` has
an `IsString` instance.

Our log output now looks like this (formatted for readability here with `jq`):

```jsonl
{
"time": "2022-05-15T20:52:15.5559417Z",
"level": "debug",
"location": {
"package": "main",
"module": "Main",
"file": "app/readme-example.hs",
"line": 11,
"char": 3
},
"message": {
"text": "Doing stuff",
"meta": {
"x": 42
}
}
}
{
"time": "2022-05-15T20:52:15.5560448Z",
"level": "info",
"location": {
"package": "main",
"module": "Main",
"file": "app/readme-example.hs",
"line": 17,
"char": 5
},
"message": {
"text": "Done"
}
}
```

Voilà! Now our Haskell code is using structured logging. Our logs are fit for
parsing, ingestion into our log aggregation/analysis service of choice, etc.

## Goals

The following goals have underpinned the development of `monad-logger-aeson`:

1. Structured logging _must_ be easy to add to existing Haskell codebases
1. Structured logging _should_ be performant

We believe we have achieved goal 1 by targeting `monad-logger`'s
`MonadLogger`/`LoggingT` interface. There are many interesting logging libraries
to choose from in Haskell: `monad-logger`, `di`, `logging-effect`, `katip`, and
so on. Both by comparing the [reverse dependency list][] for `monad-logger` with
the other logging libraries' reverse dependency lists, and also consulting our
personal experiences working on Haskell codebases, `monad-logger` would seem to
be the most prevalent logging library in the wild. In developing our library as
a (largely) drop-in replacement for `monad-logger`, we hope to empower
Haskellers using this popular logging interface to add structured logging to
their programs with minimal fuss.

We believe we have achieved goal 2 by directly representing in-flight `Message`
values using a fixed `aeson` object `Encoding`, by never (internally) converting
anything to intermediate `Value`s, and by never parsing these in-flight log
messages when assembling the final logged message. Regarding the latter point,
we need to know the origin of an input `LogStr` (i.e. is it from
`monad-logger-aeson` or not?). If we know an input `LogStr` came from
`monad-logger-aeson`, then we know the `LogStr` is an `aeson` object `Encoding`
of a `Message`, and so we can pass this encoding along untouched as a piece of
the final log message's encoding. If we know an input `LogStr` did not come from
`monad-logger-aeson`, then we can scoop this `LogStr` up into a text-only
`Message`, encode that, and pass the encoding along as a piece of the final log
message's encoding. A straightforward and relatively expensive implementation of
determining a `LogStr`'s origin would involve parsing of in-flight log messages
back into `Message` values. Rather than resort to parsing _every_ in-flight
message, we simply check the first 9 characters of the `LogStr` for a match with
`{"text":"`. Yes, there is the possibility that a `monad-logger` user logs out a
`LogStr` with this same prefix and that `LogStr` makes its way into a
`monad-logger-aeson` user's `LoggingT` runner function. This would cause
`monad-logger-aeson` to erroneously assume the message's origin is
`monad-logger-aeson`. We feel this possibility is overall unlikely, and have
accepted this as a tradeoff in the design space of the library. While we believe
the principles described previously should provide good performance, please note
that benchmarks do not yet exist for this library. Caveat emptor!

[monad-logger-aeson]: https://github.com/jship/monad-logger-aeson
[Build badge]: https://github.com/jship/monad-logger-aeson/workflows/CI/badge.svg
[build]: https://github.com/jship/monad-logger-aeson/actions
[Version badge]: https://img.shields.io/hackage/v/monad-logger-aeson?color=brightgreen&label=version&logo=haskell
[version]: https://hackage.haskell.org/package/monad-logger-aeson
[Haddocks]: https://hackage.haskell.org/package/monad-logger-aeson
[announcement blog post]: https://jship.github.io/posts/2022-05-17-announcing-monad-logger-aeson/
[reverse dependency list]: https://packdeps.haskellers.com/reverse/monad-logger