never executed always true always false
1 module PureClaw.Transcript.Combinator
2 ( withTranscript
3 ) where
4
5 import Control.Exception
6 import Data.Aeson qualified as Aeson
7 import Data.ByteString (ByteString)
8 import Data.Map.Strict qualified as Map
9 import Data.Text (Text)
10 import Data.Text qualified as T
11 import Data.Time
12 import Data.UUID qualified as UUID
13 import Data.UUID.V4 qualified as UUID
14
15 import PureClaw.Handles.Transcript
16 import PureClaw.Transcript.Types
17
18 -- | Wrap a @ByteString -> IO ByteString@ function with automatic transcript
19 -- logging. Records a 'Request' entry before the call and a 'Response' entry
20 -- after, linked by a shared correlation UUID. Exceptions from the wrapped
21 -- function are caught, logged with @"error"@ metadata, and re-thrown.
22 -- Transcript write failures are silently dropped so the actual call result
23 -- always takes priority over logging.
24 withTranscript
25 :: TranscriptHandle
26 -> Maybe Text -- ^ harness name (Nothing if direct)
27 -> Maybe Text -- ^ model name (Nothing if unknown)
28 -> (ByteString -> IO ByteString) -- ^ raw call
29 -> ByteString -- ^ input
30 -> IO ByteString -- ^ output (also logged)
31 withTranscript th harness model fn input = do
32 correlationId <- UUID.toText <$> UUID.nextRandom
33 entryId1 <- UUID.toText <$> UUID.nextRandom
34 startTime <- getCurrentTime
35
36 let reqEntry = TranscriptEntry
37 { _te_id = entryId1
38 , _te_timestamp = startTime
39 , _te_harness = harness
40 , _te_model = model
41 , _te_direction = Request
42 , _te_payload = encodePayload input
43 , _te_durationMs = Nothing
44 , _te_correlationId = correlationId
45 , _te_metadata = Map.empty
46 }
47
48 -- Record request; swallow transcript write failures
49 safeRecord th reqEntry
50
51 -- Call the wrapped function, catching exceptions
52 result <- try (fn input)
53
54 endTime <- getCurrentTime
55 let durationMs = utcTimeToDurationMs startTime endTime
56 entryId2 <- UUID.toText <$> UUID.nextRandom
57
58 case result of
59 Right output -> do
60 let respEntry = TranscriptEntry
61 { _te_id = entryId2
62 , _te_timestamp = endTime
63 , _te_harness = harness
64 , _te_model = model
65 , _te_direction = Response
66 , _te_payload = encodePayload output
67 , _te_durationMs = Just durationMs
68 , _te_correlationId = correlationId
69 , _te_metadata = Map.empty
70 }
71 safeRecord th respEntry
72 pure output
73
74 Left (ex :: SomeException) -> do
75 let respEntry = TranscriptEntry
76 { _te_id = entryId2
77 , _te_timestamp = endTime
78 , _te_harness = harness
79 , _te_model = model
80 , _te_direction = Response
81 , _te_payload = encodePayload mempty
82 , _te_durationMs = Just durationMs
83 , _te_correlationId = correlationId
84 , _te_metadata = Map.singleton "error" (Aeson.String (T.pack (show ex)))
85 }
86 safeRecord th respEntry
87 throwIO ex
88
89 -- | Record an entry, silently dropping any exceptions from the transcript handle.
90 safeRecord :: TranscriptHandle -> TranscriptEntry -> IO ()
91 safeRecord th entry =
92 _th_record th entry `catch` \(_ :: SomeException) -> pure ()
93
94 -- | Compute duration in milliseconds between two UTCTimes.
95 utcTimeToDurationMs :: UTCTime -> UTCTime -> Int
96 utcTimeToDurationMs start end =
97 let diffSec = realToFrac (diffUTCTime end start) :: Double
98 in round (diffSec * 1000)