Skip to content

Commit

Permalink
[WPB-4406] federator improve logging (#3556)
Browse files Browse the repository at this point in the history
  • Loading branch information
battermann authored Sep 6, 2023
1 parent a56a18a commit e859176
Show file tree
Hide file tree
Showing 22 changed files with 290 additions and 71 deletions.
2 changes: 2 additions & 0 deletions changelog.d/5-internal/WPB-4406
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Extending the information returned in errors for Federator. Paths and response bodies, if available, are included in error logs.
- Prometheus metrics for outgoing and incoming federation requests added.
1 change: 1 addition & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ library
API.BrigInternal
API.Cargohold
API.Common
API.Federator
API.Galley
API.GalleyInternal
API.Gundeck
Expand Down
24 changes: 24 additions & 0 deletions integration/test/API/Federator.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module API.Federator where

import Data.Function
import GHC.Stack
import Network.HTTP.Client qualified as HTTP
import Testlib.Prelude

getMetrics ::
(HasCallStack, MakesValue domain) =>
domain ->
(ServiceMap -> HostPort) ->
App Response
getMetrics domain service = do
req <- rawBaseRequestF domain service "i/metrics"
submit "GET" req

rawBaseRequestF :: (HasCallStack, MakesValue domain) => domain -> (ServiceMap -> HostPort) -> String -> App HTTP.Request
rawBaseRequestF domain getService path = do
domainV <- objDomain domain
serviceMap <- getServiceMap domainV

liftIO . HTTP.parseRequest $
let HostPort h p = getService serviceMap
in "http://" <> h <> ":" <> show p <> ("/" <> joinHttpPath (splitHttpPath path))
55 changes: 40 additions & 15 deletions integration/test/Test/Federator.hs
Original file line number Diff line number Diff line change
@@ -1,36 +1,61 @@
{-# LANGUAGE OverloadedStrings #-}

module Test.Federator where

import API.Brig
import API.Federator (getMetrics)
import Data.Attoparsec.Text
import Data.ByteString qualified as BS
import Data.String.Conversions
import Network.HTTP.Client qualified as HTTP
import Data.Text
import SetupHelpers (randomUser)
import Testlib.Prelude

runFederatorMetrics :: (ServiceMap -> HostPort) -> App ()
runFederatorMetrics getService = do
let req = submit "GET" =<< rawBaseRequestF OwnDomain getService "/i/metrics"
handleRes res = res <$ res.status `shouldMatchInt` 200
first <- bindResponse req handleRes
second <- bindResponse req handleRes
let handleRes res = res <$ res.status `shouldMatchInt` 200
first <- bindResponse (getMetrics OwnDomain getService) handleRes
second <- bindResponse (getMetrics OwnDomain getService) handleRes
assertBool "Two metric requests should never match" $ first.body /= second.body
assertBool "Second metric response should never be 0 length (the first might be)" $ BS.length second.body /= 0
assertBool "The seconds metric response should have text indicating that it is returning metrics" $
BS.isInfixOf (cs expectedString) second.body
BS.isInfixOf expectedString second.body
where
expectedString = "# TYPE http_request_duration_seconds histogram"

rawBaseRequestF :: (HasCallStack, MakesValue domain) => domain -> (ServiceMap -> HostPort) -> String -> App HTTP.Request
rawBaseRequestF domain getService path = do
domainV <- objDomain domain
serviceMap <- getServiceMap domainV

liftIO . HTTP.parseRequest $
let HostPort h p = getService serviceMap
in "http://" <> h <> ":" <> show p <> ("/" <> joinHttpPath (splitHttpPath path))

-- The metrics setup for both internal and external federator servers
-- are the same, so we can simply run the same test for both.
testFederatorMetricsInternal :: App ()
testFederatorMetricsInternal = runFederatorMetrics federatorInternal

testFederatorMetricsExternal :: App ()
testFederatorMetricsExternal = runFederatorMetrics federatorExternal

testFederatorNumRequestsMetrics :: HasCallStack => App ()
testFederatorNumRequestsMetrics = do
u1 <- randomUser OwnDomain def
u2 <- randomUser OtherDomain def
incomingBefore <- getMetric parseIncomingRequestCount OtherDomain OwnDomain
outgoingBefore <- getMetric parseOutgoingRequestCount OwnDomain OtherDomain
bindResponse (searchContacts u1 (u2 %. "name") OtherDomain) $ \resp ->
resp.status `shouldMatchInt` 200
incomingAfter <- getMetric parseIncomingRequestCount OtherDomain OwnDomain
outgoingAfter <- getMetric parseOutgoingRequestCount OwnDomain OtherDomain
assertBool "Incoming requests count should have increased by at least 2" $ incomingAfter >= incomingBefore + 2
assertBool "Outgoing requests count should have increased by at least 2" $ outgoingAfter >= outgoingBefore + 2
where
getMetric :: (Text -> Parser Integer) -> Domain -> Domain -> App Integer
getMetric p domain origin = do
m <- getMetrics domain federatorInternal
d <- cs <$> asString origin
pure $ fromRight 0 (parseOnly (p d) (cs m.body))

parseIncomingRequestCount :: Text -> Parser Integer
parseIncomingRequestCount d =
manyTill anyChar (string ("com_wire_federator_incoming_requests{origin_domain=\"" <> d <> "\"} "))
*> decimal

parseOutgoingRequestCount :: Text -> Parser Integer
parseOutgoingRequestCount d =
manyTill anyChar (string ("com_wire_federator_outgoing_requests{target_domain=\"" <> d <> "\"} "))
*> decimal
11 changes: 7 additions & 4 deletions libs/wai-utilities/src/Network/Wai/Utilities/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Control.Error
import Data.Aeson hiding (Error)
import Data.Aeson.Types (Pair)
import Data.Domain
import Data.Text.Lazy.Encoding (decodeUtf8)
import Data.Text.Lazy.Encoding (decodeUtf8, encodeUtf8)
import Imports
import Network.HTTP.Types

Expand All @@ -50,23 +50,26 @@ instance Exception Error

data ErrorData = FederationErrorData
{ federrDomain :: !Domain,
federrPath :: !Text
federrPath :: !Text,
federrResp :: !(Maybe LByteString)
}
deriving (Eq, Show, Typeable)

instance ToJSON ErrorData where
toJSON (FederationErrorData d p) =
toJSON (FederationErrorData d p b) =
object
[ "type" .= ("federation" :: Text),
"domain" .= d,
"path" .= p
"path" .= p,
"response" .= fmap decodeUtf8 b
]

instance FromJSON ErrorData where
parseJSON = withObject "ErrorData" $ \o ->
FederationErrorData
<$> o .: "domain"
<*> o .: "path"
<*> (fmap encodeUtf8 <$> (o .: "response"))

-- | Assumes UTF-8 encoding.
byteStringError :: Status -> LByteString -> LByteString -> Error
Expand Down
3 changes: 2 additions & 1 deletion libs/wai-utilities/src/Network/Wai/Utilities/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,10 @@ logErrorMsg (Wai.Error c l m md) =
. maybe id logErrorData md
. msg (val "\"" +++ m +++ val "\"")
where
logErrorData (Wai.FederationErrorData d p) =
logErrorData (Wai.FederationErrorData d p b) =
field "domain" (domainText d)
. field "path" p
. field "response" (fromMaybe "" b)

logErrorMsgWithRequest :: Maybe ByteString -> Wai.Error -> Msg -> Msg
logErrorMsgWithRequest mr e =
Expand Down
3 changes: 2 additions & 1 deletion libs/wire-api-federation/src/Wire/API/Federation/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ mkFailureResponse status domain path body
{ Wai.federrDomain = domain,
Wai.federrPath =
"/federation"
<> Text.decodeUtf8With Text.lenientDecode (LBS.toStrict path)
<> Text.decodeUtf8With Text.lenientDecode (LBS.toStrict path),
Wai.federrResp = pure body
}
}
where
Expand Down
74 changes: 45 additions & 29 deletions libs/wire-api-federation/src/Wire/API/Federation/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ module Wire.API.Federation.Error
)
where

import Data.Domain (Domain (..))
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Text.Lazy qualified as LT
Expand Down Expand Up @@ -206,27 +207,35 @@ federationClientErrorToWai FederatorClientVersionMismatch =
"internal-error"
"Endpoint version mismatch in federation client"

federationRemoteHTTP2Error :: FederatorClientHTTP2Error -> Wai.Error
federationRemoteHTTP2Error FederatorClientNoStatusCode =
Wai.mkError
unexpectedFederationResponseStatus
"federation-http2-error"
"No status code in HTTP2 response"
federationRemoteHTTP2Error (FederatorClientHTTP2Exception e) =
Wai.mkError
unexpectedFederationResponseStatus
"federation-http2-error"
(LT.pack (displayException e))
federationRemoteHTTP2Error (FederatorClientTLSException e) =
Wai.mkError
(HTTP.mkStatus 525 "SSL Handshake Failure")
"federation-tls-error"
(LT.pack (displayException e))
federationRemoteHTTP2Error (FederatorClientConnectionError e) =
Wai.mkError
federatorConnectionRefusedStatus
"federation-connection-refused"
(LT.pack (displayException e))
federationRemoteHTTP2Error :: Domain -> Text -> FederatorClientHTTP2Error -> Wai.Error
federationRemoteHTTP2Error domain path FederatorClientNoStatusCode =
let err =
Wai.mkError
unexpectedFederationResponseStatus
"federation-http2-error"
"No status code in HTTP2 response"
in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing}
federationRemoteHTTP2Error domain path (FederatorClientHTTP2Exception e) =
let err =
Wai.mkError
unexpectedFederationResponseStatus
"federation-http2-error"
(LT.pack (displayException e))
in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing}
federationRemoteHTTP2Error domain path (FederatorClientTLSException e) =
let err =
Wai.mkError
(HTTP.mkStatus 525 "SSL Handshake Failure")
"federation-tls-error"
(LT.pack (displayException e))
in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing}
federationRemoteHTTP2Error domain path (FederatorClientConnectionError e) =
let err =
Wai.mkError
federatorConnectionRefusedStatus
"federation-connection-refused"
(LT.pack (displayException e))
in err {Wai.errorData = pure $ Wai.FederationErrorData domain path Nothing}

