mackerel-client (empty) → 0.0.0
raw patch · 36 files changed
+1935/−0 lines, 36 filesdep +aesondep +aeson-qqdep +basesetup-changed
Dependencies added: aeson, aeson-qq, base, bytestring, data-default, directory, filepath, hspec, htoml, http-client, http-client-tls, http-types, mackerel-client, parsec, raw-strings-qq, split, text, unordered-containers
Files
- LICENSE +21/−0
- Setup.hs +2/−0
- mackerel-client.cabal +82/−0
- src/Web/Mackerel.hs +6/−0
- src/Web/Mackerel/Api.hs +11/−0
- src/Web/Mackerel/Api/Alert.hs +26/−0
- src/Web/Mackerel/Api/Dashboard.hs +43/−0
- src/Web/Mackerel/Api/Host.hs +90/−0
- src/Web/Mackerel/Api/Monitor.hs +39/−0
- src/Web/Mackerel/Api/Organization.hs +12/−0
- src/Web/Mackerel/Api/Role.hs +20/−0
- src/Web/Mackerel/Api/Service.hs +30/−0
- src/Web/Mackerel/Api/User.hs +24/−0
- src/Web/Mackerel/Client.hs +17/−0
- src/Web/Mackerel/Config.hs +108/−0
- src/Web/Mackerel/Internal/Api.hs +52/−0
- src/Web/Mackerel/Internal/TH.hs +23/−0
- src/Web/Mackerel/Types.hs +10/−0
- src/Web/Mackerel/Types/Alert.hs +47/−0
- src/Web/Mackerel/Types/Dashboard.hs +42/−0
- src/Web/Mackerel/Types/Host.hs +148/−0
- src/Web/Mackerel/Types/Monitor.hs +129/−0
- src/Web/Mackerel/Types/Organization.hs +11/−0
- src/Web/Mackerel/Types/Role.hs +14/−0
- src/Web/Mackerel/Types/Service.hs +15/−0
- src/Web/Mackerel/Types/User.hs +29/−0
- test/ConfigSpec.hs +63/−0
- test/Spec.hs +1/−0
- test/Types/AlertSpec.hs +107/−0
- test/Types/DashboardSpec.hs +69/−0
- test/Types/HostSpec.hs +314/−0
- test/Types/MonitorSpec.hs +173/−0
- test/Types/OrganizationSpec.hs +32/−0
- test/Types/RoleSpec.hs +39/−0
- test/Types/ServiceSpec.hs +45/−0
- test/Types/UserSpec.hs +41/−0
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2016 itchyny <https://github.com/itchyny>++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ mackerel-client.cabal view
@@ -0,0 +1,82 @@+name: mackerel-client+version: 0.0.0+author: itchyny+maintainer: itchyny <https://github.com/itchyny>+license: MIT+license-file: LICENSE+category: Web+build-type: Simple+cabal-version: >=1.8+synopsis: An API client library for Mackerel (https://mackerel.io)+description: This library provides bindings to Mackerel APIs.+ .+ The official site of Mackerel: https://mackerel.io/.+ The reference of Mackerel API: https://mackerel.io/api-docs/.++library+ hs-source-dirs: src+ ghc-options: -Wall+ exposed-modules: Web.Mackerel+ , Web.Mackerel.Types+ , Web.Mackerel.Types.Organization+ , Web.Mackerel.Types.User+ , Web.Mackerel.Types.Service+ , Web.Mackerel.Types.Role+ , Web.Mackerel.Types.Host+ , Web.Mackerel.Types.Monitor+ , Web.Mackerel.Types.Alert+ , Web.Mackerel.Types.Dashboard+ , Web.Mackerel.Api+ , Web.Mackerel.Api.Organization+ , Web.Mackerel.Api.User+ , Web.Mackerel.Api.Service+ , Web.Mackerel.Api.Role+ , Web.Mackerel.Api.Host+ , Web.Mackerel.Api.Monitor+ , Web.Mackerel.Api.Alert+ , Web.Mackerel.Api.Dashboard+ , Web.Mackerel.Client+ , Web.Mackerel.Config+ other-modules: Web.Mackerel.Internal.TH+ , Web.Mackerel.Internal.Api+ build-depends: base >= 4.7 && < 5.0+ , filepath+ , directory+ , http-types+ , http-client+ , http-client-tls+ , bytestring+ , parsec+ , aeson+ , htoml+ , text+ , unordered-containers+ , data-default+ , split++test-suite spec+ hs-source-dirs: test+ main-is: Spec.hs+ type: exitcode-stdio-1.0+ ghc-options: -Wall+ other-modules: Types.OrganizationSpec+ , Types.UserSpec+ , Types.ServiceSpec+ , Types.RoleSpec+ , Types.HostSpec+ , Types.MonitorSpec+ , Types.AlertSpec+ , Types.DashboardSpec+ , ConfigSpec+ build-depends: base >= 4.7 && < 5.0+ , mackerel-client+ , aeson+ , aeson-qq+ , raw-strings-qq+ , unordered-containers+ , data-default+ , hspec++source-repository head+ type: git+ location: git@github.com:itchyny/mackerel-client-hs.git
+ src/Web/Mackerel.hs view
@@ -0,0 +1,6 @@+module Web.Mackerel (module Mackerel) where++import Web.Mackerel.Api as Mackerel+import Web.Mackerel.Client as Mackerel+import Web.Mackerel.Config as Mackerel+import Web.Mackerel.Types as Mackerel
+ src/Web/Mackerel/Api.hs view
@@ -0,0 +1,11 @@+module Web.Mackerel.Api (module Api, ApiError, errorStatusCode, errorMessage) where++import Web.Mackerel.Api.Alert as Api+import Web.Mackerel.Api.Dashboard as Api+import Web.Mackerel.Api.Host as Api+import Web.Mackerel.Api.Monitor as Api+import Web.Mackerel.Api.Organization as Api+import Web.Mackerel.Api.Role as Api+import Web.Mackerel.Api.Service as Api+import Web.Mackerel.Api.User as Api+import Web.Mackerel.Internal.Api (ApiError, errorStatusCode, errorMessage)
+ src/Web/Mackerel/Api/Alert.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Alert API.+module Web.Mackerel.Api.Alert (listAlerts, closeAlert) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import qualified Data.HashMap.Lazy as HM+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Alert++data ListAlertsResponse = ListAlertsResponse { responseAlerts :: [Alert] }+$(deriveJSON options ''ListAlertsResponse)++listAlerts :: Client -> IO (Either ApiError [Alert])+listAlerts client+ = request client GET "/api/v0/alerts" [] emptyBody (createHandler responseAlerts)++closeAlert :: Client -> AlertId -> String -> IO (Either ApiError Alert)+closeAlert client (AlertId alertId') reason = do+ let body = Just $ HM.fromList [("reason", reason) :: (String, String)]+ request client POST ("/api/v0/alerts/" <> BS.pack alertId' <> "/close") [] body (createHandler id)
+ src/Web/Mackerel/Api/Dashboard.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Dashboard API.+module Web.Mackerel.Api.Dashboard+ ( listDashboards+ , getDashboard+ , createDashboard+ , updateDashboard+ , deleteDashboard+ ) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Dashboard++data ListDashboardsResponse = ListDashboardsResponse { responseDashboards :: [Dashboard] }+$(deriveJSON options ''ListDashboardsResponse)++listDashboards :: Client -> IO (Either ApiError [Dashboard])+listDashboards client+ = request client GET "/api/v0/dashboards" [] emptyBody (createHandler responseDashboards)++getDashboard :: Client -> DashboardId -> IO (Either ApiError Dashboard)+getDashboard client (DashboardId dashboardId')+ = request client GET ("/api/v0/dashboards/" <> BS.pack dashboardId') [] emptyBody (createHandler id)++createDashboard :: Client -> DashboardCreate -> IO (Either ApiError Dashboard)+createDashboard client dashboard+ = request client POST "/api/v0/dashboards" [] (Just dashboard) (createHandler id)++updateDashboard :: Client -> Dashboard -> IO (Either ApiError Dashboard)+updateDashboard client dashboard = do+ let (DashboardId dashboardId') = dashboardId dashboard+ request client PUT ("/api/v0/dashboards/" <> BS.pack dashboardId') [] (Just dashboard) (createHandler id)++deleteDashboard :: Client -> DashboardId -> IO (Either ApiError Dashboard)+deleteDashboard client (DashboardId dashboardId')+ = request client DELETE ("/api/v0/dashboards/" <> BS.pack dashboardId') [] emptyBody (createHandler id)
+ src/Web/Mackerel/Api/Host.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Host API.+module Web.Mackerel.Api.Host+ ( createHost+ , getHost+ , updateHost+ , updateHostStatus+ , updateHostRoleFullnames+ , retireHost+ , listHosts+ , ListHostsParams(..)+ , listHostMetricNames+ ) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Default (Default(..))+import qualified Data.HashMap.Lazy as HM+import Data.Maybe (maybeToList)+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Host++data CreateHostResponse = CreateHostResponse { responseId :: HostId }+$(deriveJSON options ''CreateHostResponse)++createHost :: Client -> HostCreate -> IO (Either ApiError HostId)+createHost client host+ = request client POST "/api/v0/hosts" [] (Just host) (createHandler responseId)++data GetHostResponse = GetHostResponse { responseHost :: Host }+$(deriveJSON options ''GetHostResponse)++getHost :: Client -> HostId -> IO (Either ApiError Host)+getHost client (HostId hostId')+ = request client GET ("/api/v0/hosts/" <> BS.pack hostId') [] emptyBody (createHandler responseHost)++updateHost :: Client -> HostId -> HostCreate -> IO (Either ApiError HostId)+updateHost client (HostId hostId') host+ = request client PUT ("/api/v0/hosts/" <> BS.pack hostId') [] (Just host) (createHandler responseId)++data SuccessResponse = SuccessResponse { responseSuccess :: Bool }+$(deriveJSON options ''SuccessResponse)++updateHostStatus :: Client -> HostId -> HostStatus -> IO (Either ApiError Bool)+updateHostStatus client (HostId hostId') status = do+ let body = Just $ HM.fromList [("status", status) :: (String, HostStatus)]+ request client POST ("/api/v0/hosts/" <> BS.pack hostId' <> "/status") [] body (createHandler responseSuccess)++updateHostRoleFullnames :: Client -> HostId -> [String] -> IO (Either ApiError Bool)+updateHostRoleFullnames client (HostId hostId') roleFullnames = do+ let body = Just $ HM.fromList [("roleFullnames", roleFullnames ) :: (String, [String])]+ request client PUT ("/api/v0/hosts/" <> BS.pack hostId' <> "/role-fullnames") [] body (createHandler responseSuccess)++retireHost :: Client -> HostId -> IO (Either ApiError Bool)+retireHost client (HostId hostId')+ = request client POST ("/api/v0/hosts/" <> BS.pack hostId' <> "/retire") [] emptyBody (createHandler responseSuccess)++data ListHostsParams+ = ListHostsParams {+ listHostsParamsService :: Maybe String,+ listHostsParamsRoles :: [String],+ listHostsParamsName :: [String],+ listHostsParamsStatus :: [HostStatus]+ }++instance Default ListHostsParams where+ def = ListHostsParams Nothing [] [] [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff]++data ListHostsResponse = ListHostsResponse { responseHosts :: [Host] }+$(deriveJSON options ''ListHostsResponse)++listHosts :: Client -> ListHostsParams -> IO (Either ApiError [Host])+listHosts client params = do+ let query = [ ("service", Just $ BS.pack s) | s <- maybeToList (listHostsParamsService params) ] +++ [ ("role", Just $ BS.pack r) | r <- listHostsParamsRoles params ] +++ [ ("name", Just $ BS.pack r) | r <- listHostsParamsName params ] +++ [ ("status", Just $ BS.pack $ show s) | s <- listHostsParamsStatus params ]+ request client GET "/api/v0/hosts" query emptyBody (createHandler responseHosts)++data ListMetricNamesResponse = ListMetricNamesResponse { responseNames :: [String] }+$(deriveJSON options ''ListMetricNamesResponse)++listHostMetricNames :: Client -> HostId -> IO (Either ApiError [String])+listHostMetricNames client (HostId hostId')+ = request client GET ("/api/v0/hosts/" <> BS.pack hostId' <> "/metric-names") [] emptyBody (createHandler responseNames)
+ src/Web/Mackerel/Api/Monitor.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Monitor API.+module Web.Mackerel.Api.Monitor+ ( listMonitors+ , createMonitor+ , updateMonitor+ , deleteMonitor+ ) where++import Data.Aeson (Value)+import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Monitor++data ListMonitorsResponse = ListMonitorsResponse { responseMonitors :: [Monitor] }+$(deriveJSON options ''ListMonitorsResponse)++listMonitors :: Client -> IO (Either ApiError [Monitor])+listMonitors client+ = request client GET "/api/v0/monitors" [] emptyBody (createHandler responseMonitors)++createMonitor :: Client -> Monitor -> IO (Either ApiError Monitor)+createMonitor client monitor+ = request client POST "/api/v0/monitors" [] (Just monitor) (createHandler id)++updateMonitor :: Client -> Monitor -> IO (Either ApiError Monitor)+updateMonitor client monitor = do+ let Just (MonitorId monitorId') = monitorId monitor+ request client PUT ("/api/v0/monitors/" <> BS.pack monitorId') [] (Just monitor) (createHandler id)++deleteMonitor :: Client -> MonitorId -> IO (Either ApiError Value)+deleteMonitor client (MonitorId monitorId')+ = request client DELETE ("/api/v0/monitors/" <> BS.pack monitorId') [] emptyBody (createHandler id)
+ src/Web/Mackerel/Api/Organization.hs view
@@ -0,0 +1,12 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Organization API.+module Web.Mackerel.Api.Organization (getOrganization) where++import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Types.Organization++getOrganization :: Client -> IO (Either ApiError Organization)+getOrganization client = request client GET "/api/v0/org" [] emptyBody (createHandler id)
+ src/Web/Mackerel/Api/Role.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Role API.+module Web.Mackerel.Api.Role (listRoles) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Role++data ListRolesResponse = ListRolesResponse { responseRoles :: [Role] }+$(deriveJSON options ''ListRolesResponse)++listRoles :: Client -> String -> IO (Either ApiError [Role])+listRoles client serviceName'+ = request client GET ("/api/v0/services/" <> BS.pack serviceName' <> "/roles") [] emptyBody (createHandler responseRoles)
+ src/Web/Mackerel/Api/Service.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | Service API.+module Web.Mackerel.Api.Service+ ( listServices+ , listServiceMetricNames+ ) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Service++data ListServicesResponse = ListServicesResponse { responseServices :: [Service] }+$(deriveJSON options ''ListServicesResponse)++listServices :: Client -> IO (Either ApiError [Service])+listServices client+ = request client GET "/api/v0/services" [] emptyBody (createHandler responseServices)++data ListMetricNamesResponse = ListMetricNamesResponse { responseNames :: [String] }+$(deriveJSON options ''ListMetricNamesResponse)++listServiceMetricNames :: Client -> String -> IO (Either ApiError [String])+listServiceMetricNames client serviceName'+ = request client GET ("/api/v0/services/" <> BS.pack serviceName' <> "/metric-names") [] emptyBody (createHandler responseNames)
+ src/Web/Mackerel/Api/User.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+-- | User API.+module Web.Mackerel.Api.User (listUsers, deleteUser) where++import Data.Aeson.TH (deriveJSON)+import qualified Data.ByteString.Char8 as BS+import Data.Semigroup ((<>))+import Network.HTTP.Types (StdMethod(..))++import Web.Mackerel.Client+import Web.Mackerel.Internal.Api+import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.User++data ListUsersResponse = ListUsersResponse { responseUsers :: [User] }+$(deriveJSON options ''ListUsersResponse)++listUsers :: Client -> IO (Either ApiError [User])+listUsers client+ = request client GET "/api/v0/users" [] emptyBody (createHandler responseUsers)++deleteUser :: Client -> UserId -> IO (Either ApiError User)+deleteUser client (UserId userId')+ = request client DELETE ("/api/v0/users/" <> BS.pack userId') [] emptyBody (createHandler id)
+ src/Web/Mackerel/Client.hs view
@@ -0,0 +1,17 @@+module Web.Mackerel.Client (Client, apiKey, apiBase, userAgent) where++import Data.Default (Default(..))++data Client+ = Client {+ apiKey :: String,+ apiBase :: String,+ userAgent :: String+ } deriving (Eq, Show)++instance Default Client where+ def = Client {+ apiKey = "",+ apiBase = "https://mackerel.io",+ userAgent = "mackerel-client-hs"+ }
+ src/Web/Mackerel/Config.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}+module Web.Mackerel.Config+ ( Config(..)+ , HostStatusConfig(..)+ , loadConfig+ , parseConfig+ , loadClient+ , mackerelRoot+ , confFile+ , pidFile+ ) where++import Data.Aeson+import Data.Aeson.Types (Value(..), Result(..), typeMismatch)+import Data.Default (Default(..))+import Data.List (isPrefixOf, isInfixOf)+import Data.Maybe (fromMaybe)+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import System.Directory (getHomeDirectory)+import System.FilePath ((</>))+import System.Info (os)+import Text.Toml (parseTomlDoc)++import Web.Mackerel.Client+import Web.Mackerel.Types.Host++data Config+ = Config {+ configApiBase :: Maybe String,+ configApiKey :: Maybe String,+ configRoot :: Maybe String,+ configPidfile :: Maybe String,+ configRoles :: Maybe [String],+ configVerbose :: Maybe Bool,+ configDiagnostic :: Maybe Bool,+ configDisplayname :: Maybe String,+ configHostStatus :: Maybe HostStatusConfig,+ configHttpProxy :: Maybe String+ } deriving (Eq, Show)++instance FromJSON Config where+ parseJSON (Object o)+ = Config <$> o .:? "apibase" <*> o .:? "apikey" <*> o .:? "root"+ <*> o .:? "pidfile" <*> o .:? "roles" <*> o .:? "verbose"+ <*> o .:? "diagnostic" <*> o .:? "display_name" <*> o .:? "host_status"+ <*> o .:? "http_proxy"+ parseJSON o = typeMismatch "Config" o++instance Default Config where+ def = Config def def def def def def def def def def++data HostStatusConfig+ = HostStatusConfig {+ hostStatusOnStart :: Maybe HostStatus,+ hostStatusOnStop :: Maybe HostStatus+ } deriving (Eq, Show)++instance FromJSON HostStatusConfig where+ parseJSON (Object o)+ = HostStatusConfig <$> o .:? "on_start" <*> o .:? "on_stop"+ parseJSON o = typeMismatch "HostStatusConfig" o++instance Default HostStatusConfig where+ def = HostStatusConfig def def++agentName :: String+agentName = "mackerel-agent"++loadConfig :: IO (Either String Config)+loadConfig = parseConfig <$> (Text.readFile =<< confFile)++parseConfig :: Text.Text -> Either String Config+parseConfig cnt+ = case parseTomlDoc "" cnt of+ Left _ -> Left "toml parse error"+ Right res -> case fromJSON $ toJSON res of+ Error err -> Left err+ Success r -> Right r++loadClient :: IO (Either String Client)+loadClient = fmap (\c -> def { apiKey = fromMaybe (apiKey def) $ configApiKey c,+ apiBase = fromMaybe (apiBase def) $ configApiBase c }) <$> loadConfig++mackerelRoot :: IO FilePath+mackerelRoot | isBSD || isDarwin = getHomeDirectory >>= \home -> return $ home </> "Library" </> agentName+ | isWindows = error "Windows is not supported yet."+ | otherwise = return "/var/lib/mackerel-agent"++confFile :: IO FilePath+confFile | isLinux = return $ "/etc" </> agentName </> agentName ++ ".conf"+ | otherwise = mackerelRoot >>= \root -> return $ root </> agentName ++ ".conf"++pidFile :: IO FilePath+pidFile | isLinux = return "/var/run/mackerel-agent.pid"+ | otherwise = mackerelRoot >>= \root -> return $ root </> agentName ++ ".pid"++isBSD :: Bool+isBSD = "bsd" `isInfixOf` os++isDarwin :: Bool+isDarwin = os == "darwin"++isWindows :: Bool+isWindows = "mingw" `isPrefixOf` os++isLinux :: Bool+isLinux = not isBSD && not isDarwin && not isWindows
+ src/Web/Mackerel/Internal/Api.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Internal helpers for APIs.+module Web.Mackerel.Internal.Api (request, emptyBody, createHandler, ApiError, errorStatusCode, errorMessage) where++import Control.Monad ((>=>))+import Data.Aeson (encode, decode, (.:), FromJSON, ToJSON)+import Data.Aeson.Types (parseMaybe)+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy as LBS+import Data.Maybe (maybe, fromMaybe)+import Data.Semigroup ((<>))+import Network.HTTP.Client+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types (StdMethod, renderStdMethod, Query, statusCode)++import Web.Mackerel.Client++type ResponseHandler a = Response LBS.ByteString -> Either ApiError a++-- | Error information of the response of API.+data ApiError = ApiError { errorStatusCode :: Int, errorMessage :: String } deriving (Eq, Show)++-- | Request to Mackerel.+request :: ToJSON a => Client -> StdMethod -> BS.ByteString -> Query -> Maybe a -> ResponseHandler b -> IO (Either ApiError b)+request client method' path' query body handler = do+ initialRequest <- setQueryString query <$> parseRequest (apiBase client)+ handler <$> (httpLbs initialRequest {+ method = renderStdMethod method', path = path', requestBody = RequestBodyLBS $ maybe "" encode body,+ requestHeaders = requestHeaders initialRequest ++ [+ ("X-Api-Key", BS.pack $ apiKey client),+ ("User-Agent", BS.pack $ userAgent client),+ ("Content-Type", "application/json")+ ]+ } =<< newManager (if secure initialRequest then tlsManagerSettings else defaultManagerSettings))++-- | Empty body (to avoid type ambiguity).+emptyBody :: Maybe ()+emptyBody = Nothing++-- | Create an api response handler.+createHandler :: FromJSON a => (a -> b) -> ResponseHandler b+createHandler extractor response+ | statusCode (responseStatus response) == 200 = maybe (Left decodeError) (Right . extractor) (decode (responseBody response))+ | otherwise = Left $ maybe decodeError getApiError $ decode (responseBody response)+ where getApiError json = ApiError {+ errorStatusCode = statusCode $ responseStatus response,+ errorMessage = fromMaybe "" $ parseMaybe (((.: "error") >=> (.: "message")) <> (.: "error")) json+ }+ decodeError = ApiError {+ errorStatusCode = statusCode $ responseStatus response,+ errorMessage = show $ responseBody response+ }
+ src/Web/Mackerel/Internal/TH.hs view
@@ -0,0 +1,23 @@+-- | Helpers for deriveJSON.+module Web.Mackerel.Internal.TH (options, kebabCase, snakeCase) where++import Data.Aeson.TH (Options(..), defaultOptions)+import Data.Char (isLower, isUpper, toLower)+import Data.List (intercalate)+import Data.List.Split (keepDelimsL, split, whenElt)++options :: Options+options = defaultOptions {+ fieldLabelModifier = (\(c:cs) -> toLower c : cs) . dropWhile isLower,+ constructorTagModifier = map toLower,+ omitNothingFields = True+}++kebabCase :: String -> String+kebabCase = modifyCase "-"++snakeCase :: String -> String+snakeCase = modifyCase "_"++modifyCase :: String -> String -> String+modifyCase cs = intercalate cs . map (map toLower) . dropWhile null . split (keepDelimsL $ whenElt isUpper)
+ src/Web/Mackerel/Types.hs view
@@ -0,0 +1,10 @@+module Web.Mackerel.Types (module Types) where++import Web.Mackerel.Types.Alert as Types+import Web.Mackerel.Types.Dashboard as Types+import Web.Mackerel.Types.Host as Types+import Web.Mackerel.Types.Monitor as Types+import Web.Mackerel.Types.Organization as Types+import Web.Mackerel.Types.Role as Types+import Web.Mackerel.Types.Service as Types+import Web.Mackerel.Types.User as Types
+ src/Web/Mackerel/Types/Alert.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Alert where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, constructorTagModifier)+import Data.Aeson.Types (typeMismatch)+import Data.Char (toUpper)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH+import Web.Mackerel.Types.Host (HostId)+import Web.Mackerel.Types.Monitor (MonitorId, MonitorType)++data AlertId = AlertId String+ deriving (Eq, Show)++instance FromJSON AlertId where+ parseJSON (Aeson.String alertId) = return $ AlertId $ Text.unpack alertId+ parseJSON o = typeMismatch "AlertId" o++instance ToJSON AlertId where+ toJSON (AlertId alertId) = toJSON alertId++data AlertStatus = AlertStatusOk+ | AlertStatusCritical+ | AlertStatusWarning+ | AlertStatusUnknown+ deriving (Eq, Show)++$(deriveJSON options { constructorTagModifier = map toUpper . drop 11 } ''AlertStatus)++data Alert+ = Alert {+ alertId :: AlertId,+ alertStatus :: AlertStatus,+ alertMonitorId :: Maybe MonitorId,+ alertType :: MonitorType,+ alertHostId :: Maybe HostId,+ alertValue :: Maybe Double,+ alertMessage :: Maybe String,+ alertReason :: Maybe String,+ alertOpenedAt :: Integer,+ alertClosedAt :: Maybe Integer+ } deriving (Eq, Show)++$(deriveJSON options ''Alert)
+ src/Web/Mackerel/Types/Dashboard.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Dashboard where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, fieldLabelModifier)+import Data.Aeson.Types (typeMismatch)+import Data.Char (toLower)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data DashboardId = DashboardId String+ deriving (Eq, Show)++instance FromJSON DashboardId where+ parseJSON (Aeson.String dashboardId') = return $ DashboardId $ Text.unpack dashboardId'+ parseJSON o = typeMismatch "DashboardId" o++instance ToJSON DashboardId where+ toJSON (DashboardId dashboardId') = toJSON dashboardId'++data DashboardCreate+ = DashboardCreate {+ dashboardCreateTitle :: String,+ dashboardCreateBodyMarkdown :: String,+ dashboardCreateUrlPath :: String+ } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 15 } ''DashboardCreate)++data Dashboard+ = Dashboard {+ dashboardId :: DashboardId,+ dashboardTitle :: String,+ dashboardBodyMarkdown :: String,+ dashboardUrlPath :: String,+ dashboardCreatedAt :: Integer,+ dashboardUpdatedAt :: Integer+ } deriving (Eq, Show)++$(deriveJSON options ''Dashboard)
+ src/Web/Mackerel/Types/Host.hs view
@@ -0,0 +1,148 @@+{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}+module Web.Mackerel.Types.Host where++import Control.Applicative ((<|>))+import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, constructorTagModifier, fieldLabelModifier)+import Data.Aeson.Types (Parser, typeMismatch)+import Data.Char (toLower)+import Data.Default (Default(..))+import qualified Data.HashMap.Lazy as HM+import Data.Maybe (isJust)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data HostId = HostId String+ deriving (Eq, Show)++instance FromJSON HostId where+ parseJSON (Aeson.String hostId) = return $ HostId $ Text.unpack hostId+ parseJSON o = typeMismatch "HostId" o++instance ToJSON HostId where+ toJSON (HostId hostId) = toJSON hostId++data HostMetaCloud+ = HostMetaCloud {+ metaCloudProvider :: String,+ metaCloudMetadata :: HM.HashMap String Value+ } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = map toLower . drop 9 } ''HostMetaCloud)++data HostMetaCpu+ = HostMetaCpu {+ metaCpuCacheSize :: Maybe String,+ metaCpuCoreId :: Maybe String,+ metaCpuCores :: Maybe String,+ metaCpuFamily :: Maybe String,+ metaCpuMhz :: Maybe String,+ metaCpuModel :: Maybe String,+ metaCpuModelName :: Maybe String,+ metaCpuPhysicalId :: Maybe String,+ metaCpuStepping :: Maybe String,+ metaCpuVendorId :: Maybe String+ } deriving (Eq, Show)++instance FromJSON HostMetaCpu where+ parseJSON (Object o)+ = HostMetaCpu+ <$> o .:? "cache_size" <*> o .:? "core_id" <*> o .:? "cores" <*> o .:? "family"+ <*> (o .:? "mhz" <|> fmap show <$> (o .:? "mhz" :: Parser (Maybe Integer)))+ <*> (o .:? "model" <|> fmap show <$> (o .:? "model" :: Parser (Maybe Integer)))+ <*> o .:? "model_name" <*> o .:? "physical_id" <*> o .:? "stepping" <*> o .:? "vendor_id"+ parseJSON o = typeMismatch "HostMetaCpu" o++instance ToJSON HostMetaCpu where+ toJSON (HostMetaCpu cacheSize coreId cores family mhz model modelName physicalId stepping vendorId)+ = object [ key .= value |+ (key, value) <- [+ ("cache_size", cacheSize), ("core_id", coreId), ("cores", cores), ("family", family),+ ("mhz", mhz), ("model", model), ("model_name", modelName),+ ("physical_id", physicalId), ("stepping", stepping), ("vendor_id", vendorId)+ ],+ isJust value+ ]++data HostMeta+ = HostMeta {+ metaAgentName :: Maybe String,+ metaAgentRevision :: Maybe String,+ metaAgentVersion :: Maybe String,+ metaBlockDevice :: Maybe (HM.HashMap String (HM.HashMap String String)),+ metaCpu :: Maybe [HostMetaCpu],+ metaFilesystem :: Maybe (HM.HashMap String (HM.HashMap String Value)),+ metaKernel :: Maybe (HM.HashMap String String),+ metaMemory :: Maybe (HM.HashMap String String),+ metaCloud :: Maybe HostMetaCloud+ } deriving (Eq, Show)++instance Default HostMeta where+ def = HostMeta def def def def def def def def def++$(deriveJSON options {+ fieldLabelModifier+ = \xs -> let ys = drop 4 xs in if take 5 ys == "Agent" then kebabCase ys else snakeCase ys+} ''HostMeta)++data HostStatus = HostStatusWorking+ | HostStatusStandby+ | HostStatusMaintenance+ | HostStatusPoweroff+ deriving Eq++instance Show HostStatus where+ show HostStatusWorking = "working"+ show HostStatusStandby = "standby"+ show HostStatusMaintenance = "maintenance"+ show HostStatusPoweroff = "poweroff"++instance Read HostStatus where+ readsPrec _ xs = [ (hs, drop (length str) xs) | (hs, str) <- pairs', take (length str) xs == str ]+ where pairs' = [(HostStatusWorking, "working"), (HostStatusStandby, "standby"), (HostStatusMaintenance, "maintenance"), (HostStatusPoweroff, "poweroff")]++$(deriveJSON options { constructorTagModifier = map toLower . drop 10 } ''HostStatus)++data HostInterface+ = HostInterface {+ hostInterfaceName :: String,+ hostInterfaceMacAddress :: Maybe String,+ hostInterfaceIpv4Addresses :: Maybe [String],+ hostInterfaceIpv6Addresses :: Maybe [String]+ } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 13 } ''HostInterface)++data Host+ = Host {+ hostId :: HostId,+ hostName :: String,+ hostDisplayName :: Maybe String,+ hostStatus :: HostStatus,+ hostMemo :: String,+ hostRoles :: HM.HashMap String [String],+ hostIsRetired :: Bool,+ hostCreatedAt :: Integer,+ hostMeta :: HostMeta,+ hostInterfaces :: [HostInterface]+ } deriving (Eq, Show)++$(deriveJSON options ''Host)++data HostCreate+ = HostCreate {+ hostCreateName :: String,+ hostCreateDisplayName :: Maybe String,+ hostCreateCustomIdentifier :: Maybe String,+ hostCreateMeta :: HostMeta,+ hostCreateInterfaces :: Maybe [HostInterface],+ hostCreateRoleFullnames :: Maybe [String],+ hostCreateChecks :: Maybe [String]+ } deriving (Eq, Show)++instance Default HostCreate where+ def = HostCreate def def def def def def def++$(deriveJSON options { fieldLabelModifier = (\(c:cs) -> toLower c : cs) . drop 10 } ''HostCreate)
+ src/Web/Mackerel/Types/Monitor.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Monitor where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON, SumEncoding(..), sumEncoding, constructorTagModifier, fieldLabelModifier)+import Data.Aeson.Types (typeMismatch)+import Data.Char (toLower)+import Data.List (isSuffixOf)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data MonitorId = MonitorId String+ deriving (Eq, Show)++instance FromJSON MonitorId where+ parseJSON (Aeson.String monitorId') = return $ MonitorId $ Text.unpack monitorId'+ parseJSON o = typeMismatch "MonitorId" o++instance ToJSON MonitorId where+ toJSON (MonitorId monitorId') = toJSON monitorId'++data MonitorOperator = MonitorGreaterThan | MonitorLessThan+ deriving Eq++instance Show MonitorOperator where+ show MonitorGreaterThan = ">"+ show MonitorLessThan = "<"++instance Read MonitorOperator where+ readsPrec _ xs = [ (op, drop 1 xs) | (op, opstr) <- [(MonitorGreaterThan, ">"), (MonitorLessThan, "<")], take 1 xs == opstr ]++instance FromJSON MonitorOperator where+ parseJSON (Aeson.String str)+ | Text.unpack str == ">" = return MonitorGreaterThan+ | Text.unpack str == "<" = return MonitorLessThan+ parseJSON o = typeMismatch "MonitorOperator" o++instance ToJSON MonitorOperator where+ toJSON op = toJSON (show op)++data MonitorExternalHeader+ = MonitorExternalHeader {+ monitorHeaderName :: String,+ monitorHeaderValue :: String+ } deriving (Eq, Show)++$(deriveJSON options { fieldLabelModifier = map toLower . drop 13 } ''MonitorExternalHeader)++data MonitorType = MonitorTypeConnectivity+ | MonitorTypeHost+ | MonitorTypeService+ | MonitorTypeExternal+ | MonitorTypeCheck+ | MonitorTypeExpression+ deriving (Eq, Show)++$(deriveJSON options { constructorTagModifier = map toLower . drop 11 } ''MonitorType)++data Monitor+ = MonitorHost {+ monitorId :: Maybe MonitorId,+ monitorName :: String,+ monitorDuration :: Integer,+ monitorMetric :: String,+ monitorOperator :: MonitorOperator,+ monitorWarning :: Double,+ monitorCritical :: Double,+ monitorIsMute :: Maybe Bool,+ monitorNotificationInterval :: Maybe Integer,+ monitorScopes :: Maybe [String],+ monitorExcludeScopes :: Maybe [String]+ }+ | MonitorConnectivity {+ monitorId :: Maybe MonitorId,+ monitorName :: String,+ monitorIsMute :: Maybe Bool,+ monitorNotificationInterval :: Maybe Integer,+ monitorScopes :: Maybe [String],+ monitorExcludeScopes :: Maybe [String]+ }+ | MonitorService {+ monitorId :: Maybe MonitorId,+ monitorName :: String,+ monitorService :: String,+ monitorDuration :: Integer,+ monitorMetric :: String,+ monitorOperator :: MonitorOperator,+ monitorWarning :: Double,+ monitorCritical :: Double,+ monitorIsMute :: Maybe Bool,+ monitorNotificationInterval :: Maybe Integer+ }+ | MonitorExternal {+ monitorId :: Maybe MonitorId,+ monitorName :: String,+ monitorUrl :: String,+ monitorServiceOption :: Maybe String,+ monitorResponseTimeDuration :: Maybe Double,+ monitorResponseTimeWarning :: Maybe Double,+ monitorResponseTimeCritical :: Maybe Double,+ monitorContainsString :: Maybe String,+ monitorMaxCheckAttempts :: Maybe Integer,+ monitorCertificationExpirationWarning :: Maybe Integer,+ monitorCertificationExpirationCritical :: Maybe Integer,+ monitorSkipCertificateVerification :: Maybe Bool,+ monitorHeaders :: Maybe [MonitorExternalHeader],+ monitorIsMute :: Maybe Bool,+ monitorNotificationInterval :: Maybe Integer+ }+ | MonitorExpression {+ monitorId :: Maybe MonitorId,+ monitorName :: String,+ monitorExpression :: String,+ monitorOperator :: MonitorOperator,+ monitorWarning :: Double,+ monitorCritical :: Double,+ monitorIsMute :: Maybe Bool,+ monitorNotificationInterval :: Maybe Integer+ } deriving (Eq, Show)++$(deriveJSON options {+ sumEncoding = TaggedObject "type" "type",+ constructorTagModifier = map toLower . drop 7,+ fieldLabelModifier = \xs ->+ let ys = fieldLabelModifier options xs+ in if "Option" `isSuffixOf` ys then take (length ys - 6) ys else ys+} ''Monitor)
+ src/Web/Mackerel/Types/Organization.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Organization where++import Data.Aeson.TH (deriveJSON)++import Web.Mackerel.Internal.TH++data Organization = Organization { organizationName :: String }+ deriving (Eq, Show)++$(deriveJSON options ''Organization)
+ src/Web/Mackerel/Types/Role.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Role where++import Data.Aeson.TH (deriveJSON)++import Web.Mackerel.Internal.TH++data Role+ = Role {+ roleName :: String,+ roleMemo :: String+ } deriving (Eq, Show)++$(deriveJSON options ''Role)
+ src/Web/Mackerel/Types/Service.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.Service where++import Data.Aeson.TH (deriveJSON)++import Web.Mackerel.Internal.TH++data Service+ = Service {+ serviceName :: String,+ serviceMemo :: String,+ serviceRoles :: [String]+ } deriving (Eq, Show)++$(deriveJSON options ''Service)
+ src/Web/Mackerel/Types/User.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE TemplateHaskell #-}+module Web.Mackerel.Types.User where++import Data.Aeson+import qualified Data.Aeson as Aeson+import Data.Aeson.TH (deriveJSON)+import Data.Aeson.Types (typeMismatch)+import qualified Data.Text as Text++import Web.Mackerel.Internal.TH++data UserId = UserId String+ deriving (Eq, Show)++instance FromJSON UserId where+ parseJSON (Aeson.String alertId) = return $ UserId $ Text.unpack alertId+ parseJSON o = typeMismatch "UserId" o++instance ToJSON UserId where+ toJSON (UserId alertId) = toJSON alertId++data User+ = User {+ userId :: UserId,+ userScreenName :: String,+ userEmail :: String+ } deriving (Eq, Show)++$(deriveJSON options ''User)
+ test/ConfigSpec.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module ConfigSpec where++import Control.Monad (forM_)+import Data.Default+import Test.Hspec+import Text.RawString.QQ++import Web.Mackerel.Config+import Web.Mackerel.Types.Host++spec :: Spec+spec = do++ let config1 = def {+ configApiKey = Just "<Mackerel-API-KEY>"+ }++ let toml1 = [r|+ apikey = "<Mackerel-API-KEY>"+ |]++ let config2 = Config {+ configApiBase = Just "https://mackerel.io",+ configApiKey = Just "<Mackerel-API-KEY>",+ configRoot = Just "/var/lib/mackerel-agent",+ configPidfile = Just "/var/run/mackerel-agent.pid",+ configRoles = Just ["service1:role1", "service2:role3"],+ configVerbose = Just True,+ configDiagnostic = Just False,+ configDisplayname = Just "Example Host",+ configHostStatus = Just HostStatusConfig {+ hostStatusOnStart = Just HostStatusWorking,+ hostStatusOnStop = Just HostStatusPoweroff+ },+ configHttpProxy = Just "http://localhost:8080"+ }++ let toml2 = [r|+ apibase = "https://mackerel.io"+ apikey = "<Mackerel-API-KEY>"+ root = "/var/lib/mackerel-agent"+ pidfile = "/var/run/mackerel-agent.pid"++ verbose = true+ diagnostic = false++ display_name = "Example Host"+ http_proxy = "http://localhost:8080"++ roles = [ "service1:role1", "service2:role3" ]++ include = "/etc/mackerel-agent/conf.d/*.conf"++ [host_status]+ on_start = "working"+ on_stop = "poweroff"+ |]++ describe "parseConfig" $+ it "should parse a configuration toml" $+ forM_ [(toml1, config1), (toml2, config2)] $ \(toml, config) ->+ parseConfig toml `shouldBe` Right config
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/Types/AlertSpec.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.AlertSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Alert+import Web.Mackerel.Types.Host+import Web.Mackerel.Types.Monitor++spec :: Spec+spec = do++ let alert1 = Alert {+ alertId = AlertId "abcde0",+ alertStatus = AlertStatusCritical,+ alertMonitorId = Just $ MonitorId "abcde2",+ alertType = MonitorTypeConnectivity,+ alertHostId = Just $ HostId "abcde1",+ alertValue = Nothing,+ alertMessage = Nothing,+ alertReason = Nothing,+ alertOpenedAt = 1483196400,+ alertClosedAt = Nothing+ }++ let json1 = [aesonQQ|+ {+ "id": "abcde0",+ "status": "CRITICAL",+ "monitorId": "abcde2",+ "type": "connectivity",+ "hostId": "abcde1",+ "openedAt": 1483196400+ }+ |]++ let alert2 = Alert {+ alertId = AlertId "abcde0",+ alertStatus = AlertStatusWarning,+ alertMonitorId = Just $ MonitorId "abcde2",+ alertType = MonitorTypeHost,+ alertHostId = Just $ HostId "abcde1",+ alertValue = Just 25.0,+ alertMessage = Nothing,+ alertReason = Nothing,+ alertOpenedAt = 1483196400,+ alertClosedAt = Nothing+ }++ let json2 = [aesonQQ|+ {+ "id": "abcde0",+ "status": "WARNING",+ "monitorId": "abcde2",+ "type": "host",+ "hostId": "abcde1",+ "value": 25.0,+ "openedAt": 1483196400+ }+ |]++ let alert3 = Alert {+ alertId = AlertId "abcde0",+ alertStatus = AlertStatusCritical,+ alertMonitorId = Just $ MonitorId "abcde2",+ alertType = MonitorTypeExternal,+ alertHostId = Nothing,+ alertValue = Just 121,+ alertMessage = Just "SSL certification will expire within 696 days (2018-11-28T21:00:00+0900)",+ alertReason = Just "Close this alert",+ alertOpenedAt = 1483196400,+ alertClosedAt = Just 1483282800+ }++ let json3 = [aesonQQ|+ {+ "id": "abcde0",+ "status": "CRITICAL",+ "monitorId": "abcde2",+ "type": "external",+ "value": 121,+ "message": "SSL certification will expire within 696 days (2018-11-28T21:00:00+0900)",+ "reason": "Close this alert",+ "openedAt": 1483196400,+ "closedAt": 1483282800+ }+ |]++ describe "Alert FromJSON" $ do+ it "should parse a json" $+ forM_ [(alert1, json1), (alert2, json2), (alert3, json3)] $ \(alert, json) ->+ decode (encode json) `shouldBe` Just alert++ it "should reject an invalid json" $ do+ decode "{}" `shouldBe` (Nothing :: Maybe Alert)+ let (Object hm) = json1+ forM_ ["id", "status", "type", "openedAt"] $ \key ->+ decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Alert)++ describe "Alert ToJSON" $+ it "should encode into a json" $+ forM_ [(alert1, json1), (alert2, json2), (alert3, json3)] $ \(alert, json) ->+ decode (encode alert) `shouldBe` Just json
+ test/Types/DashboardSpec.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.DashboardSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Dashboard++spec :: Spec+spec = do++ let dashboard = Dashboard {+ dashboardId = DashboardId "abcde",+ dashboardTitle = "This is a dashboard",+ dashboardBodyMarkdown = "# Example\n[example](https://example.com)",+ dashboardUrlPath = "example",+ dashboardCreatedAt = 1483196400,+ dashboardUpdatedAt = 1483282800+ }++ let json = [aesonQQ|+ {+ "id": "abcde",+ "title": "This is a dashboard",+ "bodyMarkdown": "# Example\n[example](https://example.com)",+ "urlPath": "example",+ "createdAt": 1483196400,+ "updatedAt": 1483282800+ }+ |]++ let dashboardCreate = DashboardCreate {+ dashboardCreateTitle = "This is a dashboard",+ dashboardCreateBodyMarkdown = "# Example\n[example](https://example.com)",+ dashboardCreateUrlPath = "example"+ }++ let jsonCreate = [aesonQQ|+ {+ "title": "This is a dashboard",+ "bodyMarkdown": "# Example\n[example](https://example.com)",+ "urlPath": "example"+ }+ |]++ describe "Dashboard FromJSON" $ do+ it "should parse a json" $+ decode (encode json) `shouldBe` Just dashboard++ it "should reject an invalid json" $ do+ decode "{}" `shouldBe` (Nothing :: Maybe Dashboard)+ let (Object hm) = json+ forM_ ["id", "title", "bodyMarkdown", "urlPath", "createdAt", "updatedAt"] $ \key ->+ decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Dashboard)++ describe "Dashboard ToJSON" $+ it "should encode into a json" $+ decode (encode dashboard) `shouldBe` Just json++ describe "DashboardCreate FromJSON" $+ it "should parse a json" $+ decode (encode jsonCreate) `shouldBe` Just dashboardCreate++ describe "DashboardCreate ToJSON" $+ it "should encode into a json" $+ decode (encode dashboardCreate) `shouldBe` Just jsonCreate
+ test/Types/HostSpec.hs view
@@ -0,0 +1,314 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.HostSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import Data.Default+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Host++spec :: Spec+spec = do++ let host1 = Host {+ hostId = HostId "abcede",+ hostName = "foohost.example",+ hostDisplayName = Nothing,+ hostStatus = HostStatusWorking,+ hostMemo = "",+ hostRoles = HM.fromList [],+ hostIsRetired = False,+ hostCreatedAt = 1483196400,+ hostMeta = def,+ hostInterfaces = []+ }++ let json1 = [aesonQQ|+ {+ "id": "abcede",+ "name": "foohost.example",+ "status": "working",+ "memo": "",+ "roles": {},+ "isRetired": false,+ "createdAt": 1483196400,+ "meta": {},+ "interfaces": []+ }+ |]++ let host2 = Host {+ hostId = HostId "abcede",+ hostName = "foohost.example",+ hostDisplayName = Just "Host Foo",+ hostStatus = HostStatusWorking,+ hostMemo = "host memo",+ hostRoles = HM.fromList [("foo", ["bar", "baz"]), ("qux", ["quux"])],+ hostIsRetired = False,+ hostCreatedAt = 1483196400,+ hostMeta = def {+ metaAgentName = Just "mackerel-agent/0.37.0 (Revision 3c11c2c)",+ metaAgentRevision = Just "3c11c2c",+ metaAgentVersion = Just "0.37.0",+ metaBlockDevice = Just $ HM.fromList [+ ("xvda", HM.fromList [+ ("removable", "0"),+ ("size", "251658240")+ ])+ ],+ metaCpu = Just [+ HostMetaCpu {+ metaCpuCacheSize = Just "25600 KB",+ metaCpuCoreId = Just "0",+ metaCpuCores = Just "1",+ metaCpuFamily = Just "6",+ metaCpuMhz = Just "2500.064",+ metaCpuModel = Just "62",+ metaCpuModelName = Just "Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz",+ metaCpuPhysicalId = Just "0",+ metaCpuStepping = Just "4",+ metaCpuVendorId = Just "GenuineIntel"+ }+ ],+ metaFilesystem = Just $ HM.fromList [+ ("/dev/xvda", HM.fromList [+ ("kb_available", Number $ read "2526784"),+ ("kb_size", Number $ read "8125880"),+ ("kb_used", Number $ read "5163284"),+ ("mount", "/"),+ ("percent_used", "68%")+ ])+ ],+ metaKernel = Just $ HM.fromList [+ ("machine", "x86_64"),+ ("name", "Linux"),+ ("os", "GNU/Linux"),+ ("platform_name", "Debian"),+ ("platform_version", "8.3"),+ ("release", "3.16.0-4-amd64"),+ ("version", "#1 SMP Debian 3.16.7-ckt20-1+deb8u3 (2016-01-17)")+ ],+ metaMemory = Just $ HM.fromList [+ ("active", "77456kB"),+ ("buffers", "11720kB"),+ ("cached", "83456kB"),+ ("free", "3685992kB"),+ ("inactive", "59368kB"),+ ("total", "3865572kB")+ ],+ metaCloud = Just HostMetaCloud {+ metaCloudProvider = "ec2",+ metaCloudMetadata = HM.fromList [+ ("ami-id", "ami-000"),+ ("hostname", "ip-0-0-0-0"),+ ("instance-id", "i-00000"),+ ("instance-type", "m3.medium")+ ]+ }+ },+ hostInterfaces = [+ HostInterface {+ hostInterfaceName = "en0",+ hostInterfaceMacAddress = Just "ff:ff:ff:ff:ff:ff",+ hostInterfaceIpv4Addresses = Just ["192.168.100.2"],+ hostInterfaceIpv6Addresses = Just ["2001:268:c00f:8a08::"]+ }+ ]+ }++ let json2 = [aesonQQ|+ {+ "id": "abcede",+ "name": "foohost.example",+ "displayName": "Host Foo",+ "status": "working",+ "memo": "host memo",+ "roles": {+ "foo": ["bar", "baz"],+ "qux": ["quux"]+ },+ "isRetired": false,+ "createdAt": 1483196400,+ "meta": {+ "agent-name": "mackerel-agent/0.37.0 (Revision 3c11c2c)",+ "agent-revision": "3c11c2c",+ "agent-version": "0.37.0",+ "block_device": {+ "xvda": {+ "removable": "0",+ "size": "251658240"+ }+ },+ "cpu": [+ {+ "cache_size": "25600 KB",+ "core_id": "0",+ "cores": "1",+ "family": "6",+ "mhz": "2500.064",+ "model": "62",+ "model_name": "Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz",+ "physical_id": "0",+ "stepping": "4",+ "vendor_id": "GenuineIntel"+ }+ ],+ "filesystem": {+ "/dev/xvda": {+ "kb_available": 2526784,+ "kb_size": 8125880,+ "kb_used": 5163284,+ "mount": "/",+ "percent_used": "68%"+ }+ },+ "kernel": {+ "machine": "x86_64",+ "name": "Linux",+ "os": "GNU/Linux",+ "platform_name": "Debian",+ "platform_version": "8.3",+ "release": "3.16.0-4-amd64",+ "version": "#1 SMP Debian 3.16.7-ckt20-1+deb8u3 (2016-01-17)"+ },+ "memory": {+ "active": "77456kB",+ "buffers": "11720kB",+ "cached": "83456kB",+ "free": "3685992kB",+ "inactive": "59368kB",+ "total": "3865572kB"+ },+ "cloud": {+ "provider": "ec2",+ "metadata": {+ "ami-id": "ami-000",+ "hostname": "ip-0-0-0-0",+ "instance-id": "i-00000",+ "instance-type": "m3.medium"+ }+ }+ },+ "interfaces": [+ {+ "name": "en0",+ "macAddress": "ff:ff:ff:ff:ff:ff",+ "ipv4Addresses": ["192.168.100.2"],+ "ipv6Addresses": ["2001:268:c00f:8a08::"]+ }+ ]+ }+ |]++ describe "Host FromJSON" $ do+ it "should parse a json" $+ forM_ [(json1, host1), (json2, host2)] $ \(json, host) ->+ decode (encode json) `shouldBe` Just host++ it "should reject an invalid json" $ do+ decode "{}" `shouldBe` (Nothing :: Maybe Host)+ let (Object hm) = json1+ forM_ ["id", "name", "status", "memo", "roles", "isRetired", "createdAt", "meta", "interfaces"] $ \key ->+ decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Host)+ forM_ ["status", "roles", "isRetired", "createdAt", "meta", "interfaces"] $ \key ->+ decode (encode (Object (HM.insert key "invalid" hm))) `shouldBe` (Nothing :: Maybe Host)++ describe "Host ToJSON" $+ it "should encode into a json" $+ forM_ [(json1, host1), (json2, host2)] $ \(json, host) ->+ decode (encode host) `shouldBe` Just json++ describe "HostStatus Show" $+ it "should show properly" $ do+ show HostStatusWorking `shouldBe` "working"+ show HostStatusMaintenance `shouldBe` "maintenance"+ show [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff] `shouldBe` "[working,standby,maintenance,poweroff]"++ describe "HostStatus Read" $+ it "should read properly" $ do+ read "working" `shouldBe` HostStatusWorking+ read "maintenance" `shouldBe` HostStatusMaintenance+ read "[working,standby,maintenance,poweroff]" `shouldBe` [HostStatusWorking, HostStatusStandby, HostStatusMaintenance, HostStatusPoweroff]++ let hostCreate1 = HostCreate {+ hostCreateName = "foohost.example",+ hostCreateDisplayName = Nothing,+ hostCreateCustomIdentifier = Nothing,+ hostCreateMeta = def,+ hostCreateInterfaces = Nothing,+ hostCreateRoleFullnames = Nothing,+ hostCreateChecks = Nothing+ }++ let jsonCreate1 = [aesonQQ|+ {+ "name": "foohost.example",+ "meta": {}+ }+ |]++ let hostCreate2 = HostCreate {+ hostCreateName = "foohost.example",+ hostCreateDisplayName = Just "Foo host",+ hostCreateCustomIdentifier = Just "foo-host-identifier",+ hostCreateMeta = def {+ metaAgentName = Just "mackerel-agent/0.37.0 (Revision 3c11c2c)",+ metaAgentRevision = Just "3c11c2c",+ metaAgentVersion = Just "0.37.0"+ },+ hostCreateInterfaces = Just [+ HostInterface {+ hostInterfaceName = "en0",+ hostInterfaceMacAddress = Just "ff:ff:ff:ff:ff:ff",+ hostInterfaceIpv4Addresses = Just ["192.168.100.2"],+ hostInterfaceIpv6Addresses = Just ["2001:268:c00f:8a08::"]+ }+ ],+ hostCreateRoleFullnames = Just ["foo:bar", "foo:baz"],+ hostCreateChecks = Just [+ "plugin.checks.access_log",+ "plugin.checks.check_cron"+ ]+ }++ let jsonCreate2 = [aesonQQ|+ {+ "name": "foohost.example",+ "displayName": "Foo host",+ "customIdentifier": "foo-host-identifier",+ "meta": {+ "agent-name": "mackerel-agent/0.37.0 (Revision 3c11c2c)",+ "agent-revision": "3c11c2c",+ "agent-version": "0.37.0"+ },+ "interfaces": [+ {+ "name": "en0",+ "macAddress": "ff:ff:ff:ff:ff:ff",+ "ipv4Addresses": ["192.168.100.2"],+ "ipv6Addresses": ["2001:268:c00f:8a08::"]+ }+ ],+ "roleFullnames": [+ "foo:bar",+ "foo:baz"+ ],+ "checks": [+ "plugin.checks.access_log",+ "plugin.checks.check_cron"+ ]+ }+ |]++ describe "HostCreate FromJSON" $+ it "should parse a json" $+ forM_ [(jsonCreate1, hostCreate1), (jsonCreate2, hostCreate2)] $ \(json, host) ->+ decode (encode json) `shouldBe` Just host++ describe "HostCreate ToJSON" $+ it "should encode into a json" $+ forM_ [(jsonCreate1, hostCreate1), (jsonCreate2, hostCreate2)] $ \(json, host) ->+ decode (encode host) `shouldBe` Just json
+ test/Types/MonitorSpec.hs view
@@ -0,0 +1,173 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.MonitorSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode)+import Data.Aeson.QQ+import Test.Hspec++import Web.Mackerel.Types.Monitor++spec :: Spec+spec = do++ let hostMonitor = MonitorHost {+ monitorId = Just $ MonitorId "abcde1",+ monitorName = "Monitor custom.foo.bar",+ monitorDuration = 5,+ monitorMetric = "custom.foo.bar",+ monitorOperator = MonitorGreaterThan,+ monitorWarning = 10.0,+ monitorCritical = 20.0,+ monitorIsMute = Just False,+ monitorNotificationInterval = Just 30,+ monitorScopes = Just ["service0"],+ monitorExcludeScopes = Just ["service0:role3"]+ }++ let hostMonitorJson = [aesonQQ|+ {+ "type": "host",+ "id": "abcde1",+ "name": "Monitor custom.foo.bar",+ "duration": 5,+ "metric": "custom.foo.bar",+ "operator": ">",+ "warning": 10.0,+ "critical": 20.0,+ "isMute": false,+ "notificationInterval": 30,+ "scopes": ["service0"],+ "excludeScopes": ["service0:role3"]+ }+ |]++ let connectivity = MonitorConnectivity {+ monitorId = Just $ MonitorId "abcde2",+ monitorName = "connectivity",+ monitorIsMute = Just False,+ monitorNotificationInterval = Nothing,+ monitorScopes = Nothing,+ monitorExcludeScopes = Nothing+ }++ let connectivityJson = [aesonQQ|+ {+ "type": "connectivity",+ "id": "abcde2",+ "name": "connectivity",+ "isMute": false+ }+ |]++ let serviceMonitor = MonitorService {+ monitorId = Just $ MonitorId "abcde3",+ monitorName = "Service count",+ monitorService = "service1",+ monitorDuration = 5,+ monitorMetric = "custom.service.count",+ monitorOperator = MonitorGreaterThan,+ monitorWarning = 100,+ monitorCritical = 200,+ monitorIsMute = Just False,+ monitorNotificationInterval = Just 30+ }++ let serviceMonitorJson = [aesonQQ|+ {+ "type": "service",+ "id": "abcde3",+ "name": "Service count",+ "service": "service1",+ "duration": 5,+ "metric": "custom.service.count",+ "operator": ">",+ "warning": 100,+ "critical": 200,+ "isMute": false,+ "notificationInterval": 30+ }+ |]++ let externalMonitor = MonitorExternal {+ monitorId = Just $ MonitorId "abcde4",+ monitorName = "Example external monitor",+ monitorUrl = "https://example.com",+ monitorServiceOption = Just "service1",+ monitorResponseTimeDuration = Just 5,+ monitorResponseTimeWarning = Just 3000,+ monitorResponseTimeCritical = Just 5000,+ monitorContainsString = Just "Example Domain",+ monitorMaxCheckAttempts = Just 5,+ monitorCertificationExpirationWarning = Just 1200,+ monitorCertificationExpirationCritical = Just 60,+ monitorSkipCertificateVerification = Just True,+ monitorHeaders = Just [MonitorExternalHeader "Cache-Control" "no-cache"],+ monitorIsMute = Just True,+ monitorNotificationInterval = Just 60+ }++ let externalMonitorJson = [aesonQQ|+ {+ "type": "external",+ "id": "abcde4",+ "name": "Example external monitor",+ "url": "https://example.com",+ "service": "service1",+ "responseTimeDuration": 5,+ "responseTimeWarning": 3000,+ "responseTimeCritical": 5000,+ "containsString": "Example Domain",+ "maxCheckAttempts": 5,+ "certificationExpirationWarning": 1200,+ "certificationExpirationCritical": 60,+ "skipCertificateVerification": true,+ "headers": [{ "name": "Cache-Control", "value": "no-cache" }],+ "isMute": true,+ "notificationInterval": 60+ }+ |]++ let expressionMonitor = MonitorExpression {+ monitorId = Just $ MonitorId "abcde5",+ monitorName = "Example expression monitor",+ monitorExpression = "min(role(\"foo:bar\", \"custom.foo.bar\"))",+ monitorOperator = MonitorLessThan,+ monitorWarning = 10.0,+ monitorCritical = 20.0,+ monitorIsMute = Just False,+ monitorNotificationInterval = Nothing+ }++ let expressionMonitorJson = [aesonQQ|+ {+ "type": "expression",+ "id": "abcde5",+ "name": "Example expression monitor",+ "expression": "min(role(\"foo:bar\", \"custom.foo.bar\"))",+ "operator": "<",+ "warning": 10.0,+ "critical": 20.0,+ "isMute": false+ }+ |]++ let monitors = [+ (hostMonitor, hostMonitorJson),+ (connectivity, connectivityJson),+ (serviceMonitor, serviceMonitorJson),+ (externalMonitor, externalMonitorJson),+ (expressionMonitor, expressionMonitorJson)+ ]++ describe "Monitor FromJSON" $+ it "should parse jsons" $ do+ forM_ monitors $ \(monitor, json) ->+ decode (encode json) `shouldBe` Just monitor+ decode (encode (map snd monitors)) `shouldBe` Just (map fst monitors)++ describe "Monitor ToJSON" $+ it "should encode into jsons" $ do+ forM_ monitors $ \(monitor, json) ->+ decode (encode monitor) `shouldBe` Just json+ decode (encode (map fst monitors)) `shouldBe` Just (map snd monitors)
+ test/Types/OrganizationSpec.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.OrganizationSpec where++import Data.Aeson (decode, encode)+import Data.Aeson.QQ+import Test.Hspec++import Web.Mackerel.Types.Organization++spec :: Spec+spec = do++ let organization = Organization {+ organizationName = "FooOrganization"+ }++ let json = [aesonQQ|+ {+ "name": "FooOrganization"+ }+ |]++ describe "Organization FromJSON" $ do+ it "should parse a json" $+ decode (encode json) `shouldBe` Just organization++ it "should reject an invalid json" $+ decode "{}" `shouldBe` (Nothing :: Maybe Organization)++ describe "Organization ToJSON" $+ it "should encode into a json" $+ decode (encode organization) `shouldBe` Just json
+ test/Types/RoleSpec.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.RoleSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Role++spec :: Spec+spec = do++ let role' = Role {+ roleName = "FooRole",+ roleMemo = "Role memo"+ }++ let json = [aesonQQ|+ {+ "name": "FooRole",+ "memo": "Role memo"+ }+ |]++ describe "Role FromJSON" $ do+ it "should parse a json" $+ decode (encode json) `shouldBe` Just role'++ it "should reject an invalid json" $ do+ decode "{}" `shouldBe` (Nothing :: Maybe Role)+ let (Object hm) = json+ forM_ ["name", "memo"] $ \key ->+ decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Role)++ describe "Role ToJSON" $+ it "should encode into a json" $+ decode (encode role') `shouldBe` Just json
+ test/Types/ServiceSpec.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.ServiceSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.Service++spec :: Spec+spec = do++ let service = Service {+ serviceName = "FooService",+ serviceMemo = "service memo",+ serviceRoles = [ "role0", "role1", "role2" ]+ }++ let json = [aesonQQ|+ {+ "name": "FooService",+ "memo": "service memo",+ "roles": [+ "role0",+ "role1",+ "role2"+ ]+ }+ |]++ describe "Service FromJSON" $ do+ it "should parse a json" $+ decode (encode json) `shouldBe` Just service++ it "should reject an invalid json" $ do+ decode "{}" `shouldBe` (Nothing :: Maybe Service)+ let (Object hm) = json+ forM_ ["name", "memo", "roles"] $ \key ->+ decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe Service)++ describe "Service ToJSON" $+ it "should encode into a json" $+ decode (encode service) `shouldBe` Just json
+ test/Types/UserSpec.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+module Types.UserSpec where++import Control.Monad (forM_)+import Data.Aeson (decode, encode, Value(..))+import Data.Aeson.QQ+import qualified Data.HashMap.Lazy as HM+import Test.Hspec++import Web.Mackerel.Types.User++spec :: Spec+spec = do++ let user = User {+ userId = UserId "abcde",+ userScreenName = "Example Mackerel",+ userEmail = "mackerel@example.com"+ }++ let json = [aesonQQ|+ {+ "id": "abcde",+ "screenName": "Example Mackerel",+ "email": "mackerel@example.com"+ }+ |]++ describe "User FromJSON" $ do+ it "should parse a json" $+ decode (encode json) `shouldBe` Just user++ it "should reject an invalid json" $ do+ decode "{}" `shouldBe` (Nothing :: Maybe User)+ let (Object hm) = json+ forM_ ["id", "screenName", "email"] $ \key ->+ decode (encode (Object (HM.delete key hm))) `shouldBe` (Nothing :: Maybe User)++ describe "User ToJSON" $+ it "should encode into a json" $+ decode (encode user) `shouldBe` Just json