-
Notifications
You must be signed in to change notification settings - Fork 66
Expand file tree
/
Copy pathSnap.hs
More file actions
149 lines (136 loc) · 5.52 KB
/
Snap.hs
File metadata and controls
149 lines (136 loc) · 5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
{-# LANGUAGE OverloadedStrings #-}
module System.Remote.Snap
( startServer
) where
import Control.Applicative ((<$>), (<|>))
import Control.Exception (throwIO)
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import Data.Function (on)
import qualified Data.HashMap.Strict as M
import qualified Data.List as List
import qualified Data.Map.Lazy as LMap
import qualified Data.Text.Encoding as T
import Data.Word (Word8)
import Network.Socket (NameInfoFlag(NI_NUMERICHOST), addrAddress, getAddrInfo,
getNameInfo)
import Paths_ekg (getDataDir)
import Prelude hiding (read)
import Snap.Core (MonadSnap, Request, Snap, finishWith, getHeader, getQueryParams,
getRequest, getResponse, method, Method(GET), modifyResponse,
pass, rqPathInfo, setContentType, setResponseStatus, writeLBS)
import Snap.Http.Server (httpServe)
import qualified Snap.Http.Server.Config as Config
import Snap.Util.FileServe (serveDirectory)
import System.FilePath ((</>))
import System.Metrics
import System.Remote.Json
------------------------------------------------------------------------
-- | Convert a host name (e.g. \"localhost\" or \"127.0.0.1\") to a
-- numeric host address (e.g. \"127.0.0.1\").
getNumericHostAddress :: S.ByteString -> IO S.ByteString
getNumericHostAddress host = do
ais <- getAddrInfo Nothing (Just (S8.unpack host)) Nothing
case ais of
[] -> unsupportedAddressError
(ai:_) -> do
ni <- getNameInfo [NI_NUMERICHOST] True False (addrAddress ai)
case ni of
(Just numericHost, _) -> return $! S8.pack numericHost
_ -> unsupportedAddressError
where
unsupportedAddressError = throwIO $
userError $ "unsupported address: " ++ S8.unpack host
startServer :: Store
-> S.ByteString -- ^ Host to listen on (e.g. \"localhost\")
-> Int -- ^ Port to listen on (e.g. 8000)
-> IO ()
startServer store host port = do
-- Snap doesn't allow for non-numeric host names in
-- 'Snap.setBind'. We work around that limitation by converting a
-- possible non-numeric host name to a numeric address.
numericHost <- getNumericHostAddress host
let conf = Config.setVerbose False $
Config.setErrorLog Config.ConfigNoLog $
Config.setAccessLog Config.ConfigNoLog $
Config.setPort port $
Config.setHostname host $
Config.setBind numericHost $
Config.defaultConfig
httpServe conf (monitor store)
-- | A handler that can be installed into an existing Snap application.
monitor :: Store -> Snap ()
monitor store = do
dataDir <- liftIO getDataDir
(jsonHandler $ serve store)
<|> serveDirectory (dataDir </> "assets")
where
jsonHandler = wrapHandler "application/json"
wrapHandler fmt handler = method GET $ format fmt $ handler
-- | The Accept header of the request.
acceptHeader :: Request -> Maybe S.ByteString
acceptHeader req = getHeader "Accept" req
-- | Runs a Snap monad action only if the request's Accept header
-- matches the given MIME type.
format :: MonadSnap m => S.ByteString -> m a -> m a
format fmt action = do
req <- getRequest
let acceptHdr = (List.head . parseHttpAccept) <$> acceptHeader req
case acceptHdr of
Just hdr | hdr == fmt -> action
_ -> pass
-- | Serve all counter, gauges and labels, built-in or not, as a
-- nested JSON object.
serve :: MonadSnap m => Store -> m ()
serve store = do
req <- getRequest
modifyResponse $ setContentType "application/json"
if S.null (rqPathInfo req)
then serveAll
else serveOne (rqPathInfo req)
where
serveAll = do
metrics <- liftIO $ sampleAll store
writeLBS $ encodeAll metrics
-- consider deprecating this feature?
serveOne pathInfo = do
params <- getQueryParams
let segments = S8.split '/' pathInfo
nameBytes = S8.intercalate "." segments
dims = maybe [] id (LMap.lookup "d" params)
decoded = (,) <$> (T.decodeUtf8' nameBytes)
<*> (traverse T.decodeUtf8' dims)
case decoded of
Left _ -> do
modifyResponse $ setResponseStatus 400 "Bad Request"
r <- getResponse
finishWith r
Right (name, dimensions) -> do
metrics <- liftIO $ sampleAll store
case M.lookup (dimensional name dimensions) metrics of
Nothing -> pass
Just metric -> writeLBS $ encodeOne metric
------------------------------------------------------------------------
-- Utilities for working with accept headers
-- | Parse the HTTP accept string to determine supported content types.
parseHttpAccept :: S.ByteString -> [S.ByteString]
parseHttpAccept = List.map fst
. List.sortBy (rcompare `on` snd)
. List.map grabQ
. S.split 44 -- comma
where
rcompare :: Double -> Double -> Ordering
rcompare = flip compare
grabQ s =
let (s', q) = breakDiscard 59 s -- semicolon
(_, q') = breakDiscard 61 q -- equals sign
in (trimWhite s', readQ $ trimWhite q')
readQ s = case reads $ S8.unpack s of
(x, _):_ -> x
_ -> 1.0
trimWhite = S.dropWhile (== 32) -- space
breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString)
breakDiscard w s =
let (x, y) = S.break (== w) s
in (x, S.drop 1 y)