mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 10:46:25 +03:00
Added a healthcheck endpoint to the Data Connector Agent API spec
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4890 GitOrigin-RevId: 605408bddf03bef66eb03be8c242797e8fcf89bb
This commit is contained in:
parent
27398f40f4
commit
3f405915e9
@ -110,6 +110,7 @@ The entry point to the reference agent application is a Fastify HTTP server. Raw
|
||||
- `GET /capabilities`, which returns the capabilities of the agent and a schema that describes the type of the configuration expected to be sent on the `X-Hasura-DataConnector-Config` header
|
||||
- `GET /schema`, which returns information about the provided _data schema_, its tables and their columns
|
||||
- `POST /query`, which receives a query structure to be executed, encoded as the JSON request body, and returns JSON conforming to the schema described by the `/schema` endpoint, and contining the requested fields.
|
||||
- `GET /health`, which can be used to either check if the agent is running, or if a particular data source is healthy
|
||||
|
||||
The `/schema` and `/query` endpoints require the request to have the `X-Hasura-DataConnector-Config` header set. That header contains configuration information that agent can use to configure itself. For example, the header could contain a connection string to the database, if the agent requires a connection string to know how to connect to a specific database. The header must be a JSON object, but the specific properties that are required are up to the agent to define.
|
||||
|
||||
@ -673,3 +674,8 @@ The key point of interest here is in the `where` field where we are comparing be
|
||||
#### Type Definitions
|
||||
|
||||
The `QueryRequest` TypeScript type in the [reference implementation](./reference/src/types/index.ts) describes the valid request body payloads which may be passed to the `POST /query` endpoint. The response body structure is captured by the `QueryResponse` type.
|
||||
|
||||
### Health endpoint
|
||||
Agents must expose a `/health` endpoint which must return a 204 No Content HTTP response code if the agent is up and running. This does not mean that the agent is able to connect to any data source it performs queries against, only that the agent is running and can accept requests, even if some of those requests might fail because a dependant service is unavailable.
|
||||
|
||||
However, this endpoint can also be used to check whether the ability of the agent to talk to a particular data source is healthy. If the endpoint is sent the `X-Hasura-DataConnector-Config` and `X-Hasura-DataConnector-SourceName` headers, then the agent is expected to check that it can successfully talk to whatever data source is being specified by those headers. If it can do so, then it must return a 204 No Content response code.
|
||||
|
@ -37,6 +37,11 @@ server.post<{ Body: QueryRequest, Reply: QueryResponse }>("/query", async (reque
|
||||
return queryData(data, request.body);
|
||||
});
|
||||
|
||||
server.get("/health", async (request, response) => {
|
||||
server.log.info({ headers: request.headers, query: request.body, }, "health.request");
|
||||
response.statusCode = 204;
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
server.log.info("interrupted");
|
||||
process.exit(0);
|
||||
|
@ -107,6 +107,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "X-Hasura-DataConnector-SourceName",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "header",
|
||||
"name": "X-Hasura-DataConnector-Config",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"additionalProperties": true,
|
||||
"nullable": false,
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid `X-Hasura-DataConnector-Config` or `X-Hasura-DataConnector-SourceName`"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -1293,6 +1293,7 @@ test-suite tests-dc-api
|
||||
, Paths_graphql_engine
|
||||
, Test.Data
|
||||
, Test.CapabilitiesSpec
|
||||
, Test.HealthSpec
|
||||
, Test.QuerySpec
|
||||
, Test.QuerySpec.BasicSpec
|
||||
, Test.QuerySpec.RelationshipsSpec
|
||||
|
@ -31,20 +31,26 @@ type CapabilitiesApi =
|
||||
|
||||
type SchemaApi =
|
||||
"schema"
|
||||
:> SourceNameHeader
|
||||
:> ConfigHeader
|
||||
:> SourceNameHeader Required
|
||||
:> ConfigHeader Required
|
||||
:> Get '[JSON] V0.SchemaResponse
|
||||
|
||||
type QueryApi =
|
||||
"query"
|
||||
:> SourceNameHeader
|
||||
:> ConfigHeader
|
||||
:> SourceNameHeader Required
|
||||
:> ConfigHeader Required
|
||||
:> ReqBody '[JSON] V0.QueryRequest
|
||||
:> Post '[JSON] V0.QueryResponse
|
||||
|
||||
type ConfigHeader = Header' '[Required, Strict] "X-Hasura-DataConnector-Config" V0.Config
|
||||
type HealthApi =
|
||||
"health"
|
||||
:> SourceNameHeader Optional
|
||||
:> ConfigHeader Optional
|
||||
:> GetNoContent
|
||||
|
||||
type SourceNameHeader = Header' '[Required, Strict] "X-Hasura-DataConnector-SourceName" SourceName
|
||||
type ConfigHeader optionality = Header' '[optionality, Strict] "X-Hasura-DataConnector-Config" V0.Config
|
||||
|
||||
type SourceNameHeader optionality = Header' '[optionality, Strict] "X-Hasura-DataConnector-SourceName" SourceName
|
||||
|
||||
type SourceName = Text
|
||||
|
||||
@ -54,13 +60,15 @@ data Routes mode = Routes
|
||||
-- | 'GET /schema'
|
||||
_schema :: mode :- SchemaApi,
|
||||
-- | 'POST /query'
|
||||
_query :: mode :- QueryApi
|
||||
_query :: mode :- QueryApi,
|
||||
-- | 'GET /health'
|
||||
_health :: mode :- HealthApi
|
||||
}
|
||||
deriving stock (Generic)
|
||||
|
||||
-- | servant-openapi3 does not (yet) support NamedRoutes so we need to compose the
|
||||
-- API the old-fashioned way using :<|> for use by @toOpenApi@
|
||||
type Api = CapabilitiesApi :<|> SchemaApi :<|> QueryApi
|
||||
type Api = CapabilitiesApi :<|> SchemaApi :<|> QueryApi :<|> HealthApi
|
||||
|
||||
-- | Provide an OpenApi 3.0 schema for the API
|
||||
openApiSchema :: OpenApi
|
||||
|
@ -12,6 +12,7 @@ import Network.HTTP.Client (defaultManagerSettings, newManager)
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, ClientError, hoistClient, mkClientEnv, runClientM, (//))
|
||||
import Test.CapabilitiesSpec qualified
|
||||
import Test.HealthSpec qualified
|
||||
import Test.Hspec (Spec)
|
||||
import Test.Hspec.Core.Runner (runSpec)
|
||||
import Test.Hspec.Core.Util (filterPredicate)
|
||||
@ -25,6 +26,7 @@ testSourceName = "dc-api-tests"
|
||||
|
||||
tests :: Client IO (NamedRoutes Routes) -> API.SourceName -> API.Config -> API.Capabilities -> Spec
|
||||
tests api sourceName agentConfig capabilities = do
|
||||
Test.HealthSpec.spec api sourceName agentConfig
|
||||
Test.CapabilitiesSpec.spec api agentConfig capabilities
|
||||
Test.SchemaSpec.spec api sourceName agentConfig
|
||||
Test.QuerySpec.spec api sourceName agentConfig capabilities
|
||||
|
@ -383,7 +383,7 @@
|
||||
"name": "TrackId",
|
||||
"type": "number",
|
||||
"nullable": false,
|
||||
"description": "The ID of the playlist"
|
||||
"description": "The ID of the track"
|
||||
},
|
||||
{
|
||||
"name": "Name",
|
||||
|
18
server/tests-dc-api/Test/HealthSpec.hs
Normal file
18
server/tests-dc-api/Test/HealthSpec.hs
Normal file
@ -0,0 +1,18 @@
|
||||
module Test.HealthSpec (spec) where
|
||||
|
||||
import Hasura.Backends.DataConnector.API (Config, Routes (..), SourceName)
|
||||
import Servant.API (NamedRoutes, NoContent (..))
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
import Test.Hspec.Expectations.Pretty (shouldBe)
|
||||
import Prelude
|
||||
|
||||
spec :: Client IO (NamedRoutes Routes) -> SourceName -> Config -> Spec
|
||||
spec api sourceName config = describe "health API" $ do
|
||||
it "returns a successful HTTP status code for a plain healthcheck" $ do
|
||||
response <- (api // _health) Nothing Nothing
|
||||
response `shouldBe` NoContent
|
||||
|
||||
it "returns a successful HTTP status code for a data source healthcheck" $ do
|
||||
response <- (api // _health) (Just sourceName) (Just config)
|
||||
response `shouldBe` NoContent
|
@ -116,7 +116,7 @@ mkLocalTestEnvironmentMock _ = do
|
||||
maeConfig <- I.newIORef chinookMock
|
||||
maeQuery <- I.newIORef Nothing
|
||||
maeThreadId <- forkIO $ runMockServer maeConfig maeQuery
|
||||
healthCheck $ "http://127.0.0.1:" <> show mockAgentPort <> "/healthz"
|
||||
healthCheck $ "http://127.0.0.1:" <> show mockAgentPort <> "/health"
|
||||
pure $ MockAgentEnvironment {..}
|
||||
|
||||
-- | Load the agent schema into HGE.
|
||||
|
@ -223,20 +223,16 @@ mockQueryHandler mcfg mquery _sourceName _cfg query = liftIO $ do
|
||||
I.writeIORef mquery (Just query)
|
||||
pure $ handler query
|
||||
|
||||
type HealthcheckApi =
|
||||
"healthz"
|
||||
:> Get '[JSON] ()
|
||||
healthcheckHandler :: Maybe API.SourceName -> Maybe API.Config -> Handler NoContent
|
||||
healthcheckHandler _sourceName _config = pure NoContent
|
||||
|
||||
healthcheckHandler :: Handler ()
|
||||
healthcheckHandler = pure ()
|
||||
|
||||
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> Server (API.Api :<|> HealthcheckApi)
|
||||
dcMockableServer mcfg mquery = (mockCapabilitiesHandler mcfg :<|> mockSchemaHandler mcfg :<|> mockQueryHandler mcfg mquery) :<|> healthcheckHandler
|
||||
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> Server API.Api
|
||||
dcMockableServer mcfg mquery = mockCapabilitiesHandler mcfg :<|> mockSchemaHandler mcfg :<|> mockQueryHandler mcfg mquery :<|> healthcheckHandler
|
||||
|
||||
mockAgentPort :: Warp.Port
|
||||
mockAgentPort = 65006
|
||||
|
||||
runMockServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> IO ()
|
||||
runMockServer mcfg mquery = do
|
||||
let app = serve (Proxy :: Proxy (API.Api :<|> HealthcheckApi)) $ dcMockableServer mcfg mquery
|
||||
let app = serve (Proxy :: Proxy API.Api) $ dcMockableServer mcfg mquery
|
||||
Warp.run mockAgentPort app
|
||||
|
@ -1,6 +1,7 @@
|
||||
-- | Helper functions for HTTP requests.
|
||||
module Harness.Http
|
||||
( get_,
|
||||
getWithStatus,
|
||||
post,
|
||||
postValue,
|
||||
postValueWithStatus,
|
||||
@ -31,6 +32,14 @@ get_ url = do
|
||||
unless (Http.getResponseStatusCode response == 200) $
|
||||
error ("Non-200 response code from HTTP request: " ++ url)
|
||||
|
||||
-- | Performs get, doesn't return the result. Simply throws if there's
|
||||
-- not an expected response status code.
|
||||
getWithStatus :: HasCallStack => [Int] -> String -> IO ()
|
||||
getWithStatus acceptableStatusCodes url = do
|
||||
response <- Http.httpNoBody (fromString url)
|
||||
unless (Http.getResponseStatusCode response `elem` acceptableStatusCodes) $
|
||||
error ("Unexpected response code from HTTP request: " ++ url ++ ". Expected: " ++ show acceptableStatusCodes)
|
||||
|
||||
-- | Post the JSON to the given URL, and produces a very descriptive
|
||||
-- exception on failure.
|
||||
postValue :: HasCallStack => String -> Http.RequestHeaders -> Value -> IO Value
|
||||
@ -94,7 +103,7 @@ healthCheck url = loop [] Constants.httpHealthCheckAttempts
|
||||
)
|
||||
loop failures attempts =
|
||||
catch
|
||||
(get_ url)
|
||||
(getWithStatus [200, 204] url)
|
||||
( \(failure :: Http.HttpException) -> do
|
||||
threadDelay Constants.httpHealthCheckIntervalMicroseconds
|
||||
loop (failure : failures) (attempts - 1)
|
||||
|
Loading…
Reference in New Issue
Block a user