Skip to content

Commit a308f41

Browse files
authored
Visualise logs (#2176)
fix #2047 New executable accepts one or more log file paths as the argument and then parses the logs back to the original types, sorts them on the timestamp field and displays using different colors for network entries, observations, messages from Head logic etc. If we like this then we could go further and use brick to conditionally show details to reduce the clutter a bit more. Since this is for internal usage I didn't bother with proper exception handling. `cabal run hydra-node:exe:visualize-logs -- log-path-1 log-path-2 log-path-3` <img width="3391" height="1357" alt="logs" src="https://github.com/user-attachments/assets/014072b5-1d89-4b0a-a334-ddf29f26292a" /> --- <!-- Consider each and tick it off one way or the other --> * [x] CHANGELOG updated or not needed * [x] Documentation updated or not needed * [x] Haddocks updated or not needed * [x] No new TODOs introduced or explained herafter
2 parents 280d7f6 + f3714be commit a308f41

File tree

17 files changed

+339
-7
lines changed

17 files changed

+339
-7
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
{-# LANGUAGE OverloadedRecordDot #-}
2+
3+
-- | Parse hydra-node logs format more easy on the eyes. Parser works with regular json logs as well as journalctl format.
4+
module Main where
5+
6+
import Hydra.Cardano.Api (Tx)
7+
import Hydra.Prelude hiding (encodeUtf8, takeWhile)
8+
9+
import Conduit
10+
import Control.Lens ((^?))
11+
import Control.Monad (foldM)
12+
import Data.Aeson (eitherDecode')
13+
import Data.Aeson.Lens (key, _String)
14+
import Data.Attoparsec.ByteString
15+
import Data.Attoparsec.ByteString qualified as A
16+
import Data.Attoparsec.ByteString.Char8 (char8, endOfLine, isEndOfLine)
17+
import Data.Text.Encoding (encodeUtf8)
18+
import Hydra.Chain (ChainEvent (..))
19+
import Hydra.HeadLogic (Effect (..), Input (..), Outcome (..), StateChanged (..))
20+
import Hydra.Logging (Envelope (..))
21+
import Hydra.Logging.Messages (HydraLog (..))
22+
import Hydra.Node (HydraNodeLog (..))
23+
import Options.Applicative hiding (Parser)
24+
import Options.Applicative qualified as Options
25+
26+
data InfoLine = InfoLine {toplabel :: LogType, details :: Text} deriving (Eq, Show)
27+
28+
data Decoded tx
29+
= DecodedHydraLog {t :: UTCTime, n :: Text, infoLine :: InfoLine}
30+
| DropLog
31+
deriving (Eq, Show)
32+
33+
-- | This instance is needed to sort results by timestamp
34+
instance Ord (Decoded tx) where
35+
compare DropLog DropLog = EQ
36+
compare DropLog DecodedHydraLog{} = LT
37+
compare DecodedHydraLog{} DropLog = GT
38+
compare (DecodedHydraLog t1 _ _) (DecodedHydraLog t2 _ _) = compare t1 t2
39+
40+
-- | Log type labels for visualization
41+
data LogType
42+
= NodeOptionsLabel
43+
| ClientSentLabel
44+
| ObservationLabel
45+
| NetworkLabel
46+
| ChainEffectLabel
47+
| ErrorLabel
48+
| LogicLabel Text
49+
| LogicError Text
50+
| Other Text
51+
deriving (Eq, Show)
52+
53+
labelLog :: LogType -> Text
54+
labelLog NodeOptionsLabel = "NODE OPTIONS"
55+
labelLog ClientSentLabel = "CLIENT SENT"
56+
labelLog ObservationLabel = "OBSERVATION"
57+
labelLog NetworkLabel = "NETWORK EFFECT"
58+
labelLog ChainEffectLabel = "POSTING TX"
59+
labelLog ErrorLabel = "ERROR"
60+
labelLog (LogicLabel t) = unlines ["LOGIC", t]
61+
labelLog (LogicError t) = unlines ["LOGIC ERROR", t]
62+
labelLog (Other t) = unlines ["OTHER", t]
63+
64+
colorLog :: LogType -> Text
65+
colorLog = \case
66+
NodeOptionsLabel -> green
67+
ClientSentLabel -> green
68+
ObservationLabel -> blue
69+
NetworkLabel -> magenta
70+
ChainEffectLabel -> blue
71+
ErrorLabel -> red
72+
LogicLabel t -> case t of
73+
"DepositExpired" -> red
74+
"DecommitInvalid" -> red
75+
"IgnoredHeadInitializing" -> red
76+
"TxInvalid" -> red
77+
_ -> cyan
78+
LogicError _ -> red
79+
Other _ -> green
80+
81+
newtype Options = Options
82+
{ paths :: [FilePath]
83+
}
84+
deriving (Show)
85+
86+
options :: Options.Parser Options
87+
options =
88+
Options
89+
<$> many
90+
( strArgument
91+
( metavar "FILES"
92+
<> help "One or more log file paths."
93+
)
94+
)
95+
96+
opts :: ParserInfo Options
97+
opts =
98+
info
99+
(options <**> helper)
100+
( fullDesc
101+
<> progDesc "Group logs by the timestamp and display using colors and separators for easy inspection."
102+
<> header "Visualize hydra-node logs"
103+
)
104+
105+
main :: IO ()
106+
main = do
107+
args <- execParser opts
108+
visualize $ paths args
109+
110+
visualize :: [FilePath] -> IO ()
111+
visualize paths = do
112+
decodedLines <-
113+
runConduitRes $
114+
mapM_ sourceFileBS paths
115+
.| linesUnboundedAsciiC
116+
.| mapMC decodeAndProcess
117+
.| filterC (/= DropLog)
118+
.| sinkList
119+
120+
forM_ (sort decodedLines) render
121+
122+
decodeAndProcess :: ByteString -> ResourceT IO (Decoded Tx)
123+
decodeAndProcess l =
124+
case inCurlyBraces l of
125+
Left _ -> lift $ pure DropLog
126+
Right incomingLine ->
127+
case incomingLine of
128+
Nothing -> lift $ pure DropLog
129+
Just jsonLine ->
130+
case jsonLine ^? key "message" . _String of
131+
Nothing -> process jsonLine
132+
Just line -> process $ encodeUtf8 line
133+
where
134+
process :: ByteString -> ResourceT IO (Decoded Tx)
135+
process line =
136+
case eitherDecode' (fromStrict line) of
137+
Left e -> do
138+
putTextLn $ red <> "Decoding failed for line: " <> decodeUtf8 l <> "\nError: " <> show e
139+
pure DropLog
140+
Right decoded -> lift $ processLogs decoded
141+
142+
charToWord8 :: Char -> Word8
143+
charToWord8 = fromIntegral . ord
144+
145+
textBetweenBraces :: A.Parser ByteString
146+
textBetweenBraces = do
147+
skipWhile (/= charToWord8 '{')
148+
_ <- char8 '{'
149+
jsonStr <- takeWhile (not . isEndOfLine)
150+
pure $ "{" <> jsonStr
151+
152+
lineParser :: A.Parser (Maybe ByteString)
153+
lineParser = do
154+
result <- optional textBetweenBraces
155+
skipWhile (not . isEndOfLine)
156+
endOfLine <|> endOfInput
157+
return result
158+
159+
inCurlyBraces :: ByteString -> Either String (Maybe ByteString)
160+
inCurlyBraces = parseOnly lineParser
161+
162+
-- | Ideally we would have Data instances for all types so we could get data type string representation
163+
-- instead of providing strings directly but that would add some compilation time overhead so not worth it.
164+
processLogs :: Envelope (HydraLog Tx) -> IO (Decoded Tx)
165+
processLogs decoded =
166+
case decoded.message of
167+
NodeOptions opt -> logIt NodeOptionsLabel opt
168+
Node msg ->
169+
case msg of
170+
BeginInput{input} ->
171+
case input of
172+
ClientInput{clientInput} -> logIt ClientSentLabel clientInput
173+
NetworkInput{} -> pure DropLog
174+
ChainInput{chainEvent} ->
175+
case chainEvent of
176+
Observation{observedTx} -> logIt ObservationLabel observedTx
177+
Rollback{} -> pure DropLog
178+
Tick{} -> pure DropLog
179+
PostTxError{postTxError} -> logIt ErrorLabel postTxError
180+
EndInput{} -> pure DropLog
181+
BeginEffect{effect} ->
182+
case effect of
183+
ClientEffect{} -> pure DropLog
184+
NetworkEffect{message} -> logIt NetworkLabel message
185+
OnChainEffect{postChainTx} -> logIt ChainEffectLabel postChainTx
186+
EndEffect{} -> pure DropLog
187+
LogicOutcome{outcome} ->
188+
case outcome of
189+
Continue{stateChanges} ->
190+
foldM
191+
( \_ a -> case a of
192+
details@HeadInitialized{} -> logIt (LogicLabel "HeadInitialized") details
193+
details@HeadOpened{} -> logIt (LogicLabel "HeadOpened") details
194+
details@CommittedUTxO{} -> logIt (LogicLabel "CommittedUTxO") details
195+
details@HeadAborted{} -> logIt (LogicLabel "HeadAborted") details
196+
details@SnapshotRequestDecided{} -> logIt (LogicLabel "SnapshotRequestDecided") details
197+
details@SnapshotRequested{} -> logIt (LogicLabel "SnapshotRequested") details
198+
details@PartySignedSnapshot{} -> logIt (LogicLabel "PartySignedSnapshot") details
199+
details@SnapshotConfirmed{} -> logIt (LogicLabel "SnapshotConfirmed") details
200+
details@DepositRecorded{} -> logIt (LogicLabel "DepositRecorded") details
201+
details@DepositActivated{} -> logIt (LogicLabel "DepositActivated") details
202+
details@DepositExpired{} -> logIt (LogicLabel "DepositExpired") details
203+
details@DepositRecovered{} -> logIt (LogicLabel "DepositRecovered") details
204+
details@CommitApproved{} -> logIt (LogicLabel "CommitApproved") details
205+
details@CommitFinalized{} -> logIt (LogicLabel "CommitFinalized") details
206+
details@DecommitRecorded{} -> logIt (LogicLabel "DecommitRecorded") details
207+
details@DecommitApproved{} -> logIt (LogicLabel "DecommitApproved") details
208+
details@DecommitInvalid{} -> logIt (LogicLabel "DecommitInvalid") details
209+
details@DecommitFinalized{} -> logIt (LogicLabel "DecommitFinalized") details
210+
details@HeadClosed{} -> logIt (LogicLabel "HeadClosed") details
211+
details@HeadContested{} -> logIt (LogicLabel "HeadContested") details
212+
details@HeadIsReadyToFanout{} -> logIt (LogicLabel "HeadIsReadyToFanout") details
213+
details@HeadFannedOut{} -> logIt (LogicLabel "HeadFannedOut") details
214+
details@IgnoredHeadInitializing{} -> logIt (LogicLabel "IgnoredHeadInitializing") details
215+
details@TxInvalid{} -> logIt (LogicLabel "TxInvalid") details
216+
NetworkConnected{} -> pure DropLog
217+
NetworkDisconnected{} -> pure DropLog
218+
PeerConnected{} -> pure DropLog
219+
PeerDisconnected{} -> pure DropLog
220+
NetworkVersionMismatch{} -> pure DropLog
221+
NetworkClusterIDMismatch{} -> pure DropLog
222+
TransactionReceived{} -> pure DropLog
223+
TransactionAppliedToLocalUTxO{} -> pure DropLog
224+
ChainRolledBack{} -> pure DropLog
225+
TickObserved{} -> pure DropLog
226+
LocalStateCleared{} -> pure DropLog
227+
Checkpoint{} -> pure DropLog
228+
)
229+
DropLog
230+
stateChanges
231+
Wait{} -> pure DropLog
232+
Error{error = err} -> logIt (LogicError "LOGIC ERROR") err
233+
DroppedFromQueue{} -> pure DropLog
234+
LoadingState -> logIt (Other "Loading state...") ()
235+
LoadedState{} -> logIt (Other "Loaded.") ()
236+
ReplayingState -> logIt (Other "Replaying state...") ()
237+
details@Misconfiguration{} -> logIt (Other "MISCONFIG!") details
238+
_ -> pure DropLog
239+
where
240+
logIt :: Show x => LogType -> x -> IO (Decoded Tx)
241+
logIt l s =
242+
pure $ DecodedHydraLog decoded.timestamp decoded.namespace (InfoLine l (show s))
243+
244+
render :: Decoded tx -> IO ()
245+
render = \case
246+
DecodedHydraLog{t, n, infoLine = InfoLine{toplabel, details}} -> do
247+
putTextLn $
248+
unlines
249+
[ "-----------------------------------"
250+
, "[" <> show t <> "]"
251+
, "NAMESPACE:" <> show n
252+
, colorLog toplabel
253+
, labelLog toplabel
254+
, details
255+
, reset
256+
]
257+
DropLog -> putTextLn ""
258+
259+
-- ANSI escape codes for colors
260+
red :: Text
261+
red = "\ESC[31m"
262+
263+
green :: Text
264+
green = "\ESC[32m"
265+
266+
blue :: Text
267+
blue = "\ESC[34m"
268+
269+
cyan :: Text
270+
cyan = "\ESC[36m"
271+
272+
magenta :: Text
273+
magenta = "\ESC[45m"
274+
275+
reset :: Text
276+
reset = "\ESC[0m"

hydra-node/hydra-node.cabal

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,25 @@ executable hydra-node
247247

248248
ghc-options: -threaded -rtsopts -with-rtsopts=-N4
249249

250+
executable visualize-logs
251+
import: project-config
252+
hs-source-dirs: exe/visualize-logs
253+
main-is: Main.hs
254+
build-depends:
255+
, aeson
256+
, attoparsec
257+
, base
258+
, conduit
259+
, hydra-cardano-api
260+
, hydra-node
261+
, hydra-prelude
262+
, lens
263+
, lens-aeson
264+
, optparse-applicative
265+
, text
266+
267+
ghc-options: -threaded -rtsopts -with-rtsopts=-N4
268+
250269
benchmark tx-cost
251270
import: project-config
252271
hs-source-dirs: bench/tx-cost

hydra-node/src/Hydra/API/APIServerLog.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
module Hydra.API.APIServerLog where
22

3-
import Hydra.Prelude
3+
import Hydra.Prelude hiding (encodeUtf8)
44

55
import Data.Aeson qualified as Aeson
6+
import Data.Text.Encoding (encodeUtf8)
67
import Hydra.Network (PortNumber)
78

89
data APIServerLog
@@ -18,7 +19,7 @@ data APIServerLog
1819
}
1920
| APITransactionSubmitted {submittedTxId :: String}
2021
deriving stock (Eq, Show, Generic)
21-
deriving anyclass (ToJSON)
22+
deriving anyclass (ToJSON, FromJSON)
2223

2324
-- | New type wrapper to define JSON instances.
2425
newtype PathInfo = PathInfo ByteString
@@ -28,6 +29,10 @@ instance ToJSON PathInfo where
2829
toJSON (PathInfo bytes) =
2930
Aeson.String $ decodeUtf8 bytes
3031

32+
instance FromJSON PathInfo where
33+
parseJSON = Aeson.withText "PathInfo" $ \t ->
34+
pure $ PathInfo $ encodeUtf8 t
35+
3136
-- | New type wrapper to define JSON instances.
3237
--
3338
-- NOTE: We are not using http-types 'StdMethod' as we do not want to be
@@ -38,3 +43,7 @@ newtype Method = Method ByteString
3843
instance ToJSON Method where
3944
toJSON (Method bytes) =
4045
Aeson.String $ decodeUtf8 bytes
46+
47+
instance FromJSON Method where
48+
parseJSON = Aeson.withText "Method" $ \t ->
49+
pure $ Method $ encodeUtf8 t

hydra-node/src/Hydra/Chain.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ data OnChainTx tx
163163
deriving stock instance IsTx tx => Eq (OnChainTx tx)
164164
deriving stock instance IsTx tx => Show (OnChainTx tx)
165165
deriving anyclass instance IsTx tx => ToJSON (OnChainTx tx)
166+
deriving anyclass instance IsTx tx => FromJSON (OnChainTx tx)
166167

167168
instance ArbitraryIsTx tx => Arbitrary (OnChainTx tx) where
168169
arbitrary = genericArbitrary
@@ -316,6 +317,7 @@ data ChainEvent tx
316317
deriving stock instance (IsTx tx, IsChainState tx) => Eq (ChainEvent tx)
317318
deriving stock instance (IsTx tx, IsChainState tx) => Show (ChainEvent tx)
318319
deriving anyclass instance (IsTx tx, IsChainState tx) => ToJSON (ChainEvent tx)
320+
deriving anyclass instance (IsTx tx, IsChainState tx) => FromJSON (ChainEvent tx)
319321

320322
instance (ArbitraryIsTx tx, IsChainState tx) => Arbitrary (ChainEvent tx) where
321323
arbitrary = genericArbitrary

hydra-node/src/Hydra/Chain/Direct/Handlers.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,4 +507,4 @@ data CardanoChainLog
507507
| RolledBackward {point :: ChainPoint}
508508
| Wallet TinyWalletLog
509509
deriving stock (Eq, Show, Generic)
510-
deriving anyclass (ToJSON)
510+
deriving anyclass (ToJSON, FromJSON)

hydra-node/src/Hydra/Chain/Direct/Wallet.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,4 @@ data TinyWalletLog
452452
deriving stock (Eq, Generic, Show)
453453

454454
deriving anyclass instance ToJSON TinyWalletLog
455+
deriving anyclass instance FromJSON TinyWalletLog

hydra-node/src/Hydra/HeadLogic/Error.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ deriving anyclass instance
4949
) =>
5050
ToJSON (LogicError tx)
5151

52+
deriving anyclass instance
53+
( FromJSON (HeadState tx)
54+
, FromJSON (Input tx)
55+
, FromJSON (RequirementFailure tx)
56+
, FromJSON (SideLoadRequirementFailure tx)
57+
) =>
58+
FromJSON (LogicError tx)
59+
5260
data RequirementFailure tx
5361
= ReqSnNumberInvalid {requestedSn :: SnapshotNumber, lastSeenSn :: SnapshotNumber}
5462
| ReqSvNumberInvalid {requestedSv :: SnapshotVersion, lastSeenSv :: SnapshotVersion}
@@ -66,6 +74,7 @@ data RequirementFailure tx
6674
deriving stock instance Eq (TxIdType tx) => Eq (RequirementFailure tx)
6775
deriving stock instance Show (TxIdType tx) => Show (RequirementFailure tx)
6876
deriving anyclass instance ToJSON (TxIdType tx) => ToJSON (RequirementFailure tx)
77+
deriving anyclass instance FromJSON (TxIdType tx) => FromJSON (RequirementFailure tx)
6978

7079
data SideLoadRequirementFailure tx
7180
= SideLoadInitialSnapshotMismatch
@@ -79,3 +88,4 @@ data SideLoadRequirementFailure tx
7988
deriving stock instance Eq (UTxOType tx) => Eq (SideLoadRequirementFailure tx)
8089
deriving stock instance Show (UTxOType tx) => Show (SideLoadRequirementFailure tx)
8190
deriving anyclass instance ToJSON (UTxOType tx) => ToJSON (SideLoadRequirementFailure tx)
91+
deriving anyclass instance FromJSON (UTxOType tx) => FromJSON (SideLoadRequirementFailure tx)

0 commit comments

Comments
 (0)