Skip to content

Commit 52b9492

Browse files
committed
feat(router): Hive Console Usage Reporting
Use the published package Go Fix More .. Update readme Enabled Enabled Improvements Go Update match patterns
1 parent afd9442 commit 52b9492

File tree

15 files changed

+493
-5
lines changed

15 files changed

+493
-5
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ retry-policies = "0.4.0"
5959
reqwest-retry = "0.7.0"
6060
reqwest-middleware = "0.4.2"
6161
vrl = { version = "0.28.0", features = ["compiler", "parser", "value", "diagnostic", "stdlib", "core"] }
62+
regex-automata = "0.4.10"

bin/router/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ reqwest-retry = { workspace = true }
4545
reqwest-middleware = { workspace = true }
4646
vrl = { workspace = true }
4747
serde_json = { workspace = true }
48+
regex-automata = { workspace = true }
4849

4950
mimalloc = { version = "0.1.48", features = ["v3"] }
5051
moka = { version = "0.12.10", features = ["future"] }
5152
hive-console-sdk = "0.2.0"
5253
ulid = "1.2.1"
5354
tokio-util = "0.7.16"
5455
cookie = "0.18.1"
55-
regex-automata = "0.4.10"
5656
arc-swap = "1.7.1"

bin/router/src/lib.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
},
2020
jwt::JwtAuthRuntime,
2121
logger::configure_logging,
22-
pipeline::graphql_request_handler,
22+
pipeline::{graphql_request_handler, usage_reporting::init_hive_user_agent},
2323
};
2424

2525
pub use crate::{schema_state::SchemaState, shared_state::RouterSharedState};
@@ -111,11 +111,23 @@ pub async fn configure_app_from_config(
111111
false => None,
112112
};
113113

114+
let hive_usage_agent = match router_config.usage_reporting.enabled {
115+
true => Some(init_hive_user_agent(
116+
bg_tasks_manager,
117+
&router_config.usage_reporting,
118+
)?),
119+
false => None,
120+
};
121+
114122
let router_config_arc = Arc::new(router_config);
115123
let schema_state =
116124
SchemaState::new_from_config(bg_tasks_manager, router_config_arc.clone()).await?;
117125
let schema_state_arc = Arc::new(schema_state);
118-
let shared_state = Arc::new(RouterSharedState::new(router_config_arc, jwt_runtime)?);
126+
let shared_state = Arc::new(RouterSharedState::new(
127+
router_config_arc,
128+
jwt_runtime,
129+
hive_usage_agent,
130+
)?);
119131

120132
Ok((shared_state, schema_state_arc))
121133
}

bin/router/src/pipeline/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::sync::Arc;
1+
use std::{sync::Arc, time::Instant};
22

33
use hive_router_plan_executor::execution::{
44
client_request_details::{ClientRequestDetails, JwtRequestDetails, OperationDetails},
@@ -46,6 +46,7 @@ pub mod normalize;
4646
pub mod parser;
4747
pub mod progressive_override;
4848
pub mod query_plan;
49+
pub mod usage_reporting;
4950
pub mod validation;
5051

5152
static GRAPHIQL_HTML: &str = include_str!("../../static/graphiql.html");
@@ -114,6 +115,7 @@ pub async fn execute_pipeline(
114115
shared_state: &Arc<RouterSharedState>,
115116
schema_state: &Arc<SchemaState>,
116117
) -> Result<PlanExecutionOutput, PipelineError> {
118+
let start = Instant::now();
117119
perform_csrf_prevention(req, &shared_state.router_config.csrf)?;
118120

119121
let mut execution_request = get_execution_request(req, body_bytes).await?;
@@ -193,5 +195,19 @@ pub async fn execute_pipeline(
193195
)
194196
.await?;
195197

198+
if shared_state.router_config.usage_reporting.enabled {
199+
if let Some(hive_usage_agent) = &shared_state.hive_usage_agent {
200+
usage_reporting::collect_usage_report(
201+
supergraph.supergraph_schema.clone(),
202+
start.elapsed(),
203+
req,
204+
&client_request_details,
205+
hive_usage_agent,
206+
&shared_state.router_config.usage_reporting,
207+
&execution_result,
208+
);
209+
}
210+
}
211+
196212
Ok(execution_result)
197213
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::{
2+
sync::Arc,
3+
time::{Duration, SystemTime, UNIX_EPOCH},
4+
};
5+
6+
use async_trait::async_trait;
7+
use graphql_parser::schema::Document;
8+
use hive_console_sdk::agent::{AgentError, ExecutionReport, UsageAgent, UsageAgentExt};
9+
use hive_router_config::usage_reporting::UsageReportingConfig;
10+
use hive_router_plan_executor::execution::{
11+
client_request_details::ClientRequestDetails, plan::PlanExecutionOutput,
12+
};
13+
use ntex::web::HttpRequest;
14+
use rand::Rng;
15+
use tokio_util::sync::CancellationToken;
16+
17+
use crate::{
18+
background_tasks::{BackgroundTask, BackgroundTasksManager},
19+
consts::ROUTER_VERSION,
20+
};
21+
22+
pub fn init_hive_user_agent(
23+
bg_tasks_manager: &mut BackgroundTasksManager,
24+
usage_config: &UsageReportingConfig,
25+
) -> Result<Arc<UsageAgent>, AgentError> {
26+
let user_agent = format!("hive-router/{}", ROUTER_VERSION);
27+
let hive_user_agent = UsageAgent::try_new(
28+
&usage_config.access_token,
29+
usage_config.endpoint.clone(),
30+
usage_config.target_id.clone(),
31+
usage_config.buffer_size,
32+
usage_config.connect_timeout,
33+
usage_config.request_timeout,
34+
usage_config.accept_invalid_certs,
35+
usage_config.flush_interval,
36+
user_agent,
37+
)?;
38+
bg_tasks_manager.register_task(hive_user_agent.clone());
39+
Ok(hive_user_agent)
40+
}
41+
42+
#[inline]
43+
pub fn collect_usage_report(
44+
schema: Arc<Document<'static, String>>,
45+
duration: Duration,
46+
req: &HttpRequest,
47+
client_request_details: &ClientRequestDetails,
48+
hive_usage_agent: &Arc<UsageAgent>,
49+
usage_config: &UsageReportingConfig,
50+
execution_result: &PlanExecutionOutput,
51+
) {
52+
let mut rng = rand::rng();
53+
let sampled = rng.random::<f64>() < usage_config.sample_rate.as_f64();
54+
if !sampled {
55+
return;
56+
}
57+
if client_request_details
58+
.operation
59+
.name
60+
.is_some_and(|op_name| usage_config.exclude.contains(&op_name.to_string()))
61+
{
62+
return;
63+
}
64+
let client_name = get_header_value(req, &usage_config.client_name_header);
65+
let client_version = get_header_value(req, &usage_config.client_version_header);
66+
let timestamp = SystemTime::now()
67+
.duration_since(UNIX_EPOCH)
68+
.unwrap()
69+
.as_millis() as u64;
70+
let execution_report = ExecutionReport {
71+
schema,
72+
client_name: client_name.map(|s| s.to_owned()),
73+
client_version: client_version.map(|s| s.to_owned()),
74+
timestamp,
75+
duration,
76+
ok: execution_result.error_count == 0,
77+
errors: execution_result.error_count,
78+
operation_body: client_request_details.operation.query.to_owned(),
79+
operation_name: client_request_details
80+
.operation
81+
.name
82+
.map(|op_name| op_name.to_owned()),
83+
persisted_document_hash: None,
84+
};
85+
86+
if let Err(err) = hive_usage_agent.add_report(execution_report) {
87+
tracing::error!("Failed to send usage report: {}", err);
88+
}
89+
}
90+
91+
fn get_header_value<'req>(req: &'req HttpRequest, header_name: &str) -> Option<&'req str> {
92+
req.headers().get(header_name).and_then(|v| v.to_str().ok())
93+
}
94+
95+
#[async_trait]
96+
impl BackgroundTask for UsageAgent {
97+
fn id(&self) -> &str {
98+
"hive_console_usage_report_task"
99+
}
100+
101+
async fn run(&self, token: CancellationToken) {
102+
self.start_flush_interval(Some(token)).await
103+
}
104+
}

bin/router/src/schema_state.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use arc_swap::{ArcSwap, Guard};
22
use async_trait::async_trait;
3+
use graphql_parser::schema::Document;
34
use graphql_tools::validation::utils::ValidationError;
45
use hive_router_config::{supergraph::SupergraphSource, HiveRouterConfig};
56
use hive_router_plan_executor::{
@@ -39,6 +40,7 @@ pub struct SupergraphData {
3940
pub metadata: SchemaMetadata,
4041
pub planner: Planner,
4142
pub subgraph_executor_map: SubgraphExecutorMap,
43+
pub supergraph_schema: Arc<Document<'static, String>>,
4244
}
4345

4446
#[derive(Debug, thiserror::Error)]
@@ -124,6 +126,7 @@ impl SchemaState {
124126
)?;
125127

126128
Ok(SupergraphData {
129+
supergraph_schema: Arc::new(parsed_supergraph_sdl),
127130
metadata,
128131
planner,
129132
subgraph_executor_map,

bin/router/src/shared_state.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use graphql_tools::validation::validate::ValidationPlan;
2+
use hive_console_sdk::agent::UsageAgent;
23
use hive_router_config::HiveRouterConfig;
34
use hive_router_plan_executor::headers::{
45
compile::compile_headers_plan, errors::HeaderRuleCompileError, plan::HeaderRulesPlan,
@@ -68,12 +69,14 @@ pub struct RouterSharedState {
6869
/// but no longer than `exp` date.
6970
pub jwt_claims_cache: JwtClaimsCache,
7071
pub jwt_auth_runtime: Option<JwtAuthRuntime>,
72+
pub hive_usage_agent: Option<Arc<UsageAgent>>,
7173
}
7274

7375
impl RouterSharedState {
7476
pub fn new(
7577
router_config: Arc<HiveRouterConfig>,
7678
jwt_auth_runtime: Option<JwtAuthRuntime>,
79+
hive_usage_agent: Option<Arc<UsageAgent>>,
7780
) -> Result<Self, SharedStateError> {
7881
Ok(Self {
7982
validation_plan: graphql_tools::validation::rules::default_rules_validation_plan(),
@@ -92,6 +95,7 @@ impl RouterSharedState {
9295
)
9396
.map_err(Box::new)?,
9497
jwt_auth_runtime,
98+
hive_usage_agent,
9599
})
96100
}
97101
}
@@ -104,4 +108,6 @@ pub enum SharedStateError {
104108
CORSConfig(#[from] Box<CORSConfigError>),
105109
#[error("invalid override labels config: {0}")]
106110
OverrideLabelsCompile(#[from] Box<OverrideLabelsCompileError>),
111+
#[error("error creating hive usage agent: {0}")]
112+
UsageAgent(#[from] Box<hive_console_sdk::agent::AgentError>),
107113
}

docs/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
|[**query\_planner**](#query_planner)|`object`|Query planning configuration.<br/>Default: `{"allow_expose":false,"timeout":"10s"}`<br/>||
1717
|[**supergraph**](#supergraph)|`object`|Configuration for the Federation supergraph source. By default, the router will use a local file-based supergraph source (`./supergraph.graphql`).<br/>||
1818
|[**traffic\_shaping**](#traffic_shaping)|`object`|Configuration for the traffic-shaper executor. Use these configurations to control how requests are being executed to subgraphs.<br/>Default: `{"dedupe_enabled":true,"max_connections_per_host":100,"pool_idle_timeout_seconds":50}`<br/>||
19+
|[**usage\_reporting**](#usage_reporting)|`object`|Configuration for usage reporting to GraphQL Hive.<br/>Default: `{"accept_invalid_certs":false,"access_token":"","buffer_size":1000,"client_name_header":"graphql-client-name","client_version_header":"graphql-client-version","connect_timeout":"5s","enabled":false,"endpoint":"https://app.graphql-hive.com/usage","exclude":[],"flush_interval":"5s","request_timeout":"15s","sample_rate":"100%","target_id":null}`<br/>|yes|
1920

2021
**Additional Properties:** not allowed
2122
**Example**
@@ -110,6 +111,20 @@ traffic_shaping:
110111
dedupe_enabled: true
111112
max_connections_per_host: 100
112113
pool_idle_timeout_seconds: 50
114+
usage_reporting:
115+
accept_invalid_certs: false
116+
access_token: ''
117+
buffer_size: 1000
118+
client_name_header: graphql-client-name
119+
client_version_header: graphql-client-version
120+
connect_timeout: 5s
121+
enabled: false
122+
endpoint: https://app.graphql-hive.com/usage
123+
exclude: []
124+
flush_interval: 5s
125+
request_timeout: 15s
126+
sample_rate: 100%
127+
target_id: null
113128

114129
```
115130

@@ -1853,4 +1868,58 @@ pool_idle_timeout_seconds: 50
18531868
18541869
```
18551870

1871+
<a name="usage_reporting"></a>
1872+
## usage\_reporting: object
1873+
1874+
Configuration for usage reporting to GraphQL Hive.
1875+
1876+
1877+
**Properties**
1878+
1879+
|Name|Type|Description|Required|
1880+
|----|----|-----------|--------|
1881+
|**accept\_invalid\_certs**|`boolean`|Accepts invalid SSL certificates<br/>Default: false<br/>Default: `false`<br/>|no|
1882+
|**access\_token**|`string`|Your [Registry Access Token](https://the-guild.dev/graphql/hive/docs/management/targets#registry-access-tokens) with write permission.<br/>|yes|
1883+
|**buffer\_size**|`integer`|A maximum number of operations to hold in a buffer before sending to Hive Console<br/>Default: 1000<br/>Default: `1000`<br/>Format: `"uint"`<br/>Minimum: `0`<br/>|no|
1884+
|**client\_name\_header**|`string`|Default: `"graphql-client-name"`<br/>|no|
1885+
|**client\_version\_header**|`string`|Default: `"graphql-client-version"`<br/>|no|
1886+
|**connect\_timeout**|`string`|A timeout for only the connect phase of a request to Hive Console<br/>Default: 5 seconds<br/>Default: `"5s"`<br/>|no|
1887+
|**enabled**|`boolean`|Default: `false`<br/>|no|
1888+
|**endpoint**|`string`|For self-hosting, you can override `/usage` endpoint (defaults to `https://app.graphql-hive.com/usage`).<br/>Default: `"https://app.graphql-hive.com/usage"`<br/>|no|
1889+
|[**exclude**](#usage_reportingexclude)|`string[]`|A list of operations (by name) to be ignored by Hive.<br/>Default: <br/>|no|
1890+
|**flush\_interval**|`string`|Frequency of flushing the buffer to the server<br/>Default: 5 seconds<br/>Default: `"5s"`<br/>|no|
1891+
|**request\_timeout**|`string`|A timeout for the entire request to Hive Console<br/>Default: 15 seconds<br/>Default: `"15s"`<br/>|no|
1892+
|**sample\_rate**|`string`|Sample rate to determine sampling.<br/>0% = never being sent<br/>50% = half of the requests being sent<br/>100% = always being sent<br/>Default: 100%<br/>Default: `"100%"`<br/>|no|
1893+
|**target\_id**|`string`, `null`|A target ID, this can either be a slug following the format “$organizationSlug/$projectSlug/$targetSlug” (e.g “the-guild/graphql-hive/staging”) or an UUID (e.g. “a0f4c605-6541-4350-8cfe-b31f21a4bf80”). To be used when the token is configured with an organization access token.<br/>|no|
1894+
1895+
**Additional Properties:** not allowed
1896+
**Example**
1897+
1898+
```yaml
1899+
accept_invalid_certs: false
1900+
access_token: ''
1901+
buffer_size: 1000
1902+
client_name_header: graphql-client-name
1903+
client_version_header: graphql-client-version
1904+
connect_timeout: 5s
1905+
enabled: false
1906+
endpoint: https://app.graphql-hive.com/usage
1907+
exclude: []
1908+
flush_interval: 5s
1909+
request_timeout: 15s
1910+
sample_rate: 100%
1911+
target_id: null
1912+
1913+
```
1914+
1915+
<a name="usage_reportingexclude"></a>
1916+
### usage\_reporting\.exclude\[\]: array
1917+
1918+
A list of operations (by name) to be ignored by Hive.
1919+
Example: ["IntrospectionQuery", "MeQuery"]
1920+
1921+
1922+
**Items**
1923+
1924+
**Item Type:** `string`
18561925

lib/executor/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ xxhash-rust = { workspace = true }
3030
tokio = { workspace = true, features = ["sync"] }
3131
dashmap = { workspace = true }
3232
vrl = { workspace = true }
33+
regex-automata = { workspace = true }
3334

3435
ahash = "0.8.12"
35-
regex-automata = "0.4.10"
3636
strum = { version = "0.27.2", features = ["derive"] }
3737
ntex-http = "0.1.15"
3838
ordered-float = "4.2.0"

0 commit comments

Comments
 (0)