Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WPB-4406] federator improve logging #3556

Merged
merged 13 commits into from
Sep 6, 2023
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 und incoming federation requests added
battermann marked this conversation as resolved.
Show resolved Hide resolved
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
Loading