statsdi-0.1.0.0: src/Control/Monad/Stats/TH.hs
module Control.Monad.Stats.TH
( defineCounter
, defineGauge
, defineTimer
, defineHistogram
, defineSet
, defineServiceCheck
, __mkByteString
) where
import Control.Monad
import qualified Data.ByteString.Char8 as Char8
import Data.Char
import Data.List
import Text.ParserCombinators.ReadP
import qualified Language.Haskell.TH as TH
--
-- defineCounter "vm.txs_processed" [("vm_type", "evm")]
-- # quasiquotes out to
-- # vm_txs_processed = Counter { counterName = "vm.txs_processed", counterTags = [("vm_type", "evm")] }
-- IMPORTANT: lets the quasiquoters run. pls keep
__mkByteString :: String -> Char8.ByteString
__mkByteString = Char8.pack
defField :: String -> [TH.FieldExp] -> String -> [(String, String)] -> TH.DecsQ
defField typeName moreFields metricName metricTags = do
unless (isValidMetricName metricName) . fail $ metricName ++ " is not a valid name for a " ++ typeName
validatedTags <- sequence $ transformTag <$> metricTags
let type' = TH.mkName typeName
nameFieldName = TH.mkName $ snek typeName ++ "Name"
nameFieldExp = (nameFieldName, bsp metricName)
tagsFieldName = TH.mkName $ snek typeName ++ "Tags"
tagsFieldExp = (tagsFieldName, TH.ListE (tagE <$> validatedTags))
allFieldExps = nameFieldExp : tagsFieldExp : moreFields
varName = mkMetricName metricName
varP = TH.VarP varName
sig = TH.SigD varName (TH.ConT type')
val = TH.ValD varP (TH.NormalB (TH.RecConE type' allFieldExps)) []
char8Pack = TH.mkName "__mkByteString"
bsp = TH.AppE (TH.VarE char8Pack) . TH.LitE . TH.StringL
tagE (metName, metVal) = TH.TupE [bsp metName, bsp metVal]
return [sig, val]
defineCounter :: String -> [(String, String)] -> TH.DecsQ
defineCounter = defField "Counter" []
defineGauge :: String -> [(String, String)] -> TH.DecsQ
defineGauge = defField "Gauge" []
defineTimer :: String -> [(String, String)] -> TH.DecsQ
defineTimer = defField "Timer" []
defineHistogram :: String -> [(String, String)] -> Rational -> TH.DecsQ
defineHistogram metricName metricTags sampleRate = do
when (sampleRate < 0.0 || sampleRate > 1.0) . fail $ "Histogram sample rate must be between 0.0 and 1.0"
defField "Histogram" [sampleRateField] metricName metricTags
where sampleRateField = (TH.mkName "_histogramSampleRate", TH.LitE (TH.RationalL sampleRate))
defineSet :: String -> [(String, String)] -> TH.DecsQ
defineSet = defField "Set" []
defineServiceCheck :: String -> [(String, String)] -> TH.DecsQ
defineServiceCheck = defField "ServiceCheck" []
transformTag :: (String, String) -> TH.Q (String, String)
transformTag (name, value) = do
let strippedName = stripTrailingUnderscores name
strippedValue = stripTrailingUnderscores value
ret = (strippedName, strippedValue)
unless (isValidTagName name) . fail $ "Tag name `" ++ name ++ "` is not a valid name.`"
unless (isValidTagValueForm value) . fail $ "Tag value`" ++ name ++ "` is not a valid value.`"
unless ((length strippedValue + length strippedValue) <= 199) . fail $ "Tag `" ++ show ret ++ "` ends up longer than 200 chars`"
return ret
satisfiesParser :: ReadP a -> String -> Bool
satisfiesParser p = predicate . readP_to_S p
where predicate hits = not (null hits) && length hits == 1
isValidMetricName :: String -> Bool
isValidMetricName = not . null . readP_to_S validateMetricName
isValidTag :: (String, String) -> Bool
isValidTag (name, val) = isValidTagName name
isValidTagName :: String -> Bool
isValidTagName name = isValidTagNameForm name && name /= "device"
isValidTagNameForm :: String -> Bool
isValidTagNameForm = satisfiesParser validateTagName
isValidTagValueForm :: String -> Bool
isValidTagValueForm = satisfiesParser validateTagValue
isAsciiAlpha :: Char -> Bool
isAsciiAlpha c = isAscii c && isAlpha c
isAsciiAlphaNum :: Char -> Bool
isAsciiAlphaNum c = isAscii c && isAlphaNum c
validateMetricName :: ReadP String
validateMetricName = do
first <- satisfy isAsciiAlpha
rest <- flip manyTill eof $ satisfy (\c -> c `elem` "_." || isAsciiAlphaNum c)
return $ first : rest
validateTagName :: ReadP String
validateTagName = do
first <- satisfy isAsciiAlpha
rest <- flip manyTill eof $ satisfy (\c -> c `elem` "_-." || isAsciiAlphaNum c)
return $ first : rest
validateTagValue :: ReadP String
validateTagValue = flip manyTill eof $ satisfy (\c -> c `elem` "_-." || isAsciiAlphaNum c)
stripTrailingUnderscores :: String -> String
stripTrailingUnderscores = reverse . dropWhile (== '_') . reverse
mkMetricName :: String -> TH.Name
mkMetricName = TH.mkName . fmap makeUnderscores
where makeUnderscores c = if isAsciiAlphaNum c then c else '_'
snek :: String -> String
snek "" = ""
snek (c:cs) = toLower c : cs