Skip to content

Commit

Permalink
feat: make health and graphql endpoints to be configurable (#2870)
Browse files Browse the repository at this point in the history
  • Loading branch information
laststylebender14 authored Sep 21, 2024
1 parent b6ce0cf commit 2cf0f58
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 9 deletions.
11 changes: 11 additions & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,12 @@ directive @server(
"""
responseValidation: Boolean
"""
`routes` allows customization of server endpoint paths. It provides options to change
the default paths for status and GraphQL endpoints. Default values are: - status:
"/status" - graphQL: "/graphql" If not specified, these default values will be used.
"""
routes: Routes
"""
A link to an external JS file that listens on every HTTP request response event.
"""
script: ScriptOptions
Expand Down Expand Up @@ -634,6 +640,11 @@ input Headers {
setCookies: Boolean
}

input Routes {
graphQL: String!
status: String!
}

input ScriptOptions {
timeout: Int
}
Expand Down
24 changes: 24 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,19 @@
}
}
},
"Routes": {
"type": "object",
"properties": {
"graphQL": {
"default": "/graphql",
"type": "string"
},
"status": {
"default": "/status",
"type": "string"
}
}
},
"ScriptOptions": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -1059,6 +1072,17 @@
"null"
]
},
"routes": {
"description": "`routes` allows customization of server endpoint paths. It provides options to change the default paths for status and GraphQL endpoints. Default values are: - status: \"/status\" - graphQL: \"/graphql\" If not specified, these default values will be used.",
"anyOf": [
{
"$ref": "#/definitions/Routes"
},
{
"type": "null"
}
]
},
"script": {
"description": "A link to an external JS file that listens on every HTTP request response event.",
"anyOf": [
Expand Down
6 changes: 3 additions & 3 deletions src/cli/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ pub use http_server::Server;

use self::server_config::ServerConfig;

const GRAPHQL_SLUG: &str = "/graphql";

fn log_launch(sc: &ServerConfig) {
let addr = sc.addr().to_string();
tracing::info!(
Expand All @@ -18,7 +16,9 @@ fn log_launch(sc: &ServerConfig) {
sc.http_version()
);

let graphiql_url = sc.graphiql_url() + GRAPHQL_SLUG;
let gql_slug = sc.app_ctx.blueprint.server.routes.graphql();

let graphiql_url = sc.graphiql_url() + gql_slug;
let url = playground::build_url(&graphiql_url);
tracing::info!("🌍 Playground: {}", url);
}
4 changes: 3 additions & 1 deletion src/core/blueprint/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer};

use super::Auth;
use crate::core::blueprint::Cors;
use crate::core::config::{self, ConfigModule, HttpVersion};
use crate::core::config::{self, ConfigModule, HttpVersion, Routes};
use crate::core::valid::{Valid, ValidationError, Validator};

#[derive(Clone, Debug, Setters)]
Expand All @@ -38,6 +38,7 @@ pub struct Server {
pub experimental_headers: HashSet<HeaderName>,
pub auth: Option<Auth>,
pub dedupe: bool,
pub routes: Routes,
}

/// Mimic of mini_v8::Script that's wasm compatible
Expand Down Expand Up @@ -154,6 +155,7 @@ impl TryFrom<crate::core::config::ConfigModule> for Server {
cors,
auth,
dedupe: config_server.get_dedupe(),
routes: config_server.get_routes(),
}
},
)
Expand Down
47 changes: 47 additions & 0 deletions src/core/config/server.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};

use derive_getters::Getters;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tailcall_macros::DirectiveDefinition;

Expand Down Expand Up @@ -120,6 +122,47 @@ pub struct Server {
/// `workers` sets the number of worker threads. @default the number of
/// system cores.
pub workers: Option<usize>,

#[serde(default, skip_serializing_if = "is_default")]
/// `routes` allows customization of server endpoint paths.
/// It provides options to change the default paths for status and GraphQL
/// endpoints. Default values are:
/// - status: "/status"
/// - graphQL: "/graphql" If not specified, these default values will be
/// used.
pub routes: Option<Routes>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, MergeRight, JsonSchema, Getters)]
pub struct Routes {
#[serde(default = "default_status")]
status: String,
#[serde(rename = "graphQL", default = "default_graphql")]
graphql: String,
}

fn default_status() -> String {
"/status".into()
}

fn default_graphql() -> String {
"/graphql".into()
}

impl Default for Routes {
fn default() -> Self {
Self { status: "/status".into(), graphql: "/graphql".into() }
}
}

impl Routes {
pub fn with_status<T: Into<String>>(self, status: T) -> Self {
Self { graphql: self.graphql, status: status.into() }
}

pub fn with_graphql<T: Into<String>>(self, graphql: T) -> Self {
Self { status: self.status, graphql: graphql.into() }
}
}

fn merge_right_vars(mut left: Vec<KeyValue>, right: Vec<KeyValue>) -> Vec<KeyValue> {
Expand Down Expand Up @@ -231,6 +274,10 @@ impl Server {
pub fn enable_jit(&self) -> bool {
self.enable_jit.unwrap_or(true)
}

pub fn get_routes(&self) -> Routes {
self.routes.clone().unwrap_or_default()
}
}

#[cfg(test)]
Expand Down
44 changes: 39 additions & 5 deletions src/core/http/request_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,14 @@ async fn handle_request_inner<T: DeserializeOwned + GraphQLRequestLike>(
return handle_rest_apis(req, app_ctx, req_counter).await;
}

let health_check_endpoint = app_ctx.blueprint.server.routes.status();
let graphql_endpoint = app_ctx.blueprint.server.routes.graphql();

match *req.method() {
// NOTE:
// The first check for the route should be for `/graphql`
// This is always going to be the most used route.
hyper::Method::POST if req.uri().path() == "/graphql" => {
hyper::Method::POST if req.uri().path() == graphql_endpoint => {
graphql_request::<T>(req, &app_ctx, req_counter).await
}
hyper::Method::POST
Expand All @@ -321,7 +324,7 @@ async fn handle_request_inner<T: DeserializeOwned + GraphQLRequestLike>(

graphql_request::<T>(req, &Arc::new(app_ctx), req_counter).await
}
hyper::Method::GET if req.uri().path() == "/status" => {
hyper::Method::GET if req.uri().path() == health_check_endpoint => {
let status_response = Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "application/json")
Expand Down Expand Up @@ -385,7 +388,7 @@ mod test {
use super::*;
use crate::core::async_graphql_hyper::GraphQLRequest;
use crate::core::blueprint::Blueprint;
use crate::core::config::{Config, ConfigModule};
use crate::core::config::{Config, ConfigModule, Routes};
use crate::core::rest::EndpointSet;
use crate::core::runtime::test::init;
use crate::core::valid::Validator;
Expand All @@ -394,7 +397,8 @@ mod test {
async fn test_health_endpoint() -> anyhow::Result<()> {
let sdl = tokio::fs::read_to_string(tailcall_fixtures::configs::JSONPLACEHOLDER).await?;
let config = Config::from_sdl(&sdl).to_result()?;
let blueprint = Blueprint::try_from(&ConfigModule::from(config))?;
let mut blueprint = Blueprint::try_from(&ConfigModule::from(config))?;
blueprint.server.routes = Routes::default().with_status("/health");
let app_ctx = Arc::new(AppContext::new(
blueprint,
init(None),
Expand All @@ -403,7 +407,7 @@ mod test {

let req = Request::builder()
.method(Method::GET)
.uri("http://localhost:8000/status".to_string())
.uri("http://localhost:8000/health".to_string())
.body(Body::empty())?;

let resp = handle_request::<GraphQLRequest>(req, app_ctx).await?;
Expand All @@ -415,6 +419,36 @@ mod test {
Ok(())
}

#[tokio::test]
async fn test_graphql_endpoint() -> anyhow::Result<()> {
let sdl = tokio::fs::read_to_string(tailcall_fixtures::configs::JSONPLACEHOLDER).await?;
let config = Config::from_sdl(&sdl).to_result()?;
let mut blueprint = Blueprint::try_from(&ConfigModule::from(config))?;
blueprint.server.routes = Routes::default().with_graphql("/gql");
let app_ctx = Arc::new(AppContext::new(
blueprint,
init(None),
EndpointSet::default(),
));

let query = r#"{"query": "{ __schema { queryType { name } } }"}"#;
let req = Request::builder()
.method(Method::POST)
.uri("http://localhost:8000/gql".to_string())
.header("Content-Type", "application/json")
.body(Body::from(query))?;

let resp = handle_request::<GraphQLRequest>(req, app_ctx).await?;

assert_eq!(resp.status(), StatusCode::OK);
let body = hyper::body::to_bytes(resp.into_body()).await?;
let body_str = String::from_utf8(body.to_vec())?;
assert!(body_str.contains("queryType"));
assert!(body_str.contains("name"));

Ok(())
}

#[test]
fn test_create_allowed_headers() {
use std::collections::BTreeSet;
Expand Down
19 changes: 19 additions & 0 deletions tests/core/snapshots/routes-param-on-server-directive.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"users": [
{
"name": "Leanne Graham"
}
]
}
}
}
13 changes: 13 additions & 0 deletions tests/core/snapshots/routes-param-on-server-directive.md_1.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"message": "ready"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
source: tests/core/spec.rs
expression: formatted
---
scalar Bytes

scalar Date

scalar DateTime

scalar Email

scalar Empty

scalar Int128

scalar Int16

scalar Int32

scalar Int64

scalar Int8

scalar JSON

scalar PhoneNumber

type Query {
users: [User]
}

scalar UInt128

scalar UInt16

scalar UInt32

scalar UInt64

scalar UInt8

scalar Url

type User {
name: String
}

schema {
query: Query
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: tests/core/spec.rs
expression: formatter
---
schema @server(port: 8000, routes: {status: "/health", graphQL: "/tailcall-gql"}) @upstream {
query: Query
}

type Query {
users: [User] @http(baseURL: "http://jsonplaceholder.typicode.com", path: "/users")
}

type User {
name: String
}
Loading

1 comment on commit 2cf0f58

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 11.63ms 4.14ms 114.56ms 84.74%
Req/Sec 2.18k 258.22 2.77k 83.33%

260399 requests in 30.03s, 1.31GB read

Requests/sec: 8672.49

Transfer/sec: 44.51MB

Please sign in to comment.