Skip to content

Commit 86368ed

Browse files
authored
trace-dispatcher: maintenance (#12)
* trace-dispatcher: maintenance pass * trace-dispatcher: additional resilience for some code paths
1 parent 87700a0 commit 86368ed

14 files changed

Lines changed: 161 additions & 156 deletions

File tree

trace-dispatcher/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
* Snoc a namespace separator `.` to the metrics prefix config value when missing.
99
* Provide `Cardano.Logging.Prometheus.Exposition.asPrometheusMetricName`, creating an external (Prometheus naming schemal compliant) metric name from an internal one.
1010
* Rename `NodeStartupInfo.suiEra` to `suiLatestSupportedEra`.
11+
* `TCPServer.hs`: `Content-Length` is now computed from byte count of the UTF-8 encoded body, not the character count.
12+
* `TracerInfoConfig` namespace form is now identical to constructor.
13+
* Limiter name is now included in the machine-readable trace output for high detail levels.
14+
* New exception type `HermodException` - currently only used for config errors.
15+
* Safeguard ceiling for PrometheusSimple response size, corresponding to roughly 56,000 distinct application metrics.
1116

1217
## 2.12.1 -- Apr 2026
1318

trace-dispatcher/src/Cardano/Logging/Configuration.hs

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
module Cardano.Logging.Configuration
77
( ConfigReflection (..)
8+
, HermodException(..)
89
, emptyConfigReflection
910
, configureTracers
1011
, withNamespaceConfig
@@ -28,18 +29,27 @@ import Cardano.Logging.Trace
2829
import Cardano.Logging.TraceDispatcherMessage
2930
import Cardano.Logging.Types
3031

32+
import Control.Applicative (asum)
33+
import Control.Exception
3134
import Control.Monad (unless)
3235
import Control.Monad.IO.Class (MonadIO, liftIO)
3336
import Control.Monad.IO.Unlift (MonadUnliftIO)
3437
import qualified Control.Tracer as T
3538
import Data.IORef (IORef, modifyIORef, newIORef, readIORef, writeIORef)
36-
import Data.List (maximumBy, nub)
37-
import qualified Data.Map as Map
38-
import Data.Maybe (fromMaybe, mapMaybe)
39+
import Data.List (inits, maximumBy, nub)
40+
import qualified Data.Map.Lazy as Map
41+
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
3942
import qualified Data.Set as Set
4043
import Data.Text (Text, intercalate, unpack)
4144

4245

46+
-- This is currently ad-hoc. With a future refactoring of trace-dispatcher,
47+
-- it will be moved and serve as a proper error / exception type.
48+
data HermodException = HermodConfigException { excMessage :: String }
49+
deriving Show
50+
51+
instance Exception HermodException
52+
4353
-- | Call this function at initialisation, and later for reconfiguration.
4454
-- Config reflection is used to optimise the tracers and has to collect
4555
-- information about the tracers. Although it is possible to give more then
@@ -153,6 +163,7 @@ withNamespaceConfig name extract withConfig tr = do
153163
ref <- liftIO (newIORef (Left (Map.empty, Nothing)))
154164
pure $ contramapM' (mapFunc ref)
155165
where
166+
configError = liftIO . throwIO . HermodConfigException
156167
mapFunc ref =
157168
\case
158169
(lc, Right a) -> do
@@ -192,11 +203,11 @@ withNamespaceConfig name extract withConfig tr = do
192203
then do
193204
Trace tt <- withConfig (Just val) tr
194205
T.traceWith tt (lc, Left (TCConfig c))
195-
else error $ "Inconsistent trace configuration with context "
206+
else configError $ "Inconsistent trace configuration with context "
196207
++ show nst
197-
Right _val -> error $ "Trace not reset before reconfiguration (1)"
208+
Right _val -> configError $ "Trace not reset before reconfiguration (1)"
198209
++ show nst
199-
Left (_cmap, Just _v) -> error $ "Trace not reset before reconfiguration (2)"
210+
Left (_cmap, Just _v) -> configError $ "Trace not reset before reconfiguration (2)"
200211
++ show nst
201212
(lc, Left (TCOptimize cr)) -> do
202213
eitherConf <- liftIO $ readIORef ref
@@ -222,10 +233,10 @@ withNamespaceConfig name extract withConfig tr = do
222233
liftIO $ writeIORef ref (Left (newmap, Just mostCommon))
223234
Trace tt <- withConfig Nothing tr
224235
T.traceWith tt (lc, Left (TCOptimize cr))
225-
Right _val -> error $ "Trace not reset before reconfiguration (3)"
236+
Right _val -> configError $ "Trace not reset before reconfiguration (3)"
226237
++ show nst
227238
Left (_cmap, Just _v) ->
228-
error $ "Trace not reset before reconfiguration (4)"
239+
configError $ "Trace not reset before reconfiguration (4)"
229240
++ show nst
230241
(lc, Left dc@TCDocument {}) -> do
231242
eitherConf <- liftIO $ readIORef ref
@@ -243,7 +254,7 @@ withNamespaceConfig name extract withConfig tr = do
243254
Nothing -> do
244255
tt <- withConfig (Just v) tr
245256
T.traceWith (unpackTrace tt) (lc, Left dc)
246-
Left (_cmap, Nothing) -> error ("Missing configuration(2) " <> name <> " ns " <> show nst)
257+
Left (_cmap, Nothing) -> configError $ "Missing configuration(2) " <> name <> " ns " <> show nst
247258

248259

249260
-- | Filter a trace by severity and take the filter value from the config
@@ -253,7 +264,7 @@ filterSeverityFromConfig :: (MonadIO m) =>
253264
filterSeverityFromConfig =
254265
withNamespaceConfig
255266
"severity"
256-
getSeverity'
267+
(\conf -> pure . getSeverity conf)
257268
(\sev tr -> contramapMCond tr (mapF sev))
258269
where
259270
mapF confSev =
@@ -282,7 +293,7 @@ withDetailsFromConfig :: (MonadIO m) =>
282293
withDetailsFromConfig =
283294
withNamespaceConfig
284295
"details"
285-
getDetails'
296+
(\conf -> pure . getDetails conf)
286297
(\mbDtl b -> case mbDtl of
287298
Just dtl -> pure $ setDetails dtl b
288299
Nothing -> pure $ setDetails DNormal b)
@@ -294,7 +305,7 @@ withBackendsFromConfig :: (MonadIO m) =>
294305
withBackendsFromConfig rappendPrefixNameAndFormatter =
295306
withNamespaceConfig
296307
"backends"
297-
getBackends'
308+
(\conf -> pure . getBackends conf)
298309
rappendPrefixNameAndFormatter
299310
(Trace T.nullTracer)
300311

@@ -377,9 +388,6 @@ getSeverity config ns =
377388
severitySelector (ConfSeverity s) = Just s
378389
severitySelector _ = Nothing
379390

380-
getSeverity' :: Applicative m => TraceConfig -> Namespace a -> m SeverityF
381-
getSeverity' config ns = pure $ getSeverity config ns
382-
383391
-- | If no details can be found in the config, it is set to DNormal
384392
getDetails :: TraceConfig -> Namespace a -> DetailLevel
385393
getDetails config ns =
@@ -389,9 +397,6 @@ getDetails config ns =
389397
detailSelector (ConfDetail d) = Just d
390398
detailSelector _ = Nothing
391399

392-
getDetails' :: Applicative m => TraceConfig -> Namespace a -> m DetailLevel
393-
getDetails' config n = pure $ getDetails config n
394-
395400
-- | If no backends can be found in the config, it is set to
396401
-- [EKGBackend, Forwarder, Stdout HumanFormatColoured]
397402
getBackends :: TraceConfig -> Namespace a -> [BackendConfig]
@@ -403,9 +408,6 @@ getBackends config ns =
403408
backendSelector (ConfBackend s) = Just s
404409
backendSelector _ = Nothing
405410

406-
getBackends' :: Applicative m => TraceConfig -> Namespace a -> m [BackendConfig]
407-
getBackends' config ns = pure $ getBackends config ns
408-
409411
-- | May return a limiter specification
410412
getLimiterSpec :: TraceConfig -> Namespace a -> Maybe (Text, Double)
411413
getLimiterSpec config ns = getOption limiterSelector config (nsGetComplete ns)
@@ -414,17 +416,10 @@ getLimiterSpec config ns = getOption limiterSelector config (nsGetComplete ns)
414416
limiterSelector (ConfLimiter f) = Just (intercalate "." (nsPrefix ns ++ nsInner ns), f)
415417
limiterSelector _ = Nothing
416418

417-
-- | Searches in the config to find an option
419+
-- | Searches in the config to find an option, most-specific as per namespace first.
420+
-- (Generates all ancestor prefixes once via `inits`, avoiding a repeated O(n) `init` call at each recursion level)
418421
getOption :: (ConfigOption -> Maybe a) -> TraceConfig -> [Text] -> Maybe a
419-
getOption sel config [] =
420-
case Map.lookup [] (tcOptions config) of
421-
Nothing -> Nothing
422-
Just options -> case mapMaybe sel options of
423-
[] -> Nothing
424-
(opt : _) -> Just opt
425-
getOption sel config ns =
426-
case Map.lookup ns (tcOptions config) of
427-
Nothing -> getOption sel config (init ns)
428-
Just options -> case mapMaybe sel options of
429-
[] -> getOption sel config (init ns)
430-
(opt : _) -> Just opt
422+
getOption sel TraceConfig{tcOptions} ns =
423+
asum $ map tryLookup $ reverse $ inits ns
424+
where
425+
tryLookup k = listToMaybe . mapMaybe sel =<< Map.lookup k tcOptions

trace-dispatcher/src/Cardano/Logging/ConfigurationParser.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ instance AE.ToJSON ConfigRepresentation where
8888
, "ApplicationName" .= traceOptionNodeName
8989
, "MetricsPrefix" .= traceOptionMetricsPrefix
9090
, "PrometheusSimpleRun" .= tracePrometheusSimpleRun
91+
-- Both *Frequency fields are scheduled for removal from the data type.
92+
-- The current data loss on round-trip wrt. `parseAsLegacy` above is deliberate.
9193
]
9294

9395
type OptionsRepresentation = Map.Map Text ConfigOptionRep

trace-dispatcher/src/Cardano/Logging/DocuGenerator.hs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ import Data.Text as T (Text, empty,
5050
intercalate, lines,
5151
pack, stripPrefix,
5252
toLower, unlines)
53-
import Data.Text.Internal.Builder (toLazyText)
53+
import qualified Data.Text as T (null)
5454
import Data.Text.Lazy (toStrict)
5555
import Data.Text.Lazy.Builder (Builder, fromString,
56-
fromText, singleton)
56+
fromText, singleton,
57+
toLazyText)
5758
import Data.Tree (Forest, Tree (..),
5859
unfoldForest)
5960

@@ -568,6 +569,8 @@ generateTOC DocTracer {..} traces metrics datapoints =
568569
link = mconcat (map (fromText . toLower) firstTracer)
569570

570571
-- The first tracer in the list of tracers that has that namespace prefix
572+
-- Safe: the tree is constructed from allTracers via toForest (and trim, which
573+
-- preserves the invariant), so ns is always a prefix of some element in allTracers.
571574
firstTracer :: [Text]
572575
firstTracer = fromJust $ find (ns `isPrefixOf`) allTracers
573576

@@ -576,15 +579,13 @@ asCode :: Builder -> Builder
576579
asCode b = singleton '`' <> b <> singleton '`'
577580

578581
accentuated :: Text -> Builder
579-
accentuated t = if t == ""
580-
then fromText "\n"
581-
else fromText "\n"
582-
<> fromText (unlines $ map addAccent (lines t))
582+
accentuated t =
583+
singleton '\n'
584+
<> mconcat (map (\l -> addAccent l <> singleton '\n') (T.lines t))
583585
where
584-
addAccent :: Text -> Text
585-
addAccent t' = if t' == ""
586-
then ">"
587-
else "> " <> t'
586+
addAccent l
587+
| T.null l = singleton '>'
588+
| otherwise = fromText "> " <> fromText l
588589

589590
-- Convert a list of namespaces to a tree representation
590591
toForest :: [[Text]] -> Forest Text

trace-dispatcher/src/Cardano/Logging/Formatter.hs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
{-# LANGUAGE FlexibleContexts #-}
21
{-# LANGUAGE FlexibleInstances #-}
3-
{-# LANGUAGE PartialTypeSignatures #-}
42
{-# LANGUAGE RankNTypes #-}
53
{-# LANGUAGE ScopedTypeVariables #-}
64

@@ -46,6 +44,8 @@ import System.IO.Unsafe (unsafePerformIO)
4644
-- | If the @TRACE_DISPATCHER_LOGGING_HOSTNAME@ environment variable is set,
4745
-- it overrides the system hostname in the trace message. This is useful when
4846
-- multiple instances of a service or application on the same host.
47+
--
48+
-- The env var is expected to be set (if desired) before the application emits its first trace, as this is evaluated only once.
4949
hostname :: Text
5050
{-# NOINLINE hostname #-}
5151
hostname = unsafePerformIO $
@@ -246,8 +246,9 @@ colorBySeverity withColor severity' msg =
246246

247247
humanFormatter
248248
:: forall a m .
249-
MonadIO m
250-
=> LogFormatting a
249+
( MonadIO m
250+
, LogFormatting a
251+
)
251252
=> Bool
252253
-> Trace m FormattedMessage
253254
-> m (Trace m a)
@@ -256,26 +257,29 @@ humanFormatter withColor =
256257

257258
machineFormatter
258259
:: forall a m .
259-
(MonadIO m
260-
, LogFormatting a)
260+
( MonadIO m
261+
, LogFormatting a
262+
)
261263
=> Trace m FormattedMessage
262264
-> m (Trace m a)
263265
machineFormatter =
264266
preFormatted False . machineFormatter'
265267

266268
cborFormatter
267269
:: forall a m .
268-
(MonadIO m
269-
, LogFormatting a)
270+
( MonadIO m
271+
, LogFormatting a
272+
)
270273
=> Trace m FormattedMessage
271274
-> m (Trace m a)
272275
cborFormatter =
273276
preFormatted False . cborFormatter'
274277

275278
forwardFormatter
276279
:: forall a m .
277-
MonadIO m
278-
=> LogFormatting a
280+
( MonadIO m
281+
, LogFormatting a
282+
)
279283
=> Trace m FormattedMessage
280284
-> m (Trace m a)
281285
forwardFormatter =

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
{-# LANGUAGE RecordWildCards #-}
44
{-# LANGUAGE ViewPatterns #-}
55

6-
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
7-
8-
96
-- | Run a TCP server, with hardening against connection flooding
107
module Cardano.Logging.Prometheus.NetworkRun
118
( NetworkRunParams (..)
@@ -25,7 +22,6 @@ import Control.Monad (forever, void, when)
2522
import qualified Data.Foldable as F (sum)
2623
import Data.Hashable (hash)
2724
import qualified Data.IntMap.Strict as IM
28-
import qualified Data.List.NonEmpty as NE
2925
import Data.Maybe (fromMaybe)
3026
import Network.Socket
3127
import qualified System.TimeManager as T

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

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import Data.Aeson.Types as AE (Value (String), (.=))
2323
import Data.ByteString (ByteString)
2424
import Data.ByteString.Builder
2525
import qualified Data.ByteString.Char8 as BC
26+
import qualified Data.ByteString.Lazy.Char8 as BCL (length)
2627
import Data.Int (Int64)
2728
import Data.List (find, intersperse)
2829
import Data.Maybe (fromMaybe)
2930
import Data.Text as TS (pack)
3031
import Data.Text.Lazy (Text)
31-
import qualified Data.Text.Lazy as T
32-
import qualified Data.Text.Lazy.Encoding as T (encodeUtf8Builder)
32+
import qualified Data.Text.Lazy as TL (length)
33+
import qualified Data.Text.Lazy.Encoding as T (encodeUtf8)
3334
import Data.Word (Word16)
3435
import Network.HTTP.Date (epochTimeToHTTPDate, formatHTTPDate)
3536
import Network.Socket (HostName, PortNumber)
@@ -158,11 +159,17 @@ hdrContentTypeOpenMetrics = "Content-Type: application/openmetrics-text;version=
158159
hdrContentLength :: Int64 -> Builder
159160
hdrContentLength len = "Content-Length: " <> int64Dec len
160161

161-
errorBadRequest, errorNotFound, errorBadMethod, errorBadContent :: (ByteString, ByteString)
162-
errorBadRequest = ("400", "Bad Request")
163-
errorNotFound = ("404", "Not Found")
164-
errorBadMethod = ("405", "Method Not Allowed")
165-
errorBadContent = ("415", "Unsupported Media Type")
162+
errorBadRequest, errorNotFound, errorBadMethod, errorBadContent, errorPayloadTooLarge :: (ByteString, ByteString)
163+
errorBadRequest = ("400", "Bad Request")
164+
errorNotFound = ("404", "Not Found")
165+
errorBadMethod = ("405", "Method Not Allowed")
166+
errorBadContent = ("415", "Unsupported Media Type")
167+
errorPayloadTooLarge = ("503", "Service Unavailable")
168+
169+
-- 8 MB ceiling as a safeguard for response size - this should never fire.
170+
-- Corresponds to roughly 56.000 distinct application metrics: a legitimate exposition should be well under this!
171+
maxResponseChars :: Int64
172+
maxResponseChars = 8 * 1024 * 1024
166173

167174
-- HTTP header line break
168175
nl :: Builder
@@ -185,14 +192,19 @@ responseError withBody (errCode, errMsg) =
185192
msg = errCode <> " " <> errMsg
186193

187194
responseMessage :: Bool -> Builder -> Text -> EpochTime -> Builder
188-
responseMessage withBody contentType msg now =
189-
mconcat $ intersperse nl
190-
[ "HTTP/1.1 200 OK"
191-
, hdrContentLength (T.length msg)
192-
, contentType
193-
, "Date: " <> byteString httpDate
194-
, ""
195-
, if withBody then T.encodeUtf8Builder msg else ""
196-
]
197-
where
198-
httpDate = formatHTTPDate $ epochTimeToHTTPDate now
195+
responseMessage withBody contentType msg now
196+
| withBody && TL.length msg > maxResponseChars =
197+
responseError True errorPayloadTooLarge
198+
| otherwise =
199+
mconcat $ intersperse nl
200+
[ "HTTP/1.1 200 OK"
201+
, hdrContentLength contentLength
202+
, contentType
203+
, "Date: " <> byteString httpDate
204+
, ""
205+
, if withBody then lazyByteString contentLBS else ""
206+
]
207+
where
208+
contentLBS = T.encodeUtf8 msg
209+
contentLength = if withBody then BCL.length contentLBS else 0
210+
httpDate = formatHTTPDate $ epochTimeToHTTPDate now

0 commit comments

Comments
 (0)