packages feed

hwm-0.4.0: src/HWM/Runtime/Logging.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HWM.Runtime.Logging (logRoot, logCommandStart, logCommandEnd, debug, logPath, rotateLogs, genLogId) where

import Data.List (isSuffixOf)
import Data.Time (defaultTimeLocale, formatTime, getCurrentTime)
import HWM.Core.Common (Name)
import HWM.Core.Formatting (Color (Cyan), Format (..), chalk, slugifyList)
import HWM.Core.Options (whenDebug)
import HWM.Runtime.UI (MonadUI, putLine)
import Relude
import System.Directory (createDirectoryIfMissing, listDirectory, removeFile)
import System.FilePath ((</>))
import qualified System.IO as TIO
import System.Process.Typed (ExitCode (..))

logRoot :: FilePath
logRoot = ".hwm/logs"

logPath :: Name -> FilePath
logPath name = logRoot <> "/" <> toString name <> ".log"

logCommandStart :: TIO.Handle -> Text -> IO ()
logCommandStart logHandle cmd =
  writeLogEntry logHandle [("EVENT", "COMMAND_START"), ("COMMAND", cmd)]

logCommandEnd :: TIO.Handle -> ExitCode -> IO ()
logCommandEnd logHandle code =
  writeLogEntry logHandle [("EVENT", "COMMAND_END"), ("EXIT_CODE", exitCodeSummary code)]

debug :: (MonadIO m, MonadUI m) => Text -> m ()
debug msg = whenDebug $ putLine $ chalk Cyan "[DEBUG] " <> msg

writeLogEntry :: TIO.Handle -> [(Text, Text)] -> IO ()
writeLogEntry logHandle table = do
  timestamp <- liftIO getCurrentTime
  TIO.hPutStr logHandle (toString (formatLogEntry (table <> [("TIMESTAMP", show timestamp)])))
  TIO.hFlush logHandle

formatLogEntry :: [(Text, Text)] -> Text
formatLogEntry table =
  let boxTop = "┌──────────────────────────────────────────────────────────"
      boxBottom = "└──────────────────────────────────────────────────────────"
      rows = map (\(k, v) -> "│ " <> k <> ": " <> v) table
   in unlines (boxTop : rows <> [boxBottom, ""])

exitCodeSummary :: ExitCode -> Text
exitCodeSummary ExitSuccess = "0"
exitCodeSummary (ExitFailure code) = show code

-- 1. Generate the Timestamp once per CLI run
genLogId :: (MonadIO m) => Text -> m Text
genLogId prefix = liftIO $ do
  timestamp <- formatTime defaultTimeLocale "%Y%m%d-%H%M%S" <$> getCurrentTime
  pure $ slugifyList [format timestamp, prefix]

-- 2. The Cleanup Function
rotateLogs :: (MonadIO m) => m ()
rotateLogs = liftIO $ do
  createDirectoryIfMissing True logRoot
  files <- listDirectory logRoot
  -- Only look at .log files, sort them alphabetically (oldest first)
  let logFiles = sort $ filter (\f -> ".log" `isSuffixOf` f) files

  -- If we have 8 files and want to keep 5, we drop the first 3 (the oldest ones)
  let filesToDelete = take (length logFiles - 12) logFiles

  forM_ filesToDelete $ \f ->
    removeFile (logRoot </> f)