federationClientHTTP2Error :: FederatorClientHTTP2Error -> Wai.Error
federationClientHTTP2Error (FederatorClientConnectionError e) =
Expand All @@ -240,14 +249,21 @@ federationClientHTTP2Error e =
"federation-local-error"
(LT.pack (displayException e))

federationRemoteResponseError :: HTTP.Status -> Wai.Error
federationRemoteResponseError status =
Wai.mkError
unexpectedFederationResponseStatus
"federation-remote-error"
( "A remote federator failed with status code "
<> LT.pack (show (HTTP.statusCode status))
)
federationRemoteResponseError :: Domain -> Text -> HTTP.Status -> LByteString -> Wai.Error
federationRemoteResponseError domain path status resp =
err
{ Wai.errorData = pure $ Wai.FederationErrorData domain path $ pure resp
}
where
err =
Wai.mkError
unexpectedFederationResponseStatus
"federation-remote-error"
( "A remote federator ("
<> LT.fromStrict domain._domainText
<> ") failed with status code "
<> LT.pack (show (HTTP.statusCode status))
)

federationServantErrorToWai :: ClientError -> Wai.Error
federationServantErrorToWai (DecodeFailure msg _) = federationInvalidBody msg
Expand Down
2 changes: 2 additions & 0 deletions services/federator/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
, pem
, polysemy
, polysemy-wire-zoo
, prometheus-client
, QuickCheck
, random
, servant
Expand Down Expand Up @@ -105,6 +106,7 @@ mkDerivation {
pem
polysemy
polysemy-wire-zoo
prometheus-client
servant
servant-client-core
servant-server
Expand Down
2 changes: 2 additions & 0 deletions services/federator/federator.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ library
Federator.ExternalServer
Federator.Health
Federator.InternalServer
Federator.Metrics
Federator.MockServer
Federator.Monitor
Federator.Monitor.Internal
Expand Down Expand Up @@ -134,6 +135,7 @@ library
, pem
, polysemy
, polysemy-wire-zoo
, prometheus-client
, servant
, servant-client-core
, servant-server
Expand Down
9 changes: 8 additions & 1 deletion services/federator/src/Federator/Env.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ import Imports
import Network.DNS.Resolver (Resolver)
import Network.HTTP.Client qualified as HTTP
import OpenSSL.Session (SSLContext)
import Prometheus
import System.Logger.Class qualified as LC
import Util.Options
import Wire.API.Federation.Component
import Wire.API.Routes.FederationDomainConfig (FederationDomainConfigs)

data FederatorMetrics = FederatorMetrics
{ outgoingRequests :: Vector Text Counter,
incomingRequests :: Vector Text Counter
}

data Env = Env
{ _metrics :: Metrics,
_applog :: LC.Logger,
Expand All @@ -46,7 +52,8 @@ data Env = Env
_externalPort :: Word16,
_internalPort :: Word16,
_httpManager :: HTTP.Manager,
_http2Manager :: IORef Http2Manager
_http2Manager :: IORef Http2Manager,
_federatorMetrics :: FederatorMetrics
}

makeLenses ''Env
Expand Down
8 changes: 6 additions & 2 deletions services/federator/src/Federator/ExternalServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import Federator.Discovery
import Federator.Env
import Federator.Error.ServerError
import Federator.Health qualified as Health
import Federator.Metrics
import Federator.RPC
import Federator.Response
import Federator.Service
Expand Down Expand Up @@ -103,7 +104,8 @@ server ::
Member (Error ValidationError) r,
Member (Error DiscoveryFailure) r,
Member (Error ServerError) r,
Member (Input FederationDomainConfigs) r
Member (Input FederationDomainConfigs) r,
Member Metrics r
) =>
Manager ->
Word16 ->
Expand All @@ -125,7 +127,8 @@ callInward ::
Member (Error ValidationError) r,
Member (Error DiscoveryFailure) r,
Member (Error ServerError) r,
Member (Input FederationDomainConfigs) r
Member (Input FederationDomainConfigs) r,
Member Metrics r
) =>
Component ->
RPC ->
Expand All @@ -134,6 +137,7 @@ callInward ::
Wai.Request ->
Sem r Wai.Response
callInward component (RPC rpc) originDomain (CertHeader cert) wreq = do
incomingCounterIncr originDomain
-- only POST is supported
when (Wai.requestMethod wreq /= HTTP.methodPost) $
throw InvalidRoute
Expand Down
Loading

0 comments on commit e859176

Please sign in to comment.