Skip to content

Commit 5c9a3ac

Browse files
committed
feat(router): Hive Console Usage Reporting
1 parent bee241d commit 5c9a3ac

File tree

12 files changed

+332
-12
lines changed

12 files changed

+332
-12
lines changed

Cargo.lock

Lines changed: 51 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ lto = true
2626
codegen-units = 1
2727

2828
[workspace.dependencies]
29-
graphql-tools = "0.4.0"
30-
graphql-parser = "0.4.1"
29+
graphql-parser = { version = "0.5.0", package = "graphql-parser-hive-fork" }
30+
graphql-tools = { version = "0.4.0", features = [
31+
"graphql_parser_fork",
32+
], default-features = false }
3133
serde = { version = "1.0.219", features = ["derive"] }
3234
serde_json = "1.0.142"
3335
sonic-rs = "0.5.3"

bin/router/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ path = "src/main.rs"
1919
hive-router-query-planner = { path = "../../lib/query-planner", version = "2.0.2" }
2020
hive-router-plan-executor = { path = "../../lib/executor", version = "6.0.0" }
2121
hive-router-config = { path = "../../lib/router-config", version = "0.0.10" }
22+
hive-console-sdk = { path = "../../../graphql-hive/packages/libraries/sdk-rs" }
2223

2324
tokio = { workspace = true }
2425
futures = { workspace = true }

bin/router/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,21 @@ pub async fn configure_app_from_config(
110110
Some(jwt_config) => Some(JwtAuthRuntime::init(bg_tasks_manager, jwt_config).await?),
111111
None => None,
112112
};
113+
let usage_agent = pipeline::usage_reporting::from_config(&router_config).map(Arc::new);
114+
115+
if let Some(usage_agent) = &usage_agent {
116+
bg_tasks_manager.register_task(usage_agent.clone());
117+
}
113118

114119
let router_config_arc = Arc::new(router_config);
115120
let schema_state =
116121
SchemaState::new_from_config(bg_tasks_manager, router_config_arc.clone()).await?;
117122
let schema_state_arc = Arc::new(schema_state);
118-
let shared_state = Arc::new(RouterSharedState::new(router_config_arc, jwt_runtime)?);
123+
let shared_state = Arc::new(RouterSharedState::new(
124+
router_config_arc,
125+
jwt_runtime,
126+
usage_agent,
127+
)?);
119128

120129
Ok((shared_state, schema_state_arc))
121130
}

bin/router/src/pipeline/mod.rs

Lines changed: 21 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::plan::{
44
ClientRequestDetails, OperationDetails, PlanExecutionOutput,
@@ -44,6 +44,7 @@ pub mod normalize;
4444
pub mod parser;
4545
pub mod progressive_override;
4646
pub mod query_plan;
47+
pub mod usage_reporting;
4748
pub mod validation;
4849

4950
static GRAPHIQL_HTML: &str = include_str!("../../static/graphiql.html");
@@ -108,6 +109,7 @@ pub async fn execute_pipeline(
108109
shared_state: &Arc<RouterSharedState>,
109110
schema_state: &Arc<SchemaState>,
110111
) -> Result<PlanExecutionOutput, PipelineError> {
112+
let start = Instant::now();
111113
perform_csrf_prevention(req, &shared_state.router_config.csrf)?;
112114

113115
let mut execution_request = get_execution_request(req, body_bytes).await?;
@@ -172,5 +174,23 @@ pub async fn execute_pipeline(
172174
)
173175
.await?;
174176

177+
shared_state.usage_agent.as_ref().and_then(|usage_agent| {
178+
shared_state
179+
.router_config
180+
.usage_reporting
181+
.as_ref()
182+
.map(|usage_config| {
183+
usage_reporting::send_usage_report(
184+
supergraph.schema.clone(),
185+
start,
186+
req,
187+
&client_request_details,
188+
usage_agent,
189+
usage_config,
190+
&execution_result,
191+
)
192+
})
193+
});
194+
175195
Ok(execution_result)
176196
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::{
2+
sync::Arc,
3+
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
4+
};
5+
6+
use async_trait::async_trait;
7+
use graphql_parser::schema::Document;
8+
use hive_console_sdk::agent::{ExecutionReport, UsageAgent};
9+
use hive_router_config::{usage_reporting::UsageReportingConfig, HiveRouterConfig};
10+
use hive_router_plan_executor::execution::plan::{ClientRequestDetails, PlanExecutionOutput};
11+
use ntex::web::HttpRequest;
12+
use rand::Rng;
13+
use tokio_util::sync::CancellationToken;
14+
15+
use crate::background_tasks::BackgroundTask;
16+
17+
pub fn from_config(router_config: &HiveRouterConfig) -> Option<UsageAgent> {
18+
router_config.usage_reporting.as_ref().map(|usage_config| {
19+
let flush_interval = Duration::from_secs(usage_config.flush_interval);
20+
hive_console_sdk::agent::UsageAgent::new(
21+
usage_config.token.clone(),
22+
usage_config.endpoint.clone(),
23+
usage_config.target_id.clone(),
24+
usage_config.buffer_size,
25+
usage_config.connect_timeout,
26+
usage_config.request_timeout,
27+
usage_config.accept_invalid_certs,
28+
flush_interval,
29+
"hive-router".to_string(),
30+
)
31+
})
32+
}
33+
34+
pub fn send_usage_report(
35+
schema: Arc<Document<'static, String>>,
36+
start: Instant,
37+
req: &HttpRequest,
38+
client_request_details: &ClientRequestDetails,
39+
usage_agent: &UsageAgent,
40+
usage_config: &UsageReportingConfig,
41+
execution_result: &PlanExecutionOutput,
42+
) {
43+
let mut rng = rand::rng();
44+
let sampled = rng.random::<f64>() < usage_config.sample_rate;
45+
if !sampled {
46+
return;
47+
}
48+
if client_request_details
49+
.operation
50+
.name
51+
.is_some_and(|op_name| usage_config.exclude.contains(&op_name.to_string()))
52+
{
53+
return;
54+
}
55+
let client_name = get_header_value(req, &usage_config.client_name_header);
56+
let client_version = get_header_value(req, &usage_config.client_version_header);
57+
let timestamp = SystemTime::now()
58+
.duration_since(UNIX_EPOCH)
59+
.unwrap()
60+
.as_secs()
61+
* 1000;
62+
let duration = start.elapsed();
63+
let execution_report = ExecutionReport {
64+
schema,
65+
client_name,
66+
client_version,
67+
timestamp,
68+
duration,
69+
ok: execution_result.error_count == 0,
70+
errors: execution_result.error_count,
71+
operation_body: client_request_details.operation.query.to_owned(),
72+
operation_name: client_request_details
73+
.operation
74+
.name
75+
.map(|op_name| op_name.to_owned()),
76+
persisted_document_hash: None,
77+
};
78+
usage_agent
79+
.add_report(execution_report)
80+
.unwrap_or_else(|err| tracing::error!("Failed to send usage report: {}", err));
81+
}
82+
83+
fn get_header_value(req: &HttpRequest, header_name: &str) -> Option<String> {
84+
req.headers()
85+
.get(header_name)
86+
.and_then(|v| v.to_str().ok())
87+
.map(|s| s.to_string())
88+
}
89+
90+
#[async_trait]
91+
impl BackgroundTask for UsageAgent {
92+
fn id(&self) -> &str {
93+
"usage_report_flush_interval"
94+
}
95+
96+
async fn run(&self, token: CancellationToken) {
97+
self.start_flush_interval(Some(token)).await
98+
}
99+
}

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 schema: Arc<Document<'static, String>>,
4244
}
4345

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

126128
Ok(SupergraphData {
129+
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,
@@ -18,12 +19,14 @@ pub struct RouterSharedState {
1819
pub override_labels_evaluator: OverrideLabelsEvaluator,
1920
pub cors_runtime: Option<Cors>,
2021
pub jwt_auth_runtime: Option<JwtAuthRuntime>,
22+
pub usage_agent: Option<Arc<UsageAgent>>,
2123
}
2224

2325
impl RouterSharedState {
2426
pub fn new(
2527
router_config: Arc<HiveRouterConfig>,
2628
jwt_auth_runtime: Option<JwtAuthRuntime>,
29+
usage_agent: Option<Arc<UsageAgent>>,
2730
) -> Result<Self, SharedStateError> {
2831
Ok(Self {
2932
validation_plan: graphql_tools::validation::rules::default_rules_validation_plan(),
@@ -36,6 +39,7 @@ impl RouterSharedState {
3639
)
3740
.map_err(Box::new)?,
3841
jwt_auth_runtime,
42+
usage_agent,
3943
})
4044
}
4145
}
@@ -48,4 +52,6 @@ pub enum SharedStateError {
4852
CORSConfig(#[from] Box<CORSConfigError>),
4953
#[error("invalid override labels config: {0}")]
5054
OverrideLabelsCompile(#[from] Box<OverrideLabelsCompileError>),
55+
#[error("error creating usage agent: {0}")]
56+
UsageAgent(#[from] Box<hive_console_sdk::agent::AgentError>),
5157
}

0 commit comments

Comments
 (0)