Merge pull request 'Replace scotty with servant' (#4) from servant into main

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-09-25 12:10:58 +03:00
6 changed files with 140 additions and 64 deletions
+2 -2
View File
@@ -40,8 +40,8 @@ import HomeAssistant.Controller.Livingroom (livingroomPresenceController, living
import HomeAssistant.Runtime.RateLimit (slidingWindowLimiter, registerRateLimitMetrics)
import UnliftIO.Async
import HomeAssistant.Runtime.Supervisor (supervised)
import qualified HomeAssistant.Runtime.Graphing
import HomeAssistant.Controller.Hallway (hallwayLightsController)
import qualified HttpServer
step :: (MonadIO m) => FilePath -> UUID -> Auto m a b -> a -> m (b, Auto m a b)
step path trace st a = do
@@ -116,7 +116,7 @@ defaultMain = withSocketsDo $ do
[ ("reader", readerAction host 8123 token ents bus)
, ("writer", writerAction writerLimiter bus)
, ("metrics", HomeAssistant.Runtime.Metrics.metricsAction store rrdPath rrdtool)
, ("metrics-http", HomeAssistant.Runtime.Graphing.graphAction rrdPath rrdtool metricsPort)
, ("metrics-http", HttpServer.runHttpServer (busLogEnv bus) rrdPath rrdtool metricsPort)
] ++ [ (name, runController rootPath bus c) | c@(Controller True name _) <- active ]
runKatipContextT (busLogEnv bus) () mempty $
mapConcurrently_ (uncurry supervised) workers
+42 -54
View File
@@ -11,37 +11,19 @@ module HomeAssistant.Runtime.Graphing
, parseNameParam
, buildGraphArgs
, readProcessBytes
, graphAction
, Graph(..)
, renderGraph
) where
import Control.Exception (evaluate)
import Control.Monad (forever)
import Control.Monad.IO.Class (liftIO, MonadIO)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Void (Void)
import Network.HTTP.Types.Status (Status, status400, status404, status500)
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort)
import System.Exit (ExitCode (..))
import System.IO (hGetContents)
import System.Process (CreateProcess (..), StdStream (..), proc, waitForProcess, withCreateProcess)
import Web.Scotty
( ActionM
, ScottyM
, defaultOptions
, get
, pathParam
, queryParamMaybe
, raw
, scottyOpts
, setHeader
, settings
, status
, text
)
import Servant.API (FromHttpApiData (..), ToHttpApiData (..))
data Range = TenMinutes | OneHour | OneDay | OneWeek | OneMonth
deriving (Eq, Show, Bounded, Enum)
@@ -56,6 +38,12 @@ rangeSuffix OneMonth = "30d"
parseRange :: Text -> Maybe Range
parseRange t = lookup t [(T.pack (rangeSuffix r), r) | r <- [minBound .. maxBound]]
instance ToHttpApiData Range where
toQueryParam = T.pack . rangeSuffix
instance FromHttpApiData Range where
parseQueryParam x = maybe (Left ("Could not parse '" <> x <> "' as Range")) Right $ parseRange x
rangeStart :: Range -> String
rangeStart r = "end-" ++ rangeSuffix r
@@ -91,6 +79,7 @@ data GraphSpec = GraphSpec
{ gsName :: Text
, gsTitle :: String
, gsPlots :: [Plot]
, gsIdentifier :: Graph
}
deriving (Eq, Show)
@@ -141,6 +130,7 @@ ratelimitGraph =
, serLabel = "Requests exhausted/min"
}
]
, gsIdentifier = RateLimit
}
trafficGraph :: GraphSpec
@@ -164,6 +154,7 @@ trafficGraph =
, serLabel = "Service calls out/min"
}
]
, gsIdentifier = Traffic
}
memoryGraph :: GraphSpec
@@ -174,17 +165,21 @@ memoryGraph =
[ area "live" "#99ccff" (average "rts_gc_current__a7a")
, line "peak" "#cc0000" (maximumCF "rts_gc_current__a7a")
]
Memory
graphs :: [GraphSpec]
graphs = [memoryGraph, ratelimitGraph, trafficGraph]
lookupGraph :: [GraphSpec] -> Text -> Maybe GraphSpec
lookupGraph specs name = case [g | g <- specs, gsName g == name] of
-- XXX: Proper lookup..?
lookupGraph :: [GraphSpec] -> Graph -> Maybe GraphSpec
lookupGraph specs graphMode = case [g | g <- specs, gsIdentifier g == graphMode] of
(g : _) -> Just g
[] -> Nothing
parseNameParam :: [GraphSpec] -> Text -> Maybe GraphSpec
parseNameParam specs t = T.stripSuffix ".png" t >>= lookupGraph specs
parseNameParam specs t = rightToMaybe (parseQueryParam t) >>= lookupGraph specs
where
rightToMaybe = either (const Nothing) Just
cfName :: CF -> String
cfName = \case
@@ -255,39 +250,32 @@ readProcessBytes cmd args =
pure (code, out, err)
_ -> pure (ExitFailure 1, BS.empty, "failed to create pipes")
respond :: Status -> TL.Text -> ActionM ()
respond st body = status st >> text body
serve :: FilePath -> FilePath -> GraphSpec -> Range -> ActionM ()
serve rrdPath rrdtool spec range = do
renderGraph :: MonadIO m => FilePath -> FilePath -> GraphSpec -> Range -> m (Either Text BS.ByteString)
renderGraph rrdPath rrdtool spec range = do
(code, out, err) <- liftIO (readProcessBytes rrdtool (buildGraphArgs rrdPath spec range))
case code of
ExitSuccess
| not (BS.null out) -> do
setHeader "Content-Type" "image/png"
raw (BL.fromStrict out)
| otherwise -> respond status500 "empty graph"
_ -> respond status500 (TL.pack (take 200 err))
| not (BS.null out) -> pure $ Right out
| otherwise -> pure $ Left "empty graph"
_ -> pure $ Left (T.pack $ take 200 err)
handler :: FilePath -> FilePath -> ActionM ()
handler rrdPath rrdtool = do
name <- pathParam "name"
case parseNameParam graphs name of
Nothing -> respond status404 "unknown graph"
Just spec -> do
rangeArg <- queryParamMaybe "range"
case rangeArg of
Nothing -> serve rrdPath rrdtool spec OneDay
Just t -> case parseRange t of
Nothing -> respond status400 "invalid range"
Just range -> serve rrdPath rrdtool spec range
app :: FilePath -> FilePath -> ScottyM ()
app rrdPath rrdtool = get "/metrics/:name" (handler rrdPath rrdtool)
graphAction :: MonadIO m => FilePath -> FilePath -> Int -> m Void
graphAction rrdPath rrdtool port = liftIO $ forever $ scottyOpts opts (app rrdPath rrdtool)
where
opts = defaultOptions
{ settings = setHost "0.0.0.0" (setPort port defaultSettings)
}
data Graph
= Memory
| Traffic
| RateLimit
deriving (Show, Eq, Enum, Bounded)
instance ToHttpApiData Graph where
toQueryParam Memory = "memory.png"
toQueryParam Traffic = "traffic.png"
toQueryParam RateLimit = "ratelimit.png"
instance FromHttpApiData Graph where
parseQueryParam "memory.png" = Right Memory
parseQueryParam "traffic.png" = Right Traffic
parseQueryParam "ratelimit.png" = Right RateLimit
parseQueryParam x = Left ("Can't parse '" <> x <> "' as Graph")