packages feed

himari-1.1.2.0: example/anomaly-monitor/Cpu.hs

module Cpu
  ( CpuStats (..)
  , totalTime
  , idleTime
  , calculateCpuUsage
  , readCpuStats
  , showIOCpuThreshold
  ) where

import Config
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Data.Text.Read qualified as T
import Env
import Himari

-- | CPU time statistics from `/proc/stat`.
data CpuStats = CpuStats
  { user :: Int
  , nice :: Int
  , system :: Int
  , idle :: Int
  , iowait :: Int
  , irq :: Int
  , softirq :: Int
  }
  deriving (Eq, Generic, Show)

-- | Parse CPU stats from /proc/stat content.
parseCpuStats :: Text -> Maybe CpuStats
parseCpuStats content = do
  -- Find the "cpu " line (aggregate of all CPUs)
  cpuLine <- find ("cpu " `T.isPrefixOf`) $ T.lines content
  let readDecimal x = case T.decimal x of
        Left _ -> Nothing
        Right (n, _) -> Just n
      values = mapMaybe readDecimal . drop 1 $ T.words cpuLine
  case values of
    (user' : nice' : system' : idle' : iowait' : irq' : softirq' : _) ->
      Just
        CpuStats
          { user = user'
          , nice = nice'
          , system = system'
          , idle = idle'
          , iowait = iowait'
          , irq = irq'
          , softirq = softirq'
          }
    _ -> Nothing

-- | Calculate total CPU time.
totalTime :: CpuStats -> Int
totalTime stats =
  sum
    [ stats.user
    , stats.nice
    , stats.system
    , stats.idle
    , stats.iowait
    , stats.irq
    , stats.softirq
    ]

-- | Calculate idle CPU time.
idleTime :: CpuStats -> Int
idleTime stats = stats.idle + stats.iowait

-- | Calculate CPU usage percentage between two measurements.
calculateCpuUsage :: CpuStats -> CpuStats -> Double
calculateCpuUsage prev curr =
  let totalDiff = totalTime curr - totalTime prev
      idleDiff = idleTime curr - idleTime prev
   in if totalDiff == 0
        then 0.0
        else 100.0 * fromIntegral (totalDiff - idleDiff) / fromIntegral totalDiff

-- | Read current CPU stats from /proc/stat.
readCpuStats :: (MonadLogger m, MonadUnliftIO m) => m (Maybe CpuStats)
readCpuStats = do
  contentEither <- tryAny . liftIO $ T.readFile "/proc/stat"
  case contentEither of
    Left exception -> do
      logWarnN $ "Failed to read /proc/stat: " <> convert (displayException exception)
      pure Nothing
    Right content -> pure $ parseCpuStats content

-- | Format CPU threshold and other config values as text.
showIOCpuThreshold :: (MonadIO m, MonadReader Env m) => m Text
showIOCpuThreshold = do
  config' <- view config
  return $
    "Configuration: CPU threshold = "
      <> convert (showFFloat (Just 1) (config' ^. cpuThreshold) "%")
      <> ", interval = "
      <> convert (show $ config' ^. checkInterval)
      <> "s"
      <> ", max checks = "
      <> convert (show $ config' ^. maxChecks)