packages feed

hs-hath-1.1.1: src/Metrics/Prometheus.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

module Metrics.Prometheus ( render ) where

import qualified Data.Map.Strict as Map
import qualified Data.Text       as Text

import           Mason.Builder   ( BuilderFor
                                 , StrictByteStringBackend
                                 , byteString
                                 , int64Dec
                                 , intersperse
                                 , textUtf8
                                 , toStrictByteString
                                 )

import           Metrics.Types

import           Relude          hiding ( intersperse )

type MBuilder = BuilderFor StrictByteStringBackend

-- | Render a list of metric snapshots to Prometheus text format
render :: [ MetricSnapshot ] -> ByteString
render = toStrictByteString . mconcat . map renderMetric

-- | Render a single metric snapshot
renderMetric :: MetricSnapshot -> MBuilder
renderMetric = \case
  CounterSnapshot metricId help val -> renderMetricLines "counter" metricId help val
  GaugeSnapshot metricId help val   -> renderMetricLines "gauge" metricId help val

-- | Render the complete metric output (HELP, TYPE, and value lines)
renderMetricLines :: ByteString -> MetricId -> Text -> Int64 -> MBuilder
renderMetricLines typ (MetricId name labels) help val
  = mconcat
    [ "# HELP "
    , textUtf8 name
    , " "
    , textUtf8 help
    , "\n"
    , "# TYPE "
    , textUtf8 name
    , " "
    , byteString typ
    , "\n"
    , textUtf8 name
    , if Map.null labels
        then mempty
        else renderLabels labels
    , " "
    , int64Dec val
    , "\n"
    ]

-- | Render label set: {label1="value1",label2="value2"}
renderLabels :: Labels -> MBuilder
renderLabels labels
  | Map.null labels = mempty
  | otherwise = mconcat [ "{", intersperse "," $ map renderLabel $ Map.toList labels, "}" ]

-- | Render a single label: key="value"
renderLabel :: ( Text, Text ) -> MBuilder
renderLabel ( k, v ) = textUtf8 k <> "=\"" <> textUtf8 (escapeValue v) <> "\""

-- | Escape special characters in label values according to Prometheus spec
-- See: https://prometheus.io/docs/instrumenting/exposition_formats/
escapeValue :: Text -> Text
escapeValue = Text.replace "\\" "\\\\" . Text.replace "\"" "\\\"" . Text.replace "\n" "\\n"

-- Example output:
--
-- # HELP hath_cache_sent_bytes_total Total bytes sent from cache
-- # TYPE hath_cache_sent_bytes_total counter
-- hath_cache_sent_bytes_total 1234567
--
-- # HELP hath_errors_total Total errors by type
-- # TYPE hath_errors_total counter
-- hath_errors_total{type="network"} 42
-- hath_errors_total{type="timeout"} 13
--
-- Note: Labels with the same metric name but different label values
-- are currently registered as separate Counter instances.