{-# LANGUAGE OverloadedStrings #-}
{-|
Module      : Network.Wai.Metrics
License     : BSD3
Stability   : experimental

A <http://hackage.haskell.org/package/wai WAI> middleware to collect the following <https://ocharles.org.uk/blog/posts/2012-12-11-24-day-of-hackage-ekg.html EKG> metrics from compatible web servers:

* number of requests (counter @wai.request_count@)
* number of server errors (counter @wai.server_error_count@)
* latency distribution (distribution @wai.latency_distribution@)


Here's an example of reading these metrics from a Scotty server, and displaying them with EKG.

> -- Compile with GHC option `-with-rtsopts=-T` for GC metrics
> import Web.Scotty
> import Control.Applicative
> import System.Remote.Monitoring (serverMetricStore, forkServer)
> import Network.Wai.Metrics
>
> main :: IO()
> main = do
>   store <- serverMetricStore <$> forkServer "localhost" 8000
>   waiMetrics <- registerWaiMetrics store
>   scotty 3000 $ do
>     middleware (metrics waiMetrics)
>     get "/" $ html "Ping"

Now have a look at <http://localhost:8000 your local EKG instance> and display the request count by clicking on 'wai.request_count'.

WAI metrics can also be stored in a bare EKG store, with no UI and no GC metrics. Use ekg-core's newStore function.

Compatible web servers include the following:

*Yesod
*Scotty
*Spock
*Servant
*Warp
-}
module Network.Wai.Metrics (
  registerWaiMetrics,
  WaiMetrics(..),
  metrics) where

import Network.Wai
import System.Metrics
import Control.Monad (when)
import Data.Time.Clock
import qualified System.Metrics.Counter as Counter
import qualified System.Metrics.Distribution as Distribution
import Network.HTTP.Types.Status (statusIsServerError)

{-|
The metrics to feed in WAI and register in EKG.
-}
data WaiMetrics = WaiMetrics {
  requestCounter :: Counter.Counter
 ,serverErrorCounter :: Counter.Counter
 ,latencyDistribution :: Distribution.Distribution
}

{-|
Register in EKG a number of metrics related to web server activity.

* @wai.request_count@
* @wai.server_error_count@
* @wai.latency_distribution@
-}
registerWaiMetrics :: Store -> IO WaiMetrics
registerWaiMetrics store = do
  req <- createCounter "wai.request_count" store
  err <- createCounter "wai.server_error_count" store
  tim <- createDistribution "wai.latency_distribution" store
  return $ WaiMetrics req err tim

{-|
Create a middleware to be added to a WAI-based webserver.
-}
metrics :: WaiMetrics -> Middleware
metrics waiMetrics app req respond = do
  Counter.inc (requestCounter waiMetrics)
  start <- getCurrentTime
  app req (respond' start)
    where respond' :: UTCTime -> Response -> IO ResponseReceived
          respond' start res = do
            when (statusIsServerError $ responseStatus res) (Counter.inc (serverErrorCounter waiMetrics))
            end <- getCurrentTime
            Distribution.add (latencyDistribution waiMetrics) (realToFrac $ diffUTCTime end start)
            respond res