|
1 |
| -{-# LANGUAGE OverloadedRecordDot #-} |
2 |
| - |
3 | 1 | -- | Parse hydra-node logs format more easy on the eyes. Parser works with regular json logs as well as journalctl format.
|
4 | 2 | module Main where
|
5 | 3 |
|
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 |
| - ) |
| 4 | +import Hydra.Prelude |
| 5 | +import Options.Applicative (execParser) |
| 6 | +import VisualizeLogs |
104 | 7 |
|
105 | 8 | main :: IO ()
|
106 | 9 | main = do
|
107 | 10 | args <- execParser opts
|
108 | 11 | 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