@@ -8,27 +8,32 @@ module Cardano.Tracer.Handlers.Metrics.Prometheus
8
8
9
9
import Cardano.Tracer.Configuration
10
10
import Cardano.Tracer.Environment
11
+ import Cardano.Tracer.Handlers.Metrics.Utils
11
12
import Cardano.Tracer.MetaTrace
12
13
13
14
import Prelude hiding (head )
14
15
16
+ import qualified Data.ByteString as ByteString
15
17
import Data.ByteString.Builder (stringUtf8 )
18
+ import Data.Char
16
19
import Data.Functor ((<&>) )
20
+ import qualified Data.HashMap.Strict as HM
21
+ import Data.List (find )
17
22
import Data.Map.Strict (Map )
23
+ import qualified Data.Map.Strict as M
18
24
import Data.Text (Text )
25
+ import qualified Data.Text as T
26
+ import qualified Data.Text.Lazy as TL
27
+ import Data.Text.Lazy.Builder (Builder )
28
+ import qualified Data.Text.Lazy.Builder as TB
29
+ import qualified Data.Text.Lazy.Builder.Int as TB
30
+ import qualified Data.Text.Lazy.Encoding as TL
19
31
import Network.HTTP.Types
20
32
import Network.Wai hiding (responseHeaders )
21
- import Network.Wai.Handler.Warp (runSettings , defaultSettings )
33
+ import Network.Wai.Handler.Warp (defaultSettings , runSettings )
34
+ import qualified System.Metrics as EKG
22
35
import System.Metrics (Sample , Value (.. ), sampleAll )
23
36
import System.Time.Extra (sleep )
24
- import qualified Cardano.Tracer.Handlers.Metrics.Utils as Utils
25
- import qualified Data.ByteString as ByteString
26
- import qualified Data.HashMap.Strict as HM
27
- import qualified Data.Map.Strict as Map
28
- import qualified Data.Text as T
29
- import qualified Data.Text.Lazy as Lazy.Text
30
- import qualified Data.Text.Lazy.Encoding as Lazy.Text
31
- import qualified System.Metrics as EKG
32
37
33
38
-- | Runs simple HTTP server that listens host and port and returns
34
39
-- the list of currently connected nodes in such a format:
@@ -52,7 +57,7 @@ import qualified System.Metrics as EKG
52
57
runPrometheusServer
53
58
:: TracerEnv
54
59
-> Endpoint
55
- -> IO Utils. RouteDictionary
60
+ -> IO RouteDictionary
56
61
-> IO ()
57
62
runPrometheusServer tracerEnv endpoint computeRoutes_autoUpdate = do
58
63
-- Pause to prevent collision between "Listening"-notifications from servers.
@@ -64,90 +69,126 @@ runPrometheusServer tracerEnv endpoint computeRoutes_autoUpdate = do
64
69
{ ttPrometheusEndpoint = endpoint
65
70
}
66
71
runSettings (setEndpoint endpoint defaultSettings) do
67
- renderPrometheus computeRoutes_autoUpdate metricsComp where
72
+ renderPrometheus computeRoutes_autoUpdate metricsComp teMetricsHelp where
68
73
69
74
TracerEnv
70
75
{ teTracer
71
76
, teConfig = TracerConfig { metricsComp }
77
+ , teMetricsHelp
72
78
} = tracerEnv
73
79
74
- renderPrometheus :: IO Utils. RouteDictionary -> Maybe (Map Text Text ) -> Application
75
- renderPrometheus computeRoutes_autoUpdate metricsComp request send = do
76
- routeDictionary :: Utils. RouteDictionary <-
80
+ renderPrometheus
81
+ :: IO RouteDictionary
82
+ -> Maybe (Map Text Text )
83
+ -> [(Text , Builder )]
84
+ -> Application
85
+ renderPrometheus computeRoutes_autoUpdate metricsComp helpTextDict request send = do
86
+ routeDictionary :: RouteDictionary <-
77
87
computeRoutes_autoUpdate
78
88
79
- let header :: RequestHeaders
80
- header = requestHeaders request
81
-
82
- let wantsJson :: Bool
83
- wantsJson = all @ Maybe (" application/json" `ByteString.isInfixOf` ) (lookup hAccept header)
89
+ let acceptHeader :: Maybe ByteString. ByteString
90
+ acceptHeader = lookup hAccept $ requestHeaders request
84
91
85
- let responseHeaders :: ResponseHeaders
86
- responseHeaders = [(hContentType, if wantsJson then " application/json" else " text/html" )]
92
+ let wantsJson, wantsOpenMetrics :: Bool
93
+ wantsJson = all @ Maybe (" application/json" `ByteString.isInfixOf` ) acceptHeader
94
+ wantsOpenMetrics = all @ Maybe (" application/openmetrics-text" `ByteString.isInfixOf` ) acceptHeader
87
95
88
96
case pathInfo request of
89
97
90
98
[] ->
91
- send $ responseLBS status200 responseHeaders if wantsJson
92
- then Utils. renderJson routeDictionary
93
- else Utils. renderListOfConnectedNodes " Prometheus metrics" (Utils. nodeNames routeDictionary)
99
+ send $ uncurry ( responseLBS status200) $ if wantsJson
100
+ then (contentHdrJSON , renderJson routeDictionary)
101
+ else (contentHdrUtf8Html, renderListOfConnectedNodes " Prometheus metrics" (nodeNames routeDictionary) )
94
102
95
103
route: _
96
- | Just (store :: EKG. Store , _ ) <- lookup route (Utils. getRouteDictionary routeDictionary)
97
- -> do metrics <- getMetricsFromNode metricsComp store
98
- send $ responseLBS status200 [(hContentType, " text/plain" )] (Lazy.Text. encodeUtf8 (Lazy.Text. fromStrict metrics))
104
+ | Just (store :: EKG. Store , _ ) <- lookup route (getRouteDictionary routeDictionary)
105
+ -> do metrics <- getMetricsFromNode metricsComp helpTextDict store
106
+ send $ responseBuilder status200
107
+ (if wantsOpenMetrics then contentHdrOpenMetrics else contentHdrUtf8Text)
108
+ (TL. encodeUtf8Builder metrics)
99
109
100
- -- all endings in ekg-wai's asset/ folder
101
110
| otherwise
102
- -> send $ responseBuilder status404 [(hContentType, " text/plain " )] do
111
+ -> send $ responseBuilder status404 contentHdrUtf8Text do
103
112
" Not found: "
104
113
<> stringUtf8 (show route)
105
114
106
- type MetricName = Text
107
- type MetricValue = Text
108
- type MetricsList = [(MetricName , MetricValue )]
115
+ type MetricName = Text
109
116
110
117
getMetricsFromNode
111
- :: Maybe (Map Text Text )
118
+ :: Maybe (Map MetricName MetricName )
119
+ -> [(Text , Builder )]
112
120
-> EKG. Store
113
- -> IO Text
114
- getMetricsFromNode metricsComp ekgStore =
115
- sampleAll ekgStore <&> renderListOfMetrics . getListOfMetrics
116
- where
117
-
118
- getListOfMetrics :: Sample -> MetricsList
119
- getListOfMetrics =
120
- metricsCompatibility
121
- . filter (not . T. null . fst )
122
- . map metricsWeNeed
123
- . HM. toList
124
-
125
- metricsWeNeed :: (Text , Value ) -> (Text , Text )
126
- metricsWeNeed (mName, mValue) =
127
- case mValue of
128
- Counter c -> (mName, T. pack $ show c)
129
- Gauge g -> (mName, T. pack $ show g)
130
- Label l -> (mName, l)
131
- _ -> (" " , " " ) -- 'ekg-forward' doesn't support 'Distribution' yet.
132
-
133
- metricsCompatibility :: MetricsList -> MetricsList
134
- metricsCompatibility metricsList =
135
- case metricsComp of
136
- Nothing -> metricsList
137
- Just mmap -> foldl (\ accu p'@ (mn,mv) -> case Map. lookup mn mmap of
138
- Nothing -> p' : accu
139
- Just rep -> p' : (rep,mv) : accu)
140
- []
141
- metricsList
142
-
143
- renderListOfMetrics :: MetricsList -> Text
144
- renderListOfMetrics [] = " No metrics were received from this node."
145
- renderListOfMetrics mList = T. intercalate " \n " $
146
- map (\ (mName, mValue) -> prepareName mName <> " " <> mValue) mList
147
-
148
- prepareName :: Text -> Text
149
- prepareName =
150
- T. filter (`elem` ([' 0' .. ' 9' ] ++ [' a' .. ' z' ] ++ [' A' .. ' Z' ] ++ [' _' ]))
151
- . T. replace " " " _"
152
- . T. replace " -" " _"
153
- . T. replace " ." " _"
121
+ -> IO TL. Text
122
+ getMetricsFromNode metricsComp helpTextDict ekgStore =
123
+ sampleAll ekgStore <&> renderExpositionFromSample metricsComp helpTextDict
124
+
125
+ renderExpositionFromSample
126
+ :: Maybe (Map MetricName MetricName )
127
+ -> [(MetricName , Builder )]
128
+ -> Sample
129
+ -> TL. Text
130
+ renderExpositionFromSample renameMap helpTextDict =
131
+ TB. toLazyText . (`mappend` buildEOF) . HM. foldlWithKey' buildMetric mempty
132
+ where
133
+ buildHelpText :: MetricName -> (Builder -> Builder )
134
+ buildHelpText name = maybe
135
+ (const mempty )
136
+ (buildHelp . snd )
137
+ (find ((`T.isInfixOf` name) . fst ) helpTextDict)
138
+
139
+ -- implements the metricsComp config option
140
+ replaceName :: MetricName -> MetricName
141
+ replaceName =
142
+ case renameMap of
143
+ Nothing -> Prelude. id
144
+ Just mmap -> \ name -> M. findWithDefault name name mmap
145
+
146
+ prepareName :: MetricName -> MetricName
147
+ prepareName =
148
+ T. filter (\ c -> isAsciiLower c || isAsciiUpper c || isDigit c || c == ' _' )
149
+ . T. replace " " " _"
150
+ . T. replace " -" " _"
151
+ . T. replace " ." " _"
152
+
153
+ -- the help annotation line
154
+ buildHelp :: Builder -> Builder -> Builder
155
+ buildHelp h n =
156
+ TB. fromText " # HELP " `mappend` (n `mappend` (space `mappend` (h `mappend` newline)))
157
+
158
+ buildMetric :: TB. Builder -> MetricName -> Value -> TB. Builder
159
+ buildMetric acc mName mValue =
160
+ acc `mappend` case mValue of
161
+ Counter c -> annotate buildCounter `mappend` buildVal space (TB. decimal c)
162
+ Gauge g -> annotate buildGauge `mappend` buildVal space (TB. decimal g)
163
+ Label l
164
+ | Just (' {' , _) <- T. uncons l
165
+ -> annotate buildInfo `mappend` buildVal mempty (TB. fromText l)
166
+ | otherwise
167
+ -> helpAnnotation `mappend` buildVal space (TB. fromText l)
168
+ _ -> mempty
169
+ where
170
+ helpAnnotation = buildHelpText mName buildName
171
+
172
+ -- annotates a metric in the order TYPE, UNIT, HELP
173
+ -- TODO: UNIT annotation
174
+ annotate annType =
175
+ buildTypeAnn annType `mappend` helpAnnotation
176
+
177
+ -- the name for exposition
178
+ buildName = TB. fromText $ prepareName $ replaceName mName
179
+
180
+ -- the type annotation line
181
+ buildTypeAnn t =
182
+ TB. fromText " # TYPE " `mappend` (buildName `mappend` (t `mappend` newline))
183
+
184
+ -- the actual metric line, optional spacing after name, because of labels: 'metric_name{label_value="foo"} 1'
185
+ buildVal spacing v =
186
+ buildName `mappend` (spacing `mappend` (v `mappend` newline))
187
+
188
+ buildGauge , buildCounter , buildInfo , buildEOF , newline , space :: Builder
189
+ buildGauge = TB. fromText " gauge"
190
+ buildCounter = TB. fromText " counter"
191
+ buildInfo = TB. fromText " info"
192
+ buildEOF = TB. fromText " # EOF\n "
193
+ newline = TB. singleton ' \n '
194
+ space = TB. singleton ' '
0 commit comments