ngx-export-tools-extra 0.4.1.1 → 0.5.0.0
raw patch · 4 files changed
+520/−4 lines, 4 filesdep +safePVP ok
version bump matches the API change (PVP)
Dependencies added: safe
API changes (from Hackage documentation)
+ NgxExport.Tools.Prometheus: instance Data.Aeson.Types.FromJSON.FromJSON NgxExport.Tools.Prometheus.HistogramLayout
+ NgxExport.Tools.Prometheus: instance GHC.Generics.Generic NgxExport.Tools.Prometheus.HistogramLayout
+ NgxExport.Tools.Prometheus: instance GHC.Read.Read NgxExport.Tools.Prometheus.PrometheusConf
+ NgxExport.Tools.Prometheus: scale :: Int -> Double -> Int
+ NgxExport.Tools.Prometheus: scale1000 :: ByteString -> ByteString
Files
- Changelog.md +6/−0
- NgxExport/Tools/Prometheus.hs +506/−0
- NgxExport/Tools/Subrequest.hs +3/−2
- ngx-export-tools-extra.cabal +5/−2
Changelog.md view
@@ -1,3 +1,9 @@+### 0.5.0.0++- Added module *NgxExport.Tools.Prometheus* to convert custom counters from+ [*nginx-custom-counters-module*](https://github.com/lyokha/nginx-custom-counters-module)+ to Prometheus metrics.+ ### 0.4.1.0 - Handler *renderEDETemplateFromFreeValue* was moved from the example to the
+ NgxExport/Tools/Prometheus.hs view
@@ -0,0 +1,506 @@+{-# LANGUAGE TemplateHaskell, DeriveGeneric, RecordWildCards #-}+{-# LANGUAGE TypeApplications, TupleSections, OverloadedStrings #-}++-----------------------------------------------------------------------------+-- |+-- Module : NgxExport.Tools.Prometheus+-- Copyright : (c) Alexey Radkov 2020+-- License : BSD-style+--+-- Maintainer : alexey.radkov@gmail.com+-- Stability : experimental+-- Portability : non-portable (requires Template Haskell)+--+-- Prometheus metrics from the more extra tools collection for+-- <http://github.com/lyokha/nginx-haskell-module nginx-haskell-module>.+--+-----------------------------------------------------------------------------+++module NgxExport.Tools.Prometheus (+ -- *Exporters+ -- $exporters++ -- *Utilities+ scale+ ,scale1000+ ) where++import NgxExport+import NgxExport.Tools++import Data.Map.Strict (Map)+import qualified Data.Map.Strict as M+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Char8 as C8L+import Data.IORef+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Aeson+import Data.Maybe+import Data.Word+import Control.Arrow+import System.IO.Unsafe+import GHC.Generics+import Safe++type ServerName = Text+type MetricsName = Text+type MetricsHelp = Text+type MetricsLabel = Text+type CounterValue = Word64+type MetricsData = Map MetricsName CounterValue+type HistogramData = Map MetricsName (MetricsLabel, Double)+type MetricsToLabelMap = Map MetricsName MetricsLabel++data PrometheusConf =+ PrometheusConf { pcMetrics :: Map MetricsName MetricsHelp+ , pcScale1000 :: [MetricsName]+ } deriving Read++data HistogramLayout =+ HistogramLayout { range :: MetricsToLabelMap+ , cnt :: (MetricsName, MetricsLabel)+ , err :: (MetricsName, MetricsLabel)+ } deriving Generic++instance FromJSON HistogramLayout++type AllCounters = Map ServerName MetricsData+type AllHistogramsLayout = Map ServerName (Map MetricsName HistogramLayout)+type AllOtherCounters = MetricsData++type AllMetrtics =+ (ServerName, AllCounters, AllHistogramsLayout, AllOtherCounters)++data MetricsType = Counter Double+ | Histogram HistogramData++type PrometheusMetrics = Map MetricsName (MetricsHelp, MetricsType)++conf :: IORef (Maybe PrometheusConf)+conf = unsafePerformIO $ newIORef Nothing+{-# NOINLINE conf #-}++-- $exporters+--+-- This module is aimed to convert custom counters from+-- [nginx-custom-counters-module](https://github.com/lyokha/nginx-custom-counters-module)+-- to Prometheus metrics. For this, it exposes three exporters:+-- __/prometheusConf/__ which is an 'ignitionService' in terms of module+-- "NgxExport.Tools", __/toPrometheusMetrics/__ to convert /custom counters/ to+-- Prometheus metrics, and __/scale1000/__: a small utility to convert small+-- floating point numbers to integers by multiplying them by /1000/ (this fits+-- well for dealing with request durations, for instance).+--+-- The module makes use of a few custom data types which are not exported while+-- still needed when writing Nginx configurations. In the following example they+-- are used in configurations of /simpleService_prometheusConf/ and+-- /toPrometheusMetrics/.+--+-- ==== File /test_tools_extra_prometheus.hs/+-- @+-- module TestToolsExtraPrometheus where+--+-- import NgxExport.Tools.Prometheus ()+-- @+--+-- The file does not contain any significant declarations as we are going to use+-- only the exporters.+--+-- ==== File /nginx.conf/+-- @+-- user nobody;+-- worker_processes 2;+--+-- events {+-- worker_connections 1024;+-- }+--+-- http {+-- default_type application\/octet-stream;+-- sendfile on;+--+-- map $status $inc_cnt_4xx {+-- default 0;+-- \'~^4(?:\\d){2}\' 1;+-- }+--+-- map $status $inc_cnt_5xx {+-- default 0;+-- \'~^5(?:\\d){2}\' 1;+-- }+--+-- map_to_range_index $hs_request_time $request_time_bucket+-- 0.005+-- 0.01+-- 0.05+-- 0.1+-- 0.5+-- 1.0+-- 5.0+-- 10.0+-- 30.0+-- 60.0;+--+-- map_to_range_index $hs_bytes_sent $bytes_sent_bucket+-- 0+-- 10+-- 100+-- 1000+-- 10000;+--+-- haskell load \/var\/lib\/nginx\/test_tools_extra_prometheus.so;+--+-- haskell_run_service __/simpleService_prometheusConf/__ $hs_prometheus_conf+-- \'__/PrometheusConf/__+-- { __/pcMetrics/__ = fromList+-- [(\"cnt_4xx\", \"Number of responses with 4xx status\")+-- ,(\"cnt_5xx\", \"Number of responses with 5xx status\")+-- ,(\"cnt_uptime\", \"Nginx master uptime\")+-- ,(\"cnt_uptime_reload\", \"Nginx master uptime after reload\")+-- ,(\"hst_request_time\", \"Request duration\")+-- ]+-- , __/pcScale1000/__ = [\"hst_request_time_sum\"]+-- }';+--+-- haskell_var_empty_on_error $hs_prom_metrics;+--+-- counters_survive_reload on;+--+-- server {+-- listen 8010;+-- server_name __/main/__;+-- error_log \/tmp\/nginx-test-haskell-error.log;+-- access_log \/tmp\/nginx-test-haskell-access.log;+--+-- counter $cnt_4xx inc $inc_cnt_4xx;+-- counter $cnt_5xx inc $inc_cnt_5xx;+--+-- \# cache $request_time and $bytes_sent+-- haskell_run ! $hs_request_time $request_time;+-- haskell_run ! $hs_bytes_sent $bytes_sent;+--+-- histogram $hst_request_time 11 $request_time_bucket;+-- haskell_run __/scale1000/__ $hs_request_time_scaled $hs_request_time;+-- counter $hst_request_time_sum inc $hs_request_time_scaled;+--+-- histogram $hst_bytes_sent 6 $bytes_sent_bucket;+-- counter $hst_bytes_sent_sum inc $hs_bytes_sent;+--+-- location \/ {+-- echo_sleep 0.5;+-- echo Ok;+-- }+--+-- location \/1 {+-- echo_sleep 1.0;+-- echo Ok;+-- }+--+-- location \/404 {+-- return 404;+-- }+-- }+--+-- server {+-- listen 8020;+-- server_name stats;+--+-- location \/ {+-- haskell_run __/toPrometheusMetrics/__ $hs_prom_metrics+-- '[\"__/main/__\"+-- ,__/$cnt_collection/__+-- ,__/$cnt_histograms/__+-- ,{\"cnt_uptime\": $cnt_uptime+-- ,\"cnt_uptime_reload\": $cnt_uptime_reload+-- }+-- ]';+--+-- if ($hs_prom_metrics = \'\') {+-- return 503;+-- }+--+-- echo -n $hs_prom_metrics;+-- }+--+-- location \/counters {+-- echo $cnt_collection;+-- }+--+-- location \/histograms {+-- echo $cnt_histograms;+-- }+--+-- location \/uptime {+-- echo "$cnt_uptime ($cnt_uptime_reload)";+-- }+-- }+-- }+-- @+--+-- Type /PrometheusConf/ contains fields /pcMetrics/ and /pcScale1000/. The+-- latter is a map from metrics names to help messages: this can be used to+-- bind small descriptions to the metrics as /nginx-custom-counters-module/+-- does not provide such functionality. Setting descriptions to counters is+-- optional. Field /pcScale1000/ contains a list of counters that were scaled+-- with /scale1000/ and must be converted back.+--+-- Handler /toPrometheusMetrics/ expects 4 fields: the name of the+-- /counter set id/: in our example there is only one counter set /main/,+-- predefined variables /cnt_collection/ and /cnt_histograms/ from+-- /nginx-custom-counters-module/, and a list of additional counters: in our+-- example there are two additional counters /cnt_uptime/ and+-- /cnt_uptime_reload/ which are also defined in+-- /nginx-custom-counters-module/.+--+-- To fulfill histogram description in Prometheus, the /sum/ value must be+-- provided. Histogram sums are not supported in /nginx-custom-counters-module/,+-- and therefore they must be declared in separate counters. In this example+-- there are two histograms collecting request durations and sent bytes:+-- accordingly, there are two sum counters: /hst_request_time_sum/ and+-- /hst_bytes_sent_sum/. As request durations may last milliseconds while being+-- shown in seconds, they must be scaled with /scale1000/.+--+-- To further ensure histogram validity, it is important to have the last bucket+-- in a histogram labeled as /\"+Inf\"/. This is achieved automatically when+-- the number of range boundaries in directive /map_to_range_index/ is less by+-- one than the number in the corresponding histogram declaration: in this+-- example, the map for /request_time_bucket/ has 10 range boundaries while+-- histogram /hst_request_time/ has 11 buckets, the map for /bytes_sent_bucket/+-- has 5 range boundaries while histogram /hst_bytes_sent/ has 6 buckets.+--+-- ==== A simple test+--+-- Let's look at the metrics right after starting Nginx.+--+-- > $ curl -s 'http://localhost:8020/'+-- > # HELP cnt_4xx Number of responses with 4xx status+-- > # TYPE cnt_4xx counter+-- > cnt_4xx 0.0+-- > # HELP cnt_5xx Number of responses with 5xx status+-- > # TYPE cnt_5xx counter+-- > cnt_5xx 0.0+-- > # HELP cnt_uptime Nginx master uptime+-- > # TYPE cnt_uptime counter+-- > cnt_uptime 8.0+-- > # HELP cnt_uptime_reload Nginx master uptime after reload+-- > # TYPE cnt_uptime_reload counter+-- > cnt_uptime_reload 8.0+-- > # HELP hst_bytes_sent +-- > # TYPE hst_bytes_sent histogram+-- > hst_bytes_sent_bucket{le="0"} 0+-- > hst_bytes_sent_bucket{le="10"} 0+-- > hst_bytes_sent_bucket{le="100"} 0+-- > hst_bytes_sent_bucket{le="1000"} 0+-- > hst_bytes_sent_bucket{le="10000"} 0+-- > hst_bytes_sent_bucket{le="+Inf"} 0+-- > hst_bytes_sent_count 0+-- > hst_bytes_sent_sum 0.0+-- > # HELP hst_bytes_sent_err +-- > # TYPE hst_bytes_sent_err counter+-- > hst_bytes_sent_err 0.0+-- > # HELP hst_request_time Request duration+-- > # TYPE hst_request_time histogram+-- > hst_request_time_bucket{le="0.005"} 0+-- > hst_request_time_bucket{le="0.01"} 0+-- > hst_request_time_bucket{le="0.05"} 0+-- > hst_request_time_bucket{le="0.1"} 0+-- > hst_request_time_bucket{le="0.5"} 0+-- > hst_request_time_bucket{le="1.0"} 0+-- > hst_request_time_bucket{le="5.0"} 0+-- > hst_request_time_bucket{le="10.0"} 0+-- > hst_request_time_bucket{le="30.0"} 0+-- > hst_request_time_bucket{le="60.0"} 0+-- > hst_request_time_bucket{le="+Inf"} 0+-- > hst_request_time_count 0+-- > hst_request_time_sum 0.0+-- > # HELP hst_request_time_err +-- > # TYPE hst_request_time_err counter+-- > hst_request_time_err 0.0+--+-- Run some requests and look at the metrics again.+--+-- > $ for in in {1..20} ; do curl -D- 'http://localhost:8010/' & done+-- > ...+-- > $ for in in {1..30} ; do curl -D- 'http://localhost:8010/1' & done+-- > ...+-- > $ curl 'http://127.0.0.1:8010/404'+-- > ...+--+-- > $ curl -s 'http://localhost:8020/'+-- > # HELP cnt_4xx Number of responses with 4xx status+-- > # TYPE cnt_4xx counter+-- > cnt_4xx 1.0+-- > # HELP cnt_5xx Number of responses with 5xx status+-- > # TYPE cnt_5xx counter+-- > cnt_5xx 0.0+-- > # HELP cnt_uptime Nginx master uptime+-- > # TYPE cnt_uptime counter+-- > cnt_uptime 371.0+-- > # HELP cnt_uptime_reload Nginx master uptime after reload+-- > # TYPE cnt_uptime_reload counter+-- > cnt_uptime_reload 371.0+-- > # HELP hst_bytes_sent +-- > # TYPE hst_bytes_sent histogram+-- > hst_bytes_sent_bucket{le="0"} 0+-- > hst_bytes_sent_bucket{le="10"} 0+-- > hst_bytes_sent_bucket{le="100"} 0+-- > hst_bytes_sent_bucket{le="1000"} 51+-- > hst_bytes_sent_bucket{le="10000"} 51+-- > hst_bytes_sent_bucket{le="+Inf"} 51+-- > hst_bytes_sent_count 51+-- > hst_bytes_sent_sum 9458.0+-- > # HELP hst_bytes_sent_err +-- > # TYPE hst_bytes_sent_err counter+-- > hst_bytes_sent_err 0.0+-- > # HELP hst_request_time Request duration+-- > # TYPE hst_request_time histogram+-- > hst_request_time_bucket{le="0.005"} 1+-- > hst_request_time_bucket{le="0.01"} 1+-- > hst_request_time_bucket{le="0.05"} 1+-- > hst_request_time_bucket{le="0.1"} 1+-- > hst_request_time_bucket{le="0.5"} 13+-- > hst_request_time_bucket{le="1.0"} 44+-- > hst_request_time_bucket{le="5.0"} 51+-- > hst_request_time_bucket{le="10.0"} 51+-- > hst_request_time_bucket{le="30.0"} 51+-- > hst_request_time_bucket{le="60.0"} 51+-- > hst_request_time_bucket{le="+Inf"} 51+-- > hst_request_time_count 51+-- > hst_request_time_sum 40.006+-- > # HELP hst_request_time_err +-- > # TYPE hst_request_time_err counter+-- > hst_request_time_err 0.0++prometheusConf :: PrometheusConf -> Bool -> IO L.ByteString+prometheusConf = ignitionService $ \a -> do+ atomicWriteIORef conf $ Just a+ return ""++ngxExportSimpleServiceTyped+ 'prometheusConf ''PrometheusConf SingleShotService++toPrometheusMetrics' :: PrometheusConf -> AllMetrtics -> PrometheusMetrics+toPrometheusMetrics' PrometheusConf {..} (srv, cnts, hs, ocnts) =+ let toValues = M.mapWithKey+ (\k v -> (if k `elem` pcScale1000+ then (/ 1000)+ else id+ ) $ fromIntegral v+ )+ cnts' = maybe M.empty toValues $ M.lookup srv cnts+ hs' = M.lookup srv hs+ (cntsH, cntsC) =+ if maybe True M.null hs'+ then (M.empty, M.map Counter cnts')+ else let hs'' = fromJust hs'+ rs = M.keys &&& M.foldr labeledRange M.empty $ hs''+ cntsH' = M.filterWithKey (hCounter rs) cnts'+ cntsC' = cnts' `M.difference` cntsH'+ cntsH'' = M.mapWithKey+ (\k -> toHistogram cntsH' k . range) hs''+ in (M.map Histogram cntsH'', M.map Counter cntsC')+ cntsA = cntsH `M.union` cntsC `M.union` M.map Counter (toValues ocnts)+ in M.mapWithKey (\k v -> case M.lookup k pcMetrics of+ Nothing -> ("", v)+ Just h -> (h, v)+ ) cntsA+ where labeledRange = M.union . M.filter (not . T.null) . range+ hCounter (ks, ts) k = const $+ k `M.member` ts ||+ headDef False+ (dropWhile not $+ foldr (\v ->+ let v1 = v `T.append` "_sum"+ v2 = v `T.append` "_cnt"+ in ((k == v1 || k == v2) :)+ ) [] ks+ )+ toHistogram cs hk rs =+ let ranges = M.mapWithKey+ (\k l -> case M.lookup k cs of+ Just v -> (l, v)+ Nothing -> (l, 0.0)+ ) rs+ sums = let v1 = hk `T.append` "_sum"+ v2 = hk `T.append` "_cnt"+ withZeroLabel = return . ("",)+ in M.fromList $+ map (second fromJust) $ filter (isJust . snd)+ [(v1, M.lookup v1 cs >>= withZeroLabel)+ ,(v2, M.lookup v2 cs >>= withZeroLabel)+ ]+ in ranges `M.union` sums++showPrometheusMetrics :: PrometheusMetrics -> L.ByteString+showPrometheusMetrics = L.fromStrict . T.encodeUtf8 . M.foldlWithKey+ (\a k (h, m) -> T.concat [a, "# HELP ", k, " ", h, "\n"+ , "# TYPE ", k, " ", showType m, "\n"+ ,case m of+ Histogram h' -> fst $+ M.foldlWithKey (showHistogram k)+ ("", 0.0) h'+ Counter v ->+ T.concat [k, " ", T.pack $ show v, "\n"]+ ]+ ) ""+ where showType (Counter _) = "counter"+ showType (Histogram _) = "histogram"+ showHistogram k a@(t, n) c (l, v) =+ if T.null l+ then if k `T.append` "_sum" == c+ then (T.concat [t, c, " ", T.pack $ show v, "\n"]+ ,n+ )+ else if k `T.append` "_cnt" == c+ then (T.concat [t, k, "_count "+ ,T.pack $+ show (round v :: Word64)+ ,"\n"+ ]+ ,n+ )+ else a+ else let n' = n + v+ in (T.concat [t, k, "_bucket{le=\"", l, "\"} "+ ,T.pack $ show (round n' :: Word64)+ ,"\n"+ ]+ ,n'+ )++toPrometheusMetrics :: ByteString -> IO L.ByteString+toPrometheusMetrics v = do+ let cs = fromJust $ readFromByteStringAsJSON @AllMetrtics v+ pc <- readIORef conf+ return $ case pc of+ Just c -> showPrometheusMetrics $ toPrometheusMetrics' c cs+ Nothing -> ""++ngxExportIOYY 'toPrometheusMetrics++-- | Multiplies a floating point value by a factor.+--+-- Returns an integer value as the result of rounding the scaled floating point+-- value.+scale+ :: Int -- ^ Factor+ -> Double -- ^ Floating point value+ -> Int+scale n = round . (fromIntegral n *)++-- | Multiplies a floating point value by /1000/.+--+-- The floating point value gets read from a 'ByteString'. Throws an exception+-- on conversion failure which results in returning an empty string.+scale1000+ :: ByteString -- ^ Floating point value+ -> L.ByteString+scale1000 v = let v' = fromJust $ readFromByteString @Double v+ in C8L.pack $ show $ scale 1000 v'++ngxExportYY 'scale1000+
NgxExport/Tools/Subrequest.hs view
@@ -115,7 +115,7 @@ -- -- location \/ { -- haskell_run_async __/makeSubrequest/__ $hs_subrequest--- \'{\"uri\": \"http:\/\/127.0.0.1:8020\/proxy\",+-- \'{\"uri\": \"http:\/\/127.0.0.1:8010\/proxy\", -- \"headers\": [[\"Custom-Header\", \"$arg_a\"]]}\'; -- -- if ($hs_subrequest = \'\') {@@ -128,7 +128,8 @@ -- } -- -- location \/proxy {--- internal;+-- allow 127.0.0.1;+-- deny all; -- proxy_pass http:\/\/backend; -- } --
ngx-export-tools-extra.cabal view
@@ -1,5 +1,5 @@ name: ngx-export-tools-extra-version: 0.4.1.1+version: 0.5.0.0 synopsis: More extra tools for Nginx haskell module description: More extra tools for <https://github.com/lyokha/nginx-haskell-module Nginx haskell module>.@@ -13,7 +13,7 @@ copyright: 2019-2020 Alexey Radkov category: Network build-type: Simple-cabal-version: >= 1.8+cabal-version: 1.20 source-repository head type: git@@ -29,6 +29,7 @@ manual: True library+ default-language: Haskell2010 build-depends: base >= 4.8 && < 5 , template-haskell >= 2.11.0.0 , bytestring >= 0.10.0.0@@ -47,6 +48,7 @@ , snap-server , text , time+ , safe if flag(EDE) if flag(ExperimentalEDE)@@ -60,6 +62,7 @@ , trifecta <= 2 exposed-modules: NgxExport.Tools.Aggregate+ , NgxExport.Tools.Prometheus , NgxExport.Tools.Subrequest if flag(EDE)