-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why there is no endpoint info in wai-middleware-prometheus metrics? #28
Comments
There are currently two options. If you've already architected your server as a composition of Wai Application's then you can use instrumentApp to instrument each component application with specific label. If you have a monolithic application you can use instrumentIO, though that won't include method and response code. If there is a use case not covered by those methods then I'd be happy to take a PR adding additional APIs that support your use case. |
@fimad but is it assumed that every endpoint should be in a separate Wai Application? I don't remember any application using design like this. |
Assuming #26 is fixed it seems like |
It does. My question was rather about |
Other thing is that using |
This is the setup we did. I use a template haskell function to generate a fixed string for each Yesod endpoint to use as a label in prometheus, along with the HTTP method. Then in a Wai middleware, I log the timing for that endpoint. module Mercury.Yesod.Routes.Metrics.TH (mkRouteToMetricName) where
import ClassyPrelude
import qualified Data.Text as T
import Language.Haskell.TH.Syntax
import Yesod.Core (Route (..))
import Yesod.Routes.TH.Types
-- | Template Haskell to generate a function named routeToMetricName.
--
-- For a route like HomeR, this function returns "HomeR".
--
-- For routes with parents, this function returns e.g. "APIR_MercuryR_AccountCardsR".
mkRouteToMetricName :: Name -> [ResourceTree a] -> Q [Dec]
mkRouteToMetricName appName ress = do
let fnName = mkName "routeToMetricName"
t1 `arrow` t2 = ArrowT `AppT` t1 `AppT` t2
clauses <- mapM (goTree id []) ress
return
[ SigD fnName ((ConT ''Route `AppT` ConT appName) `arrow` ConT ''Text),
FunD fnName $ concat clauses
]
-- This code was primarily copied from https://github.com/yesodweb/yesod/blob/e7cf662af7971c5558130de45c0be2d47b99324a/yesod-core/src/Yesod/Routes/TH/RouteAttrs.hs
-- Then modified to support the use case of building up metric names
-- (I usually would use more verbose names, possibly rewrite this? Somewhat nice to just be copying library code though)
goTree :: (Pat -> Pat) -> [String] -> ResourceTree a -> Q [Clause]
goTree front names (ResourceLeaf res) = return <$> goRes front names res
goTree front names (ResourceParent name _check pieces trees) =
concat <$> mapM (goTree front' newNames) trees
where
ignored = (replicate toIgnore WildP ++) . return
toIgnore = length $ filter isDynamic pieces
isDynamic Dynamic {} = True
isDynamic Static {} = False
front' = front . ConP (mkName name) . ignored
newNames = names <> [name]
goRes :: (Pat -> Pat) -> [String] -> Resource a -> Q Clause
goRes front names Resource {..} =
return $
Clause
[front $ RecP (mkName resourceName) []]
(NormalB $ toText $ intercalate "_" (names <> [resourceName]))
[]
where
toText s = VarE 'T.pack `AppE` LitE (StringL s)
module Mercury.Yesod.Routes.Metrics.Function (routeToMetricName) where
import App -- Open import to import all routes
import Mercury.Yesod.Routes.Metrics.TH (mkRouteToMetricName)
-- Generates routeToMetricName
$(mkRouteToMetricName ''App resourcesApp)
-- | Module to track metrics on all incoming requests
module Mercury.Network.Wai.Middleware.Metrics (collectMetrics) where
import App (App (..))
import ClassyPrelude
import Data.String.Conversions (cs)
import qualified Data.Text.Lazy as DTL
import qualified Data.Text.Lazy.Builder as TLB
import qualified Data.Text.Lazy.Builder.Int as TLBI
import Mercury.Timing (getElapsedSeconds, getStartTime)
import Mercury.Yesod.Routes.Metrics.Function (routeToMetricName)
import Metrics
import Network.HTTP.Types.Status (Status (..))
import Network.Wai (Middleware)
import qualified Network.Wai as Wai
import Prometheus (incCounter, observe, withLabel)
import Yesod.Core (Route, parseRoute) -- NB: Avoid importing unqualified from Yesod, since Yesod names conflict with Wai ones
collectMetrics :: App -> Middleware
collectMetrics app waiApp req sendResponse = do
-- Technically parseRoute takes a query string
-- But, afaict it isn't actually used to create the route
-- And to pass it in I need to do a little parsing, so skipping for now
let mRoute :: Maybe (Route App) = parseRoute (Wai.pathInfo req, [])
routeMetricName = maybe "404" routeToMetricName mRoute
Metrics {..} = appMetrics app
methodText = cs $ Wai.requestMethod req
start <- getStartTime
withLabel incomingHTTPTotal (routeMetricName, methodText) incCounter
waiApp req $ \(res :: Wai.Response) -> do
let httpCodeText = DTL.toStrict $ TLB.toLazyText $ TLBI.decimal $ statusCode $ Wai.responseStatus res
withLabel outgoingHTTPTotal (routeMetricName, methodText, httpCodeText) incCounter
elapsed <- getElapsedSeconds start
withLabel incomingHTTPSeconds (routeMetricName, methodText, httpCodeText) (`observe` elapsed)
withLabel incomingHTTPHistogramSeconds (routeMetricName, methodText, httpCodeText) (`observe` elapsed)
sendResponse res
|
It seems to me that use of
wai-middleware-prometheus
is quite limited as usually people use multiple endpoints and measuring average value for all of them (while the nature of those endpoints could be very different) sounds to be at least odd.Do I miss some way to get per endpoint metrics or I should create a PR adding extra label with endpoint information?
The text was updated successfully, but these errors were encountered: