-- | This module provides "Network.Wai" middlware for exporting "Prometheus"
-- metrics and for instrumenting WAI applications.
module JmlSvc.Instrument
  ( prometheus
  , requestLatency
  ) where

import Protolude

import qualified Data.ByteString.Builder as BS
import qualified Data.Default as Default
import Data.Ratio ((%))
import Data.String (String)
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wai as Wai
import qualified Prometheus as Prom
import System.Clock (Clock(..), diffTimeSpec, getTime, toNanoSecs)

-- | Settings that control the behavior of the Prometheus middleware.
data PrometheusSettings = PrometheusSettings
  { prometheusEndPoint :: [Text]
        -- ^ The path that will be used for exporting metrics. The default value
        -- is ["metrics"] which corresponds to the path /metrics.
  , prometheusHandlerName :: Maybe Text
        -- ^ The name of the handler used to record metrics about the Prometheus
        -- endpoint. If Nothing, then we won't record any.
  }

instance Default.Default PrometheusSettings where
  def = PrometheusSettings {prometheusEndPoint = ["metrics"], prometheusHandlerName = Just "metrics"}

type RequestLatency = Prom.Metric (Prom.Vector Prom.Label3 Prom.Histogram)

requestLatency :: IO RequestLatency
requestLatency = Prom.vector ("handler", "method", "status_code") $ Prom.histogram info Prom.defaultBuckets
  where
    info = Prom.Info "http_request_duration_seconds" "The HTTP request latencies in seconds."

-- | Instrument a WAI app with the default WAI metrics.
--
-- If you use this function you will likely want to override the default value
-- of 'prometheusInstrumentApp' to be false so that your app does not get double
-- instrumented.
instrumentApp ::
     RequestLatency -- ^ Metric to record thingy on
  -> Text -- ^ The label used to identify this app
  -> Wai.Application -- ^ The app to instrument
  -> Wai.Application -- ^ The instrumented app
instrumentApp metric handler app req respond = do
  start <- getTime Monotonic
  app req $ \res -> do
    end <- getTime Monotonic
    let method = toS (Wai.requestMethod req)
    let status = show @Int @String (HTTP.statusCode (Wai.responseStatus res))
    let latency = fromRational $ toRational (toNanoSecs (end `diffTimeSpec` start) % 1000000000)
    Prom.withLabel (toS handler, method, status) (Prom.observe latency) metric
    respond res

-- | Instrument an app with Prometheus and export metrics from the configured
-- handler.
prometheus ::
     PrometheusSettings -- ^ How we're going to use Prometheus
  -> RequestLatency -- ^ A metric to instrument with request information
  -> Text -- ^ The label used to identify the app
  -> Wai.Middleware
prometheus PrometheusSettings {..} duration appName app req respond =
  if Wai.requestMethod req == HTTP.methodGet && Wai.pathInfo req == prometheusEndPoint
    then case prometheusHandlerName of
           Nothing -> respondWithMetrics respond
           Just name -> instrumentApp duration name (const respondWithMetrics) req respond
    else instrumentApp duration appName app req respond

respondWithMetrics :: (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
respondWithMetrics respond = do
  metrics <- Prom.exportMetricsAsText
  respond $ Wai.responseBuilder HTTP.status200 headers $ BS.byteString metrics
  where
    headers = [(HTTP.hContentType, "text/plain; version=0.0.4")]