refactor(users/Profpatsch/whatcd-resolver): use builder for otel

This makes `addAttribute` a little nicer to handle, because we can now
just put a `(builder, value)` tuple (yay, orphan instances!)

Change-Id: I145a0d2b764d44755fb3c548a40188f13ee4ed83
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12956
Tested-by: BuildkiteCI
Reviewed-by: Profpatsch <mail@profpatsch.de>
This commit is contained in:
Profpatsch 2025-01-05 04:05:23 +01:00
parent 444b67b010
commit 77aadfa46c
7 changed files with 62 additions and 29 deletions

View file

@ -11,12 +11,21 @@ module Builder
bytesLazyB, bytesLazyB,
utf8B, utf8B,
utf8LazyB, utf8LazyB,
utf8LenientT,
utf8LenientLazyT,
intDecimalT, intDecimalT,
intDecimalNaturalT, intDecimalB,
integerDecimalT,
integerDecimalB,
naturalDecimalT,
naturalDecimalB,
scientificDecimalT,
scientificDecimalB,
) )
where where
import Data.ByteString.Builder qualified as Bytes import Data.ByteString.Builder qualified as Bytes
import Data.ByteString.Builder.Scientific qualified as Scientific.Bytes
import Data.ByteString.Lazy qualified as Bytes.Lazy import Data.ByteString.Lazy qualified as Bytes.Lazy
import Data.Functor.Contravariant import Data.Functor.Contravariant
import Data.Functor.Contravariant.Divisible import Data.Functor.Contravariant.Divisible
@ -24,6 +33,7 @@ import Data.String
import Data.Text.Lazy qualified as Text.Lazy import Data.Text.Lazy qualified as Text.Lazy
import Data.Text.Lazy.Builder qualified as Text import Data.Text.Lazy.Builder qualified as Text
import Data.Text.Lazy.Builder.Int qualified as Text import Data.Text.Lazy.Builder.Int qualified as Text
import Data.Text.Lazy.Builder.Scientific qualified as Scientific.Text
import MyPrelude import MyPrelude
newtype TextBuilder a = TextBuilder {unTextBuilder :: a -> Text.Builder} newtype TextBuilder a = TextBuilder {unTextBuilder :: a -> Text.Builder}
@ -81,6 +91,12 @@ bytesB = BytesBuilder Bytes.byteString
bytesLazyB :: BytesBuilder Bytes.Lazy.ByteString bytesLazyB :: BytesBuilder Bytes.Lazy.ByteString
bytesLazyB = BytesBuilder Bytes.lazyByteString bytesLazyB = BytesBuilder Bytes.lazyByteString
utf8LenientT :: TextBuilder ByteString
utf8LenientT = bytesToTextUtf8Lenient >$< textT
utf8LenientLazyT :: TextBuilder Bytes.Lazy.ByteString
utf8LenientLazyT = bytesToTextUtf8LenientLazy >$< textLazyT
utf8B :: BytesBuilder Text utf8B :: BytesBuilder Text
utf8B = textToBytesUtf8 >$< bytesB utf8B = textToBytesUtf8 >$< bytesB
@ -90,5 +106,23 @@ utf8LazyB = textToBytesUtf8Lazy >$< bytesLazyB
intDecimalT :: TextBuilder Int intDecimalT :: TextBuilder Int
intDecimalT = TextBuilder Text.decimal intDecimalT = TextBuilder Text.decimal
intDecimalNaturalT :: TextBuilder Natural intDecimalB :: BytesBuilder Int
intDecimalNaturalT = TextBuilder Text.decimal intDecimalB = BytesBuilder Bytes.intDec
integerDecimalT :: TextBuilder Integer
integerDecimalT = TextBuilder Text.decimal
integerDecimalB :: BytesBuilder Integer
integerDecimalB = BytesBuilder Bytes.integerDec
naturalDecimalT :: TextBuilder Natural
naturalDecimalT = TextBuilder Text.decimal
naturalDecimalB :: BytesBuilder Natural
naturalDecimalB = toInteger >$< integerDecimalB
scientificDecimalT :: TextBuilder Scientific
scientificDecimalT = TextBuilder Scientific.Text.scientificBuilder
scientificDecimalB :: BytesBuilder Scientific
scientificDecimalB = BytesBuilder Scientific.Bytes.scientificBuilder

View file

@ -862,17 +862,10 @@ traceQueryIfEnabled span logDatabaseQueries prettyQuery qry params = do
HasSingleParam p -> pgFormatQuery' prettyQuery qry p HasSingleParam p -> pgFormatQuery' prettyQuery qry p
HasMultiParams ps -> pgFormatQueryMany' prettyQuery qry ps HasMultiParams ps -> pgFormatQueryMany' prettyQuery qry ps
let doLog errs = let doLog errs = do
Otel.addAttributes Otel.addAttribute span "_.postgres.query" (errs.query & bytesToTextUtf8Lenient & Otel.toAttribute)
span for_ errs.explain $ \ex ->
$ HashMap.fromList Otel.addAttribute span "_.postgres.explain" (Otel.toAttribute @Text ex)
$ ( ("_.postgres.query", Otel.toAttribute @Text (errs.query & bytesToTextUtf8Lenient))
: ( errs.explain
& \case
Nothing -> []
Just ex -> [("_.postgres.explain", Otel.toAttribute @Text ex)]
)
)
let doExplain = do let doExplain = do
q <- formattedQuery q <- formattedQuery
Otel.inSpan "Postgres EXPLAIN Query" Otel.defaultSpanArguments $ do Otel.inSpan "Postgres EXPLAIN Query" Otel.defaultSpanArguments $ do

View file

@ -1,7 +1,9 @@
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module AppT where module AppT where
import Builder
import Control.Monad.Logger qualified as Logger import Control.Monad.Logger qualified as Logger
import Control.Monad.Logger.CallStack import Control.Monad.Logger.CallStack
import Control.Monad.Reader import Control.Monad.Reader
@ -98,6 +100,9 @@ addEventSimple span name =
jsonAttribute :: Enc -> Otel.Attribute jsonAttribute :: Enc -> Otel.Attribute
jsonAttribute e = e & Enc.encToTextPretty & Otel.toAttribute 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 :: (MonadThrow m, MonadIO m) => Otel.Span -> FieldParser from to -> from -> m to
parseOrThrow span fp f = parseOrThrow span fp f =
f & Field.runFieldParser fp & \case f & Field.runFieldParser fp & \case
@ -208,6 +213,7 @@ runPGTransaction (Transaction transaction) = do
withPGTransaction pool $ \conn -> do withPGTransaction pool $ \conn -> do
unliftIO $ runReaderT transaction conn 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 :: (ToOtelJsonAttr a) => a -> Otel.Attribute
toOtelJsonAttr = toOtelJsonAttrImpl >>> Enc.encToTextPretty >>> Otel.toAttribute toOtelJsonAttr = toOtelJsonAttrImpl >>> Enc.encToTextPretty >>> Otel.toAttribute
@ -215,6 +221,9 @@ toOtelJsonAttr = toOtelJsonAttrImpl >>> Enc.encToTextPretty >>> Otel.toAttribute
class ToOtelJsonAttr a where class ToOtelJsonAttr a where
toOtelJsonAttrImpl :: a -> Enc 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. -- | Bytes are leniently converted to Text, because they are often used as UTF-8 encoded strings.
instance ToOtelJsonAttr ByteString where instance ToOtelJsonAttr ByteString where
toOtelJsonAttrImpl = Enc.text . bytesToTextUtf8Lenient toOtelJsonAttrImpl = Enc.text . bytesToTextUtf8Lenient

View file

@ -2,6 +2,7 @@
module Html where module Html where
import Builder
import Data.Aeson qualified as Json import Data.Aeson qualified as Json
import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.KeyMap qualified as KeyMap
import Data.List.NonEmpty qualified as NonEmpty import Data.List.NonEmpty qualified as NonEmpty
@ -15,7 +16,7 @@ import Prelude hiding (span)
-- | Render an arbitrary json value to HTML in a more-or-less reasonable fashion. -- | Render an arbitrary json value to HTML in a more-or-less reasonable fashion.
mkVal :: Json.Value -> Html mkVal :: Json.Value -> Html
mkVal = \case mkVal = \case
Json.Number n -> Html.toHtml @Text $ showToText n Json.Number n -> Html.lazyText $ buildTextLazy scientificDecimalT n
Json.String s -> Html.toHtml @Text s Json.String s -> Html.toHtml @Text s
Json.Bool True -> [hsx|<em>true</em>|] Json.Bool True -> [hsx|<em>true</em>|]
Json.Bool False -> [hsx|<em>false</em>|] Json.Bool False -> [hsx|<em>false</em>|]

View file

@ -4,6 +4,7 @@ module Redacted where
import AppT import AppT
import Arg import Arg
import Builder
import Comparison import Comparison
import Control.Monad.Logger.CallStack import Control.Monad.Logger.CallStack
import Control.Monad.Reader import Control.Monad.Reader
@ -65,7 +66,7 @@ redactedGetTorrentFile dat = inSpan' "Redacted Get Torrent File" $ \span -> do
( T2 ( T2
(label @"action" "download") (label @"action" "download")
( label @"actionArgs" ( label @"actionArgs"
[ ("id", Just (dat.torrentId & showToText @Int & textToBytesUtf8)) [ ("id", Just (buildBytes intDecimalB dat.torrentId))
-- try using tokens as long as we have them (TODO: what if theres no tokens left? -- try using tokens as long as we have them (TODO: what if theres no tokens left?
-- ANSWER: it breaks: -- ANSWER: it breaks:
-- responseBody = "{\"status\":\"failure\",\"error\":\"You do not have any freeleech tokens left. Please use the regular DL link.\"}", -- responseBody = "{\"status\":\"failure\",\"error\":\"You do not have any freeleech tokens left. Please use the regular DL link.\"}",
@ -139,7 +140,7 @@ redactedSearchAndInsert extraArguments = do
redactedSearch redactedSearch
( extraArguments ( extraArguments
-- pass the page (for every search but the first one) -- pass the page (for every search but the first one)
<> (mpage & ifExists (\page -> ("page", (page :: Natural) & showToText & textToBytesUtf8))) <> (mpage & ifExists (\page -> ("page", buildBytes naturalDecimalB page)))
) )
( do ( do
status <- Json.key "status" Json.asText status <- Json.key "status" Json.asText

View file

@ -3,6 +3,7 @@
module Transmission where module Transmission where
import AppT import AppT
import Builder
import Control.Monad.Logger.CallStack import Control.Monad.Logger.CallStack
import Control.Monad.Reader import Control.Monad.Reader
import Data.Aeson qualified as Json import Data.Aeson qualified as Json
@ -25,7 +26,6 @@ import Json.Enc qualified as Enc
import Label import Label
import MyPrelude import MyPrelude
import Network.HTTP.Types import Network.HTTP.Types
import OpenTelemetry.Attributes (ToAttribute (toAttribute))
import OpenTelemetry.Trace qualified as Otel hiding (getTracer, inSpan, inSpan') import OpenTelemetry.Trace qualified as Otel hiding (getTracer, inSpan, inSpan')
import Optional import Optional
import Postgres.MonadPostgres import Postgres.MonadPostgres
@ -253,7 +253,7 @@ doTransmissionRequest ::
doTransmissionRequest span dat (req, parser) = do doTransmissionRequest span dat (req, parser) = do
sessionId <- getCurrentTransmissionSessionId sessionId <- getCurrentTransmissionSessionId
let textArg t = (Enc.text t, Otel.toAttribute @Text t) let textArg t = (Enc.text t, Otel.toAttribute @Text t)
let encArg enc = (enc, Otel.toAttribute @Text $ enc & Enc.encToTextPretty) let encArg enc = (enc, enc & toOtelJsonAttr)
let intArg i = (Enc.int i, Otel.toAttribute @Int i) let intArg i = (Enc.int i, Otel.toAttribute @Int i)
let body :: [(Text, (Enc, Otel.Attribute))] = let body :: [(Text, (Enc, Otel.Attribute))] =
@ -292,20 +292,14 @@ doTransmissionRequest span dat (req, parser) = do
& liftIO & liftIO
<&> NonEmpty.head <&> NonEmpty.head
addAttributes span' $ addAttribute span' "transmission.new_session_id" (tid, utf8LenientT)
HashMap.fromList addAttribute span' "transmission.old_session_id" (sessionId, utf8LenientT >&< fromMaybe "<none yet>")
[ ("transmission.new_session_id", tid & bytesToTextUtf8Lenient & toAttribute),
("transmission.old_session_id", sessionId <&> bytesToTextUtf8Lenient & fromMaybe "<none yet>" & toAttribute)
]
updateTransmissionSessionId tid updateTransmissionSessionId tid
doTransmissionRequest span dat (req, parser) doTransmissionRequest span dat (req, parser)
200 -> do 200 -> do
addAttributes span $ addAttribute span "transmission.valid_session_id" (sessionId, utf8LenientT >&< fromMaybe "<none yet>")
HashMap.fromList
[ ("transmission.valid_session_id", sessionId <&> bytesToTextUtf8Lenient & fromMaybe "<none yet>" & toAttribute)
]
resp resp
& Http.getResponseBody & Http.getResponseBody
& Json.parseStrict & Json.parseStrict

View file

@ -5,6 +5,7 @@ module WhatcdResolver where
import AppT import AppT
import Arg import Arg
import Builder
import Comparison import Comparison
import Control.Category qualified as Cat import Control.Category qualified as Cat
import Control.Monad.Catch.Pure (runCatch) import Control.Monad.Catch.Pure (runCatch)
@ -562,7 +563,7 @@ getBestTorrentsData ::
Maybe (Label "artistRedactedId" Natural) -> Maybe (Label "artistRedactedId" Natural) ->
Transaction m [TorrentData (Label "percentDone" Percentage)] Transaction m [TorrentData (Label "percentDone" Percentage)]
getBestTorrentsData artistFilter = inSpan' "get torrents table data" $ \span -> do getBestTorrentsData artistFilter = inSpan' "get torrents table data" $ \span -> do
artistFilter & doIfJust (\a -> addAttribute span "artist-filter.redacted-id" (a.artistRedactedId & showToText & Otel.toAttribute)) artistFilter & doIfJust (\a -> addAttribute span "artist-filter.redacted-id" (a.artistRedactedId, naturalDecimalT))
let getBest = getBestTorrents GetBestTorrentsFilter {onlyArtist = artistFilter, onlyDownloaded = False} let getBest = getBestTorrents GetBestTorrentsFilter {onlyArtist = artistFilter, onlyDownloaded = False}
bestStale :: [TorrentData ()] <- getBest bestStale :: [TorrentData ()] <- getBest
(statusInfo, transmissionStatus) <- (statusInfo, transmissionStatus) <-