Skip to content

Commit b1feef4

Browse files
authored
[TSD-70] Exception handling when reading log files (#26)
* [TSD-70] Fix test cases * [TSD-70] Exception handling when reading log files * [TSD-70] Exception handling on reading zip file * [TSD-70] Exception handling on decompression failure * [TSD-70] Minor fixes * [TSD-70] Implement exception handling on ticket exceptions * [TSD-70] Small improvements * [TSD-70] Add show instance of processticket exception * [TSD-70] Fix test cases, add cases to check if it throws proper exception * [TSD-70] Change function name * [TSD-70] Make exception message more specific, fix text case messages * [TSD-70] Remove comment * [TSD-70] Exception handling within analysis module * [TSD-70] Small refactoring * [TSD-70] Catch exception without using tryDeep * [TSD-70] Use ! to force evaluation * [TSD-70] Cosmetic fix for readability. * [TSD-70] Small improvement to the code
1 parent 12e9d57 commit b1feef4

File tree

9 files changed

+315
-159
lines changed

9 files changed

+315
-159
lines changed

log-classifier.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ library
1717
hs-source-dirs: src
1818
exposed-modules: Classify
1919
CLI
20+
Exceptions
2021
Lib
2122
LogAnalysis.Classifier
23+
LogAnalysis.Exceptions
2224
LogAnalysis.KnowledgeCSVParser
2325
LogAnalysis.Types
2426
Regex

src/DataSource/Types.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ newtype App a = App { runAppBase :: ReaderT Config IO a }
8080
, MonadIO
8181
, MonadBase IO
8282
, MonadUnliftIO
83+
, MonadCatch
84+
, MonadThrow
8385
)
8486

8587
instance MonadBaseControl IO App where

src/Exceptions.hs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module Exceptions
2+
( ProcessTicketExceptions (..)
3+
, ZipFileExceptions (..)
4+
) where
5+
6+
import Universum
7+
8+
import DataSource (TicketId (..))
9+
10+
import Prelude (Show (..))
11+
12+
-- | Exceptions that can occur during ticket processing
13+
data ProcessTicketExceptions
14+
= AttachmentNotFound TicketId
15+
-- ^ Could not fetch the attachment even though ticket info has the url of the attachment
16+
| CommentAndAttachmentNotFound TicketId
17+
-- ^ Both attachment and comment were not found
18+
| TicketInfoNotFound TicketId
19+
-- ^ TicketInfo could not be fetched
20+
deriving (Eq)
21+
22+
-- | Exception for reading zip files
23+
data ZipFileExceptions
24+
= ReadZipFileException
25+
-- ^ Could not read the zip file because it was corrupted
26+
| DecompressionException
27+
-- ^ Decompresson of a zip file was not sucessful
28+
deriving Show
29+
30+
instance Exception ProcessTicketExceptions
31+
instance Exception ZipFileExceptions
32+
33+
instance Show ProcessTicketExceptions where
34+
show (AttachmentNotFound tid) = "Attachment was not found on ticket ID: " <> showTicketId tid
35+
show (CommentAndAttachmentNotFound tid) = "Both comment and attachment were not found on ticket ID: " <> showTicketId tid
36+
show (TicketInfoNotFound tid) = "Ticket information was not found on ticket ID: " <> showTicketId tid
37+
38+
showTicketId :: TicketId -> String
39+
showTicketId tid = Universum.show (getTicketId tid)

src/Lib.hs

Lines changed: 96 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Lib
66
, collectEmails
77
, getZendeskResponses
88
, processTicket
9+
, processTicketSafe
910
, processTickets
1011
, fetchTickets
1112
, showStatistics
@@ -21,12 +22,12 @@ import UnliftIO.Async (mapConcurrently)
2122
import UnliftIO.Concurrent (threadDelay)
2223

2324
import Data.Attoparsec.Text.Lazy (eitherResult, parse)
25+
import qualified Data.ByteString.Lazy as BS
2426
import Data.List (nub)
2527
import Data.Text (isInfixOf, stripEnd)
26-
import qualified Data.ByteString.Lazy as BS
2728

2829
import System.Directory (createDirectoryIfMissing)
29-
import System.IO (hSetBuffering, BufferMode (..))
30+
import System.IO (BufferMode (..), hSetBuffering)
3031

3132
import CLI (CLI (..), getCliArgs)
3233
import DataSource (App, Attachment (..), AttachmentContent (..), Comment (..),
@@ -37,9 +38,12 @@ import DataSource (App, Attachment (..), AttachmentContent (..), Comme
3738
asksIOLayer, asksZendeskLayer, assignToPath, connPoolDBLayer,
3839
createProdConnectionPool, defaultConfig, knowledgebasePath,
3940
renderTicketStatus, runApp, tokenPath)
41+
42+
import Exceptions (ProcessTicketExceptions (..), ZipFileExceptions (..))
4043
import LogAnalysis.Classifier (extractErrorCodes, extractIssuesFromLogs,
4144
prettyFormatAnalysis, prettyFormatLogReadError,
4245
prettyFormatNoIssues, prettyFormatNoLogs)
46+
import LogAnalysis.Exceptions (LogAnalysisException (..))
4347
import LogAnalysis.KnowledgeCSVParser (parseKnowLedgeBase)
4448
import LogAnalysis.Types (ErrorCode (..), Knowledge, renderErrorCode, setupAnalysis)
4549
import Statistics (showStatistics)
@@ -80,7 +84,7 @@ runZendeskMain = do
8084
FetchAgents -> void $ runApp fetchAgents cfg
8185
FetchTickets -> runApp fetchAndShowTickets cfg
8286
FetchTicketsFromTime fromTime -> runApp (fetchAndShowTicketsFrom fromTime) cfg
83-
(ProcessTicket ticketId) -> void $ runApp (processTicket (TicketId ticketId)) cfg
87+
(ProcessTicket ticketId) -> void $ runApp (processTicketSafe (TicketId ticketId)) cfg
8488
ProcessTickets -> void $ runApp processTickets cfg
8589
ProcessTicketsFromTime fromTime -> runApp (processTicketsFromTime fromTime) cfg
8690
ShowStatistics -> void $ runApp (fetchTickets >>= showStatistics) cfg
@@ -207,7 +211,7 @@ saveTicketDataToLocalDB (ticket, ticketComments) = do
207211

208212
pure ()
209213

210-
-- TODO(hs): Remove this function since it's not used
214+
-- | TODO(hs): Remove this function since it's not used
211215
collectEmails :: App ()
212216
collectEmails = do
213217
cfg <- ask
@@ -236,8 +240,19 @@ fetchAgents = do
236240
mapM_ print agents
237241
pure agents
238242

239-
-- | When we want to process a specific ticket.
240-
processTicket :: TicketId -> App (Maybe ZendeskResponse)
243+
-- | 'processTicket' with exception handling
244+
processTicketSafe :: TicketId -> App ()
245+
processTicketSafe tId = catch (void $ processTicket tId)
246+
-- Print and log any exceptions related to process ticket
247+
(\(e :: ProcessTicketExceptions) -> do
248+
printText <- asksIOLayer iolPrintText
249+
printText $ show e
250+
-- TODO(hs): Implement concurrent logging
251+
appendF <- asksIOLayer iolAppendFile
252+
appendF "./logs/errors.log" (show e <> "\n"))
253+
254+
-- | Process ticket with given 'TicketId'
255+
processTicket :: TicketId -> App ZendeskResponse
241256
processTicket tId = do
242257

243258
-- We see 3 HTTP calls here.
@@ -249,14 +264,14 @@ processTicket tId = do
249264
comments <- getTicketComments tId
250265

251266
let attachments = getAttachmentsFromComment comments
252-
let ticketInfo = fromMaybe (error "No ticket info") mTicketInfo
253-
254-
zendeskResponse <- getZendeskResponses comments attachments ticketInfo
267+
case mTicketInfo of
268+
Nothing -> throwM $ TicketInfoNotFound tId
269+
Just ticketInfo -> do
270+
zendeskResponse <- getZendeskResponses comments attachments ticketInfo
255271

256-
whenJust zendeskResponse $ \response -> do
257-
postTicketComment ticketInfo response
272+
postTicketComment ticketInfo zendeskResponse
258273

259-
pure zendeskResponse
274+
pure zendeskResponse
260275

261276
-- | When we want to process all tickets from a specific time onwards.
262277
-- Run in parallel.
@@ -282,33 +297,32 @@ processTicketsFromTime exportFromTime = do
282297
putTextLn "All the tickets has been processed."
283298
where
284299
-- | Process a single ticket after they were analyzed.
285-
processSingleTicket :: Maybe ZendeskResponse -> App ()
300+
processSingleTicket :: ZendeskResponse -> App ()
286301
processSingleTicket zendeskResponse = do
287302

288303
-- We first fetch the function from the configuration
289304
printText <- asksIOLayer iolPrintText
290305
appendF <- asksIOLayer iolAppendFile -- We need to remove this.
291306

292-
whenJust zendeskResponse $ \response -> do
293-
let ticketId = getTicketId $ zrTicketId response
307+
let ticketId = getTicketId $ zrTicketId zendeskResponse
294308

295-
-- Printing id before inspecting the ticket so that when the process stops by the
296-
-- corrupted log file, we know which id to blacklist.
297-
printText $ "Analyzing ticket id: " <> show ticketId
309+
-- Printing id before inspecting the ticket so that when the process stops by the
310+
-- corrupted log file, we know which id to blacklist.
311+
printText $ "Analyzing ticket id: " <> show ticketId
298312

299-
let tags = getTicketTags $ zrTags response
300-
forM_ tags $ \tag -> do
301-
let formattedTicketIdAndTag = show ticketId <> " " <> tag
302-
printText formattedTicketIdAndTag
303-
appendF "logs/analysis-result.log" (formattedTicketIdAndTag <> "\n")
313+
let tags = getTicketTags $ zrTags zendeskResponse
314+
forM_ tags $ \tag -> do
315+
let formattedTicketIdAndTag = show ticketId <> " " <> tag
316+
printText formattedTicketIdAndTag
317+
appendF "logs/analysis-result.log" (formattedTicketIdAndTag <> "\n")
304318

305319

306320
-- | When we want to process all possible tickets.
307321
processTickets :: App ()
308322
processTickets = do
309323

310324
allTickets <- fetchTickets
311-
_ <- mapM (processTicket . tiId) allTickets
325+
_ <- mapM (processTicketSafe . tiId) allTickets
312326

313327
putTextLn "All the tickets has been processed."
314328

@@ -447,30 +461,28 @@ getAttachmentsFromComment comments = do
447461
isAttachmentZip :: Attachment -> Bool
448462
isAttachmentZip attachment = "application/zip" == aContentType attachment
449463

450-
-- | Get zendesk responses
451-
-- | Returns with maybe because it could return no response
452-
getZendeskResponses :: [Comment] -> [Attachment] -> TicketInfo -> App (Maybe ZendeskResponse)
464+
-- | Inspects the comment, attachment, ticket info and create 'ZendeskResponse'
465+
getZendeskResponses :: [Comment] -> [Attachment] -> TicketInfo -> App ZendeskResponse
453466
getZendeskResponses comments attachments ticketInfo
454467
| not (null attachments) = inspectAttachments ticketInfo attachments
455-
| not (null comments) = Just <$> responseNoLogs ticketInfo
456-
| otherwise = return Nothing
468+
| not (null comments) = responseNoLogs ticketInfo
469+
| otherwise = throwM $ CommentAndAttachmentNotFound (tiId ticketInfo)
470+
-- No attachment, no comments means something is wrong with ticket itself
457471

458-
-- | Inspect only the latest attachment. We could propagate this
459-
-- @Maybe@ upwards or use an @Either@ which will go hand in hand
460-
-- with the idea that we need to improve our exception handling.
461-
inspectAttachments :: TicketInfo -> [Attachment] -> App (Maybe ZendeskResponse)
462-
inspectAttachments ticketInfo attachments = runMaybeT $ do
472+
-- | Inspect the latest attachment
473+
inspectAttachments :: TicketInfo -> [Attachment] -> App ZendeskResponse
474+
inspectAttachments ticketInfo attachments = do
463475

464476
config <- ask
465477
getAttachment <- asksZendeskLayer zlGetAttachment
466478

467-
let lastAttach :: Maybe Attachment
468-
lastAttach = safeHead . reverse . sort $ attachments
469-
470-
lastAttachment <- MaybeT . pure $ lastAttach
471-
att <- MaybeT $ getAttachment lastAttachment
472-
473-
pure $ inspectAttachment config ticketInfo att
479+
lastAttach <- handleMaybe . safeHead . reverse . sort $ attachments
480+
att <- handleMaybe =<< getAttachment lastAttach
481+
inspectAttachment config ticketInfo att
482+
where
483+
handleMaybe :: Maybe a -> App a
484+
handleMaybe Nothing = throwM $ AttachmentNotFound (tiId ticketInfo)
485+
handleMaybe (Just a) = return a
474486

475487
-- | Inspection of the local zip.
476488
-- This function prints out the analysis result on the console.
@@ -482,14 +494,14 @@ inspectLocalZipAttachment filePath = do
482494

483495
-- Read the zip file
484496
fileContent <- liftIO $ BS.readFile filePath
485-
let results = extractLogsFromZip 100 fileContent
497+
let eResults = extractLogsFromZip 100 fileContent
486498

487-
case results of
488-
Left err -> do
489-
printText err
499+
case eResults of
500+
Left (err :: ZipFileExceptions) -> do
501+
printText $ show err
490502
Right result -> do
491503
let analysisEnv = setupAnalysis $ cfgKnowledgebase config
492-
let eitherAnalysisResult = extractIssuesFromLogs result analysisEnv
504+
eitherAnalysisResult <- try $ extractIssuesFromLogs result analysisEnv
493505

494506
case eitherAnalysisResult of
495507
Right analysisResult -> do
@@ -501,51 +513,64 @@ inspectLocalZipAttachment filePath = do
501513
printText "Error codes:"
502514
void $ mapM printText errorCodes
503515

504-
Left e -> do
505-
printText e
516+
Left (e :: LogAnalysisException) -> do
517+
printText $ show e
506518

507519
-- | Given number of file of inspect, knowledgebase and attachment,
508520
-- analyze the logs and return the results.
509-
inspectAttachment :: Config -> TicketInfo -> AttachmentContent -> ZendeskResponse
510-
inspectAttachment Config{..} ticketInfo@TicketInfo{..} attContent = do
511-
512-
let rawLog = getAttachmentContent attContent
513-
let results = extractLogsFromZip cfgNumOfLogsToAnalyze rawLog
521+
inspectAttachment :: (MonadCatch m) => Config -> TicketInfo -> AttachmentContent -> m ZendeskResponse
522+
inspectAttachment Config{..} ticketInfo@TicketInfo{..} attachment = do
514523

515-
case results of
516-
Left _ -> do
524+
let analysisEnv = setupAnalysis cfgKnowledgebase
525+
let eLogFiles = extractLogsFromZip cfgNumOfLogsToAnalyze (getAttachmentContent attachment)
517526

518-
ZendeskResponse
527+
case eLogFiles of
528+
Left _ ->
529+
-- Log file was corrupted
530+
pure $ ZendeskResponse
519531
{ zrTicketId = tiId
520532
, zrComment = prettyFormatLogReadError ticketInfo
521533
, zrTags = TicketTags [renderErrorCode SentLogCorrupted]
522534
, zrIsPublic = cfgIsCommentPublic
523535
}
524-
Right result -> do
525-
let analysisEnv = setupAnalysis cfgKnowledgebase
526-
let eitherAnalysisResult = extractIssuesFromLogs result analysisEnv
527536

528-
case eitherAnalysisResult of
537+
Right logFiles -> do
538+
-- Log files maybe corrupted or issue was not found
539+
tryAnalysisResult <- try $ extractIssuesFromLogs logFiles analysisEnv
540+
541+
case tryAnalysisResult of
529542
Right analysisResult -> do
543+
-- Known issue was found
530544
let errorCodes = extractErrorCodes analysisResult
531545
let commentRes = prettyFormatAnalysis analysisResult ticketInfo
532546

533-
ZendeskResponse
547+
pure $ ZendeskResponse
534548
{ zrTicketId = tiId
535549
, zrComment = commentRes
536550
, zrTags = TicketTags errorCodes
537551
, zrIsPublic = cfgIsCommentPublic
538552
}
539553

540-
Left _ -> do
541-
542-
ZendeskResponse
543-
{ zrTicketId = tiId
544-
, zrComment = prettyFormatNoIssues ticketInfo
545-
, zrTags = TicketTags [renderTicketStatus NoKnownIssue]
546-
, zrIsPublic = cfgIsCommentPublic
547-
}
548-
554+
Left (analysisException :: LogAnalysisException) ->
555+
case analysisException of
556+
-- Could not read the log files
557+
LogReadException ->
558+
pure $ ZendeskResponse
559+
{ zrTicketId = tiId
560+
, zrComment = prettyFormatLogReadError ticketInfo
561+
, zrTags = TicketTags [renderErrorCode DecompressionFailure]
562+
, zrIsPublic = cfgIsCommentPublic
563+
}
564+
-- No known issue was found
565+
NoKnownIssueFound ->
566+
pure $ ZendeskResponse
567+
{ zrTicketId = tiId
568+
, zrComment = prettyFormatNoIssues ticketInfo
569+
, zrTags = TicketTags [renderTicketStatus NoKnownIssue]
570+
, zrIsPublic = cfgIsCommentPublic
571+
}
572+
573+
-- | Create 'ZendeskResponse' stating no logs were found on the ticket
549574
responseNoLogs :: TicketInfo -> App ZendeskResponse
550575
responseNoLogs TicketInfo{..} = do
551576
Config {..} <- ask
@@ -556,7 +581,7 @@ responseNoLogs TicketInfo{..} = do
556581
, zrIsPublic = cfgIsCommentPublic
557582
}
558583

559-
-- | Filter analyzed tickets
584+
-- | Filter tickets
560585
filterAnalyzedTickets :: [TicketInfo] -> [TicketInfo]
561586
filterAnalyzedTickets ticketsInfo =
562587
filter ticketsFilter ticketsInfo

0 commit comments

Comments
 (0)