|
| 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" |
0 commit comments