snix/users/Profpatsch/whatcd-resolver/src/AppT.hs
Profpatsch 3e5b3b82a6 feat(users/Profpatsch/whatcd-resolver): generic otel struct attrs
A generic way to turn Haskell structs into json objects, and therefor
to otel attributes.

I don’t know how to make it so I can give anything where Rep
implements that thing a `toOtelJsonAttr` implementation, things
overlap too much if I do that …

Change-Id: Iededb697dff206ddc6481a2eead210d44faa70c9
Reviewed-on: https://cl.tvl.fyi/c/depot/+/13006
Reviewed-by: Profpatsch <mail@profpatsch.de>
Tested-by: BuildkiteCI
2025-01-16 19:44:32 +00:00

328 lines
12 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module AppT where
import Builder
import Control.Monad.Logger qualified as Logger
import Control.Monad.Logger.CallStack
import Control.Monad.Reader
import Data.Error.Tree
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.Pool (Pool)
import Data.String (IsString (fromString))
import Data.Text qualified as Text
import Database.PostgreSQL.Simple qualified as Postgres
import FieldParser (FieldParser)
import FieldParser qualified as Field
import GHC.Generics qualified as G
import GHC.Records (getField)
import GHC.Stack qualified
import GHC.TypeLits
import Json.Enc
import Json.Enc qualified as Enc
import Label
import MyPrelude
import OpenTelemetry.Trace qualified as Otel hiding (getTracer, inSpan, inSpan')
import OpenTelemetry.Trace.Core qualified as Otel hiding (inSpan, inSpan')
import OpenTelemetry.Trace.Monad qualified as Otel
import Postgres.MonadPostgres
import Pretty qualified
import System.IO qualified as IO
import UnliftIO
import Prelude hiding (span)
data Context = Context
{ pgConfig ::
T2
"logDatabaseQueries"
DebugLogDatabaseQueries
"prettyPrintDatabaseQueries"
PrettyPrintDatabaseQueries,
pgConnPool :: (Pool Postgres.Connection),
tracer :: Otel.Tracer,
transmissionSessionId :: IORef (Maybe ByteString),
redactedApiKey :: ByteString
}
newtype AppT m a = AppT {unAppT :: ReaderT Context m a}
deriving newtype (Functor, Applicative, Monad, MonadIO, MonadUnliftIO, MonadThrow)
data AppException
= AppExceptionTree ErrorTree
| AppExceptionPretty [Pretty.Err]
| AppExceptionEnc Enc
deriving anyclass (Exception)
instance IsString AppException where
fromString s = AppExceptionTree (fromString s)
instance Show AppException where
showsPrec _ (AppExceptionTree t) = ("AppException: " ++) . ((textToString $ prettyErrorTree t) ++)
showsPrec _ (AppExceptionPretty t) = ("AppException: " ++) . ((Pretty.prettyErrsNoColor t) ++)
showsPrec _ (AppExceptionEnc e) = ((textToString $ Enc.encToTextPretty e) ++)
instance (MonadIO m) => MonadLogger (AppT m) where
monadLoggerLog loc src lvl msg = liftIO $ Logger.defaultOutput IO.stderr loc src lvl (Logger.toLogStr msg)
instance (Monad m) => Otel.MonadTracer (AppT m) where
getTracer = AppT $ asks (.tracer)
class (MonadUnliftIO m, Otel.MonadTracer m) => MonadOtel m
instance (MonadUnliftIO m) => MonadOtel (AppT m)
instance (MonadOtel m) => MonadOtel (Transaction m)
inSpan :: (MonadOtel m) => Text -> m a -> m a
inSpan name = Otel.inSpan name Otel.defaultSpanArguments
inSpan' :: (MonadOtel m) => Text -> (Otel.Span -> m a) -> m a
inSpan' name = Otel.inSpan' name Otel.defaultSpanArguments
-- | Add the attribute to the span, prefixing it with the `_` namespace (to easier distinguish our applications tags from standard tags)
addAttribute :: (MonadIO m, Otel.ToAttribute a) => Otel.Span -> Text -> a -> m ()
addAttribute span key a = Otel.addAttribute span ("_." <> key) a
-- | Add the attributes to the span, prefixing each key with the `_` namespace (to easier distinguish our applications tags from standard tags)
addAttributes :: (MonadIO m) => Otel.Span -> HashMap Text Otel.Attribute -> m ()
addAttributes span attrs = Otel.addAttributes span $ attrs & HashMap.mapKeys ("_." <>)
addEventSimple :: (MonadIO m) => Otel.Span -> Text -> m ()
addEventSimple span name =
Otel.addEvent
span
Otel.NewEvent
{ Otel.newEventName = name,
Otel.newEventTimestamp = Nothing,
Otel.newEventAttributes = mempty
}
-- | Create an otel attribute from a json encoder
jsonAttribute :: Enc -> Otel.Attribute
jsonAttribute e = e & Enc.encToTextPretty & Otel.toAttribute
instance Otel.ToAttribute (a, TextBuilder a) where
toAttribute (a, b) = buildText b a & Otel.toAttribute
parseOrThrow :: (MonadThrow m, MonadIO m) => Otel.Span -> FieldParser from to -> from -> m to
parseOrThrow span fp f =
f & Field.runFieldParser fp & \case
Left err -> appThrow span (AppExceptionTree $ singleError err)
Right a -> pure a
orThrowAppErrorNewSpan :: (MonadThrow m, MonadOtel m) => Text -> Either AppException a -> m a
orThrowAppErrorNewSpan msg = \case
Left err -> appThrowNewSpan msg err
Right a -> pure a
appThrowNewSpan :: (MonadThrow m, MonadOtel m) => Text -> AppException -> m a
appThrowNewSpan spanName exc = inSpan' spanName $ \span -> do
let msg = case exc of
AppExceptionTree e -> prettyErrorTree e
AppExceptionPretty p -> Pretty.prettyErrsNoColor p & stringToText
AppExceptionEnc e -> Enc.encToTextPretty e
recordException
span
( T2
(label @"type_" "AppException")
(label @"message" msg)
)
throwM $ exc
appThrow :: (MonadThrow m, MonadIO m) => Otel.Span -> AppException -> m a
appThrow span exc = do
let msg = case exc of
AppExceptionTree e -> prettyErrorTree e
AppExceptionPretty p -> Pretty.prettyErrsNoColor p & stringToText
AppExceptionEnc e -> Enc.encToTextPretty e
recordException
span
( T2
(label @"type_" "AppException")
(label @"message" msg)
)
throwM $ exc
orAppThrow :: (MonadThrow m, MonadIO m) => Otel.Span -> Either AppException a -> m a
orAppThrow span = \case
Left err -> appThrow span err
Right a -> pure a
-- | If action returns a Left, throw an AppException
assertM :: (MonadThrow f, MonadIO f) => Otel.Span -> (t -> Either AppException a) -> t -> f a
assertM span f v = case f v of
Right a -> pure a
Left err -> appThrow span err
assertMNewSpan :: (MonadThrow f, MonadOtel f) => Text -> (t -> Either AppException a) -> t -> f a
assertMNewSpan spanName f v = case f v of
Right a -> pure a
Left err -> appThrowNewSpan spanName err
-- | A specialized variant of @addEvent@ that records attributes conforming to
-- the OpenTelemetry specification's
-- <https://github.com/open-telemetry/opentelemetry-specification/blob/49c2f56f3c0468ceb2b69518bcadadd96e0a5a8b/specification/trace/semantic_conventions/exceptions.md semantic conventions>
--
-- @since 0.0.1.0
recordException ::
( MonadIO m,
HasField "message" r Text,
HasField "type_" r Text
) =>
Otel.Span ->
r ->
m ()
recordException span dat = liftIO $ do
callStack <- GHC.Stack.whoCreated dat.message
newEventTimestamp <- Just <$> Otel.getTimestamp
Otel.addEvent span $
Otel.NewEvent
{ newEventName = "exception",
newEventAttributes =
HashMap.fromList
[ ("exception.type", Otel.toAttribute @Text dat.type_),
("exception.message", Otel.toAttribute @Text dat.message),
("exception.stacktrace", Otel.toAttribute @Text $ Text.unlines $ Prelude.map stringToText callStack)
],
..
}
-- * Postgres
instance (MonadThrow m, MonadUnliftIO m) => MonadPostgres (AppT m) where
execute = executeImpl dbConfig
executeMany = executeManyImpl dbConfig
executeManyReturningWith = executeManyReturningWithImpl dbConfig
queryWith = queryWithImpl dbConfig
queryWith_ = queryWithImpl_ (dbConfig <&> snd)
foldRowsWithAcc = foldRowsWithAccImpl dbConfig
runTransaction = runPGTransaction
dbConfig :: (Monad m) => AppT m (DebugLogDatabaseQueries, PrettyPrintDatabaseQueries)
dbConfig =
AppT $
asks
( \c ->
( c.pgConfig.logDatabaseQueries,
c.pgConfig.prettyPrintDatabaseQueries
)
)
runPGTransaction :: (MonadUnliftIO m) => Transaction (AppT m) a -> AppT m a
runPGTransaction (Transaction transaction) = do
pool <- AppT ask <&> (.pgConnPool)
withRunInIO $ \unliftIO ->
withPGTransaction pool $ \conn -> do
unliftIO $ runReaderT transaction conn
-- | Best effort to convert a value to a JSON string that can be put in an Otel attribute.
toOtelJsonAttr :: (ToOtelJsonAttr a) => a -> Otel.Attribute
toOtelJsonAttr = toOtelJsonAttrImpl >>> Enc.encToTextPretty >>> Otel.toAttribute
-- | Best effort to convert a value to a JSON string that can be put in an Otel attribute.
class ToOtelJsonAttr a where
toOtelJsonAttrImpl :: a -> Enc
instance ToOtelJsonAttr Enc where
toOtelJsonAttrImpl = id
-- | Bytes are leniently converted to Text, because they are often used as UTF-8 encoded strings.
instance ToOtelJsonAttr ByteString where
toOtelJsonAttrImpl = Enc.text . bytesToTextUtf8Lenient
instance ToOtelJsonAttr Text where
toOtelJsonAttrImpl = Enc.text
instance ToOtelJsonAttr Int where
toOtelJsonAttrImpl = Enc.int
instance ToOtelJsonAttr Natural where
toOtelJsonAttrImpl = Enc.natural
instance ToOtelJsonAttr Bool where
toOtelJsonAttrImpl = Enc.bool
instance (ToOtelJsonAttr a) => ToOtelJsonAttr (Maybe a) where
toOtelJsonAttrImpl = \case
Nothing -> Enc.null
Just a -> toOtelJsonAttrImpl a
instance (ToOtelJsonAttr a) => ToOtelJsonAttr [a] where
toOtelJsonAttrImpl = Enc.list toOtelJsonAttrImpl
instance (ToOtelJsonAttr t1, ToOtelJsonAttr t2, KnownSymbol l1, KnownSymbol l2) => ToOtelJsonAttr (T2 l1 t1 l2 t2) where
toOtelJsonAttrImpl (T2 a b) =
Enc.object
[ (symbolText @l1, a & getField @l1 & toOtelJsonAttrImpl),
(symbolText @l2, b & getField @l2 & toOtelJsonAttrImpl)
]
instance (ToOtelJsonAttr t1, ToOtelJsonAttr t2, ToOtelJsonAttr t3, KnownSymbol l1, KnownSymbol l2, KnownSymbol l3) => ToOtelJsonAttr (T3 l1 t1 l2 t2 l3 t3) where
toOtelJsonAttrImpl (T3 a b c) =
Enc.object
[ (symbolText @l1, a & getField @l1 & toOtelJsonAttrImpl),
(symbolText @l2, b & getField @l2 & toOtelJsonAttrImpl),
(symbolText @l3, c & getField @l3 & toOtelJsonAttrImpl)
]
instance (ToOtelJsonAttr t1, ToOtelJsonAttr t2) => ToOtelJsonAttr (t1, t2) where
toOtelJsonAttrImpl t = Enc.tuple2 toOtelJsonAttrImpl toOtelJsonAttrImpl t
instance (ToOtelJsonAttr t1, ToOtelJsonAttr t2, ToOtelJsonAttr t3) => ToOtelJsonAttr (t1, t2, t3) where
toOtelJsonAttrImpl t = Enc.tuple3 toOtelJsonAttrImpl toOtelJsonAttrImpl toOtelJsonAttrImpl t
-- | Pretty-print the given value to a string
toOtelAttrGenericStruct :: (Generic a, GenericStructSimple (G.Rep a)) => a -> Otel.Attribute
toOtelAttrGenericStruct a = toOtelJsonAttr @Enc $ encodeSimpleValue $ G.from a
class GenericStruct f where
encodeStructAsObject :: f a -> [(Text, Enc)]
-- :*: (product)
-- Object fields (get field name and put into a list of key-value pair)
instance
(KnownSymbol l, ToOtelJsonAttr val) =>
GenericStruct (G.M1 G.S (G.MetaSel (Just l) u s f) (G.K1 i val))
where
encodeStructAsObject (G.M1 (G.K1 x)) = [(symbolText @l, toOtelJsonAttrImpl x)]
-- Concatenate two fields in a struct
instance (GenericStruct f, GenericStruct g) => GenericStruct (f G.:*: g) where
encodeStructAsObject (f G.:*: g) = encodeStructAsObject f <> encodeStructAsObject g
class GenericStructSimple f where
encodeSimpleValue :: f a -> Enc
instance
(ToOtelJsonAttr val, KnownSymbol l) =>
GenericStructSimple (G.M1 G.S (G.MetaSel (Just l) u s f) (G.K1 i val))
where
encodeSimpleValue (G.M1 x) = Enc.object $ [(symbolText @l, encodeSimpleValue x)]
-- pass through other M1
instance (GenericStructSimple f) => GenericStructSimple (G.M1 G.D u f) where
encodeSimpleValue (G.M1 x) = encodeSimpleValue x
-- pass through other M1
instance (GenericStructSimple f) => GenericStructSimple (G.M1 G.C u f) where
encodeSimpleValue (G.M1 x) = encodeSimpleValue x
-- | Encode a generic representation as an object with :*:
instance (GenericStruct f, GenericStruct g) => GenericStructSimple (f G.:*: g) where
encodeSimpleValue (a G.:*: b) = Enc.object $ encodeStructAsObject a <> encodeStructAsObject b
-- Void
instance GenericStructSimple G.V1 where
encodeSimpleValue x = case x of {}
-- Empty type is the empty object
instance GenericStructSimple G.U1 where
encodeSimpleValue _ = emptyObject
-- K1
instance (ToOtelJsonAttr val) => GenericStructSimple (G.K1 i val) where
encodeSimpleValue (G.K1 x) = toOtelJsonAttrImpl x