@@ -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)
2122import UnliftIO.Concurrent (threadDelay )
2223
2324import Data.Attoparsec.Text.Lazy (eitherResult , parse )
25+ import qualified Data.ByteString.Lazy as BS
2426import Data.List (nub )
2527import Data.Text (isInfixOf , stripEnd )
26- import qualified Data.ByteString.Lazy as BS
2728
2829import System.Directory (createDirectoryIfMissing )
29- import System.IO (hSetBuffering , BufferMode (.. ))
30+ import System.IO (BufferMode (.. ), hSetBuffering )
3031
3132import CLI (CLI (.. ), getCliArgs )
3233import 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 (.. ))
4043import LogAnalysis.Classifier (extractErrorCodes , extractIssuesFromLogs ,
4144 prettyFormatAnalysis , prettyFormatLogReadError ,
4245 prettyFormatNoIssues , prettyFormatNoLogs )
46+ import LogAnalysis.Exceptions (LogAnalysisException (.. ))
4347import LogAnalysis.KnowledgeCSVParser (parseKnowLedgeBase )
4448import LogAnalysis.Types (ErrorCode (.. ), Knowledge , renderErrorCode , setupAnalysis )
4549import 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
211215collectEmails :: App ()
212216collectEmails = 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
241256processTicket 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.
307321processTickets :: App ()
308322processTickets = 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
453466getZendeskResponses 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
549574responseNoLogs :: TicketInfo -> App ZendeskResponse
550575responseNoLogs TicketInfo {.. } = do
551576 Config {.. } <- ask
@@ -556,7 +581,7 @@ responseNoLogs TicketInfo{..} = do
556581 , zrIsPublic = cfgIsCommentPublic
557582 }
558583
559- -- | Filter analyzed tickets
584+ -- | Filter tickets
560585filterAnalyzedTickets :: [TicketInfo ] -> [TicketInfo ]
561586filterAnalyzedTickets ticketsInfo =
562587 filter ticketsFilter ticketsInfo
0 commit comments