Skip to content

Commit 5151313

Browse files
committed
cardano-tracer: Prometheus HTTP service discovery
1 parent 96c171a commit 5151313

File tree

10 files changed

+116
-45
lines changed

10 files changed

+116
-45
lines changed

cardano-tracer/bench/cardano-tracer-bench.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ main = do
155155
, hasForwarding = Nothing
156156
, resourceFreq = Nothing
157157
, ekgRequestFull = Nothing
158+
, prometheusLabels = Nothing
158159
}
159160

160161
generate :: Int -> IO [TraceObject]

cardano-tracer/src/Cardano/Tracer/Configuration.hs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,14 @@ data TracerConfig = TracerConfig
196196
, Maybe [[Text]]
197197
, Log.TraceOptionForwarder
198198
))
199-
, logging :: !(NonEmpty LoggingParams) -- ^ Logging parameters.
200-
, rotation :: !(Maybe RotationParams) -- ^ Rotation parameters.
201-
, verbosity :: !(Maybe Verbosity) -- ^ Verbosity of the tracer itself.
202-
, metricsNoSuffix :: !(Maybe Bool) -- ^ Prometheus ONLY: Dropping metrics name suffixes (like "_int") increases similiarity with old system names - if desired; default: False
203-
, metricsHelp :: !(Maybe FileOrMap) -- ^ Prometheus ONLY: JSON file or object containing a key-value map "metric name -> help text" for "# HELP " annotations
204-
, resourceFreq :: !(Maybe Int) -- ^ Frequency (1/millisecond) for gathering resource data.
205-
, ekgRequestFull :: !(Maybe Bool) -- ^ Request full set of metrics always, vs. deltas only (safer, but more overhead); default: False
199+
, logging :: !(NonEmpty LoggingParams) -- ^ Logging parameters.
200+
, rotation :: !(Maybe RotationParams) -- ^ Rotation parameters.
201+
, verbosity :: !(Maybe Verbosity) -- ^ Verbosity of the tracer itself.
202+
, metricsNoSuffix :: !(Maybe Bool) -- ^ Prometheus ONLY: Dropping metrics name suffixes (like "_int") increases similiarity with old system names - if desired; default: False
203+
, metricsHelp :: !(Maybe FileOrMap) -- ^ Prometheus ONLY: JSON file or object containing a key-value map "metric name -> help text" for "# HELP " annotations
204+
, resourceFreq :: !(Maybe Int) -- ^ Frequency (1/millisecond) for gathering resource data.
205+
, ekgRequestFull :: !(Maybe Bool) -- ^ Request full set of metrics always, vs. deltas only (safer, but more overhead); default: False
206+
, prometheusLabels :: !(Maybe (Map Text Text)) -- ^ A common label set for all Prometheus scrape targets (only used in Prometheus HTTP service discovery)
206207
}
207208
deriving stock (Eq, Show, Generic)
208209
deriving anyclass (FromJSON, ToJSON)
Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{-# LANGUAGE NamedFieldPuns #-}
22
{-# LANGUAGE OverloadedStrings #-}
33
{-# LANGUAGE ScopedTypeVariables #-}
4+
{-# LANGUAGE ViewPatterns #-}
45

56
module Cardano.Tracer.Handlers.Metrics.Prometheus
67
( runPrometheusServer
@@ -14,23 +15,27 @@ import Cardano.Tracer.MetaTrace
1415

1516
import Prelude hiding (head)
1617

18+
import Control.Applicative ((<|>))
19+
import Data.Aeson (ToJSON (..), encode, pairs, (.=))
1720
import qualified Data.ByteString as ByteString
18-
import Data.ByteString.Builder (stringUtf8)
1921
import Data.Functor ((<&>))
20-
import Data.Text (Text)
22+
import qualified Data.Map as Map (Map, empty, fromList)
23+
import Data.Maybe
24+
import Data.Text as T (Text, cons)
25+
import qualified Data.Text.Encoding as T (decodeUtf8)
2126
import qualified Data.Text.Lazy as TL
2227
import Data.Text.Lazy.Builder (Builder)
2328
import qualified Data.Text.Lazy.Encoding as TL
2429
import Network.HTTP.Types
25-
import Network.Wai hiding (responseHeaders)
30+
import Network.Wai
2631
import Network.Wai.Handler.Warp (defaultSettings, runSettings)
2732
import System.Metrics as EKG (Store, sampleAll)
2833
import System.Time.Extra (sleep)
2934

3035
-- | Runs a simple HTTP server that listens on @endpoint@.
3136
--
3237
-- At the root, it lists the connected nodes, either as HTML or JSON, depending
33-
-- on the requests 'Accept: ' header.
38+
-- on the request's 'Accept: ' header.
3439
--
3540
-- Routing is dynamic, depending on the connected nodes. A valid URL is derived
3641
-- from the nodeName configured for the connecting node. E.g. a node name
@@ -40,10 +45,6 @@ import System.Time.Extra (sleep)
4045
-- # TYPE Mem_resident_int gauge
4146
-- # HELP Mem_resident_int Kernel-reported RSS (resident set size)
4247
-- Mem_resident_int 103792640
43-
-- # TYPE rts_gc_max_bytes_used gauge
44-
-- rts_gc_max_bytes_used 5811512
45-
-- # TYPE rts_gc_gc_cpu_ms counter
46-
-- rts_gc_gc_cpu_ms 50
4748
-- # TYPE RTS_gcMajorNum_int gauge
4849
-- # HELP RTS_gcMajorNum_int Major GCs
4950
-- RTS_gcMajorNum_int 4
@@ -56,6 +57,23 @@ import System.Time.Extra (sleep)
5657
-- # TYPE nodeCannotForge_int gauge
5758
-- # HELP nodeCannotForge_int How many times was this node unable to forge [a block]?
5859
--
60+
-- The `/targets` path can be used for Prometheus HTTP service discovery. This lets
61+
-- Prometheus dynamically discover all connected nodes, and scrape their metrics.
62+
-- Below is a minimal example of a corresponding job definition that goes into the
63+
-- `prometheus.yml` configuration:
64+
--
65+
-- - job_name: "cardano-tracer"
66+
--
67+
-- http_sd_configs:
68+
-- - url: 'http://127.0.0.1:3200/targets' # <-- Your cardano-tracer's real hostname:prometheus port
69+
--
70+
-- Each target will have a label "node_name" which corresponds to the TraceOptionNodeName setting in the node config.
71+
--
72+
-- In cardano-tracer's config, you can optionally provide additional labels to be attached to *all* targets
73+
-- (default is no additional labels):
74+
-- "prometheusLabels": {
75+
-- "<labelname>": "<labelvalue>", ...
76+
-- }
5977
runPrometheusServer
6078
:: TracerEnv
6179
-> Endpoint
@@ -71,50 +89,79 @@ runPrometheusServer tracerEnv endpoint computeRoutes_autoUpdate = do
7189
{ ttPrometheusEndpoint = endpoint
7290
}
7391
runSettings (setEndpoint endpoint defaultSettings) do
74-
renderPrometheus computeRoutes_autoUpdate noSuffix teMetricsHelp
92+
renderPrometheus computeRoutes_autoUpdate noSuffix teMetricsHelp promLabels
7593
where
7694
TracerEnv
7795
{ teTracer
78-
, teConfig = TracerConfig { metricsNoSuffix }
96+
, teConfig = TracerConfig { metricsNoSuffix, prometheusLabels }
7997
, teMetricsHelp
8098
} = tracerEnv
8199

82-
noSuffix = or @Maybe metricsNoSuffix
100+
noSuffix = or @Maybe metricsNoSuffix
101+
promLabels = fromMaybe Map.empty prometheusLabels
83102

84103
renderPrometheus
85104
:: IO RouteDictionary
86105
-> Bool
87106
-> [(Text, Builder)]
107+
-> Map.Map Text Text
88108
-> Application
89-
renderPrometheus computeRoutes_autoUpdate noSuffix helpTextDict request send = do
109+
renderPrometheus computeRoutes_autoUpdate noSuffix helpTextDict promLabels request send = do
90110
routeDictionary :: RouteDictionary <-
91111
computeRoutes_autoUpdate
92112

93-
let acceptHeader :: Maybe ByteString.ByteString
94-
acceptHeader = lookup hAccept $ requestHeaders request
95-
96-
let wantsJson, wantsOpenMetrics :: Bool
97-
wantsJson = all @Maybe ("application/json" `ByteString.isInfixOf`) acceptHeader
98-
wantsOpenMetrics = all @Maybe ("application/openmetrics-text" `ByteString.isInfixOf`) acceptHeader
99-
100113
case pathInfo request of
101114

102115
[] ->
103116
send $ uncurry (responseLBS status200) $ if wantsJson
104117
then (contentHdrJSON , renderJson routeDictionary)
105118
else (contentHdrUtf8Html, renderListOfConnectedNodes "Prometheus metrics" routeDictionary)
106119

120+
["targets"]
121+
| wantsJson
122+
-> serviceDiscovery routeDictionary
123+
124+
| otherwise
125+
-> wrongMType
126+
107127
route:_
108128
| Just (store :: EKG.Store, _) <- lookup route (getRouteDictionary routeDictionary)
109-
-> do metrics <- getMetricsFromNode noSuffix helpTextDict store
110-
send $ responseBuilder status200
111-
(if wantsOpenMetrics then contentHdrOpenMetrics else contentHdrUtf8Text)
112-
(TL.encodeUtf8Builder metrics)
129+
-> metricsExposition store
113130

114131
| otherwise
115-
-> send $ responseBuilder status404 contentHdrUtf8Text do
116-
"Not found: "
117-
<> stringUtf8 (show route)
132+
-> notFound route
133+
134+
where
135+
acceptHeader :: Maybe ByteString.ByteString
136+
acceptHeader = lookup hAccept $ requestHeaders request
137+
138+
wantsJson, wantsOpenMetrics :: Bool
139+
wantsJson = all @Maybe ("application/json" `ByteString.isInfixOf`) acceptHeader
140+
wantsOpenMetrics = all @Maybe ("application/openmetrics-text" `ByteString.isInfixOf`) acceptHeader
141+
142+
-- we might support the more complex 'Forward:' header in the future
143+
getHostNameRequest :: Maybe ByteString.ByteString
144+
getHostNameRequest =
145+
lookup "x-forwarded-host" (requestHeaders request)
146+
<|> requestHeaderHost request
147+
148+
metricsExposition store = do
149+
metrics <- getMetricsFromNode noSuffix helpTextDict store
150+
send $ responseBuilder status200
151+
(if wantsOpenMetrics then contentHdrOpenMetrics else contentHdrPrometheus)
152+
(TL.encodeUtf8Builder metrics)
153+
154+
serviceDiscovery (RouteDictionary routeDict) =
155+
send $ responseLBS status200 contentHdrJSON $
156+
case getHostNameRequest of
157+
Just (T.decodeUtf8 -> hostName) -> encode
158+
[PSD (slug, nodeName, hostName, promLabels) | (slug, (_, nodeName)) <- routeDict]
159+
Nothing -> "[]"
160+
161+
notFound t = send $ responseLBS status404 contentHdrUtf8Text $
162+
"Not found: " <> (TL.encodeUtf8 . TL.fromStrict) t
163+
wrongMType = send $ responseLBS status415 contentHdrUtf8Text
164+
"Unsupported Media Type"
118165

119166
getMetricsFromNode
120167
:: Bool
@@ -123,3 +170,21 @@ getMetricsFromNode
123170
-> IO TL.Text
124171
getMetricsFromNode noSuffix helpTextDict ekgStore =
125172
sampleAll ekgStore <&> renderExpositionFromSampleWith helpTextDict noSuffix
173+
174+
175+
-- This wrapper type implements the Prometheus HTTP SD format
176+
-- cf. https://prometheus.io/docs/prometheus/latest/http_sd
177+
-- It is local to this module, and never expected to provide an Aeson.Value.
178+
newtype PrometheusServiceDiscovery = PSD (Text, Text, Text, Map.Map Text Text)
179+
180+
instance ToJSON PrometheusServiceDiscovery where
181+
toJSON _ = error "ToJSON.toJSON(PrometheusServiceDiscovery): implementation error"
182+
183+
toEncoding (PSD (slug, nodeName, hostName, labelMap)) = pairs $
184+
("targets" .= [hostName])
185+
<> ("labels" .= (labels <> labelMap))
186+
where
187+
labels = Map.fromList
188+
[ ("__metrics_path__", '/' `T.cons` slug)
189+
, ("node_name" , nodeName)
190+
]

cardano-tracer/src/Cardano/Tracer/Handlers/Metrics/Utils.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ computeRoutes TracerEnv{teConnectedNodesNames, teAcceptedMetrics} = atomically d
8585

8686

8787

88-
contentHdrJSON, contentHdrOpenMetrics, contentHdrUtf8Html, contentHdrUtf8Text :: ResponseHeaders
88+
contentHdrJSON, contentHdrOpenMetrics, contentHdrUtf8Html, contentHdrUtf8Text, contentHdrPrometheus :: ResponseHeaders
8989
contentHdrJSON = [(hContentType, "application/json")]
90-
contentHdrOpenMetrics = [(hContentType, "application/openmetrics-text; version=1.0.0; charset=utf-8")]
91-
contentHdrUtf8Html = [(hContentType, "text/html; charset=utf-8")]
92-
contentHdrUtf8Text = [(hContentType, "text/plain; charset=utf-8")]
90+
contentHdrOpenMetrics = [(hContentType, "application/openmetrics-text;version=1.0.0;charset=utf-8")]
91+
contentHdrUtf8Html = [(hContentType, "text/html;charset=utf-8")]
92+
contentHdrUtf8Text = [(hContentType, "text/plain;charset=utf-8")]
93+
contentHdrPrometheus = [(hContentType, "text/plain;version=0.0.4;charset=utf-8")]

cardano-tracer/src/Cardano/Tracer/Handlers/State/TraceObjects.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{-# LANGUAGE NamedFieldPuns #-}
2-
{-# LANGUAGE OverloadedStrings #-}
32

43
module Cardano.Tracer.Handlers.State.TraceObjects
54
( LogsLiveViewCounters

cardano-tracer/test/Cardano/Tracer/Test/Acceptor.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ launchAcceptorsSimple mode localSock dpName = do
119119
, hasForwarding = Nothing
120120
, resourceFreq = Nothing
121121
, ekgRequestFull = Nothing
122+
, prometheusLabels = Nothing
122123
}
123124

124125
-- | To be able to ask any 'DataPoint' by the name without knowing the actual type,

cardano-tracer/test/Cardano/Tracer/Test/DataPoint/Tests.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,5 @@ propDataPoint ts@TestSetup{..} rootDir localSock = do
9696
, hasForwarding = Nothing
9797
, resourceFreq = Nothing
9898
, ekgRequestFull = Nothing
99+
, prometheusLabels = Nothing
99100
}

cardano-tracer/test/Cardano/Tracer/Test/Logs/Tests.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ propLogs ts@TestSetup{..} format logRotLimitBytes logRotMaxAgeMinutes rootDir lo
8282
, hasForwarding = Nothing
8383
, resourceFreq = Nothing
8484
, ekgRequestFull = Nothing
85+
, prometheusLabels = Nothing
8586
}
8687

8788
propMultiInit :: TestSetup Identity -> LogFormat -> FilePath -> HowToConnect -> HowToConnect -> IO Property
@@ -121,6 +122,7 @@ propMultiInit ts@TestSetup{..} format rootDir howToConnect1 howToConnect2 = do
121122
, hasForwarding = Nothing
122123
, resourceFreq = Nothing
123124
, ekgRequestFull = Nothing
125+
, prometheusLabels = Nothing
124126
}
125127

126128
-- | Tests
@@ -163,6 +165,7 @@ propMultiResp ts@TestSetup{..} format rootDir howToConnect = do
163165
, hasForwarding = Nothing
164166
, resourceFreq = Nothing
165167
, ekgRequestFull = Nothing
168+
, prometheusLabels = Nothing
166169
}
167170

168171
checkPropLogsResults :: FilePath -> LogFormat -> IO Property

cardano-tracer/test/Cardano/Tracer/Test/Restart/Tests.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,5 @@ mkConfig TestSetup{..} rootDir p = TracerConfig
102102
, hasForwarding = Nothing
103103
, resourceFreq = Nothing
104104
, ekgRequestFull = Nothing
105+
, prometheusLabels = Nothing
105106
}

trace-dispatcher/src/Cardano/Logging/Prometheus/TCPServer.hs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,14 @@ buildResponse getCurrentExposition = \case
8787
| method == UNSUPPORTED -> pure $ responseError withBody errorBadMethod
8888
| accept == Unsupported -> pure $ responseError withBody errorBadContent
8989
| otherwise ->
90-
let content = if accept == OpenMetrics then hdrContentTypeOpenMetrics else hdrContentTypeText
90+
let content = if accept == OpenMetrics then hdrContentTypeOpenMetrics else hdrContentTypePrometheus
9191
in responseMessage withBody content <$> getCurrentExposition <*> epochTime
9292
where withBody = method == GET
9393

94-
hdrContentType :: [ByteString] -> Builder
95-
hdrContentType = mconcat . ("Content-Type: " :) . intersperse (char8 ';') . map byteString
96-
97-
hdrContentTypeText, hdrContentTypeOpenMetrics :: Builder
98-
hdrContentTypeText = hdrContentType ["text/plain", "charset=utf-8"]
99-
hdrContentTypeOpenMetrics = hdrContentType ["application/openmetrics-text", "version=1.0.0", "charset=utf-8"]
94+
hdrContentTypeText, hdrContentTypePrometheus, hdrContentTypeOpenMetrics :: Builder
95+
hdrContentTypeText = "Content-Type: text/plain;charset=utf-8"
96+
hdrContentTypePrometheus = "Content-Type: text/plain;version=0.0.4;charset=utf-8"
97+
hdrContentTypeOpenMetrics = "Content-Type: application/openmetrics-text;version=1.0.0;charset=utf-8"
10098

10199
hdrContentLength :: Int64 -> Builder
102100
hdrContentLength len = "Content-Length: " <> int64Dec len

0 commit comments

Comments
 (0)