Skip to content

Commit 24c0998

Browse files
authored
enhance(sdk-rs): use usage report json schema as a source of truth (#7405)
1 parent 4183e55 commit 24c0998

File tree

7 files changed

+158
-76
lines changed

7 files changed

+158
-76
lines changed

.changeset/dark-feet-heal.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'hive-console-sdk-rs': patch
3+
---
4+
5+
Use the JSON Schema specification of the usage reports directly to generate Rust structs as a source
6+
of truth instead of manually written types

configs/cargo/Cargo.lock

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

docker/docker.hcl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ target "apollo-router" {
363363
contexts = {
364364
router_pkg = "${PWD}/packages/libraries/router"
365365
sdk_rs_pkg = "${PWD}/packages/libraries/sdk-rs"
366+
usage_service = "${PWD}/packages/services/usage"
366367
config = "${PWD}/configs/cargo"
367368
}
368369
args = {

docker/router.dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ COPY --from=router_pkg Cargo.toml /usr/src/router/
2222
COPY --from=sdk_rs_pkg Cargo.toml /usr/src/sdk-rs/
2323
COPY --from=config Cargo.lock /usr/src/router/
2424

25+
# Copy usage report schema
26+
# `usage.rs` uses it with the path `../../services/usage/usage-report-v2.schema.json`
27+
# So we need to place it accordingly
28+
COPY --from=usage_service usage-report-v2.schema.json /usr/services/usage/
29+
2530
WORKDIR /usr/src/sdk-rs
2631
# Get the dependencies cached, so we can use dummy input files so Cargo wont fail
2732
RUN echo 'fn main() { println!(""); }' > ./src/main.rs

packages/libraries/sdk-rs/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ serde_json = "1"
3232
moka = { version = "0.12.10", features = ["future", "sync"] }
3333
sha2 = { version = "0.10.8", features = ["std"] }
3434
tokio-util = "0.7.16"
35+
typify = "0.5.0"
36+
regress = "0.10.5"
3537

3638
[dev-dependencies]
3739
mockito = "1.7.0"

packages/libraries/sdk-rs/src/agent.rs

Lines changed: 32 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,14 @@ use graphql_parser::schema::Document;
33
use reqwest::header::{HeaderMap, HeaderValue};
44
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
55
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
6-
use serde::{Deserialize, Serialize};
76
use std::{
8-
collections::{HashMap, VecDeque},
7+
collections::{hash_map::Entry, HashMap, VecDeque},
98
sync::{Arc, Mutex},
109
time::Duration,
1110
};
1211
use thiserror::Error;
1312
use tokio_util::sync::CancellationToken;
1413

15-
#[derive(Serialize, Deserialize, Debug)]
16-
pub struct Report {
17-
size: usize,
18-
map: HashMap<String, OperationMapRecord>,
19-
operations: Vec<Operation>,
20-
}
21-
22-
#[allow(non_snake_case)]
23-
#[derive(Serialize, Deserialize, Debug)]
24-
struct OperationMapRecord {
25-
operation: String,
26-
#[serde(skip_serializing_if = "Option::is_none")]
27-
operationName: Option<String>,
28-
fields: Vec<String>,
29-
}
30-
31-
#[allow(non_snake_case)]
32-
#[derive(Serialize, Deserialize, Debug)]
33-
struct Operation {
34-
operationMapKey: String,
35-
timestamp: u64,
36-
execution: Execution,
37-
#[serde(skip_serializing_if = "Option::is_none")]
38-
metadata: Option<Metadata>,
39-
#[serde(skip_serializing_if = "Option::is_none")]
40-
persistedDocumentHash: Option<String>,
41-
}
42-
43-
#[allow(non_snake_case)]
44-
#[derive(Serialize, Deserialize, Debug)]
45-
struct Execution {
46-
ok: bool,
47-
duration: u128,
48-
errorsTotal: usize,
49-
}
50-
51-
#[derive(Serialize, Deserialize, Debug)]
52-
struct Metadata {
53-
#[serde(skip_serializing_if = "Option::is_none")]
54-
client: Option<ClientInfo>,
55-
}
56-
57-
#[derive(Serialize, Deserialize, Debug)]
58-
struct ClientInfo {
59-
#[serde(skip_serializing_if = "Option::is_none")]
60-
name: Option<String>,
61-
#[serde(skip_serializing_if = "Option::is_none")]
62-
version: Option<String>,
63-
}
64-
6514
#[derive(Debug, Clone)]
6615
pub struct ExecutionReport {
6716
pub schema: Arc<Document<'static, String>>,
@@ -76,6 +25,8 @@ pub struct ExecutionReport {
7625
pub persisted_document_hash: Option<String>,
7726
}
7827

28+
typify::import_types!(schema = "../../services/usage/usage-report-v2.schema.json");
29+
7930
#[derive(Debug, Default)]
8031
pub struct Buffer(Mutex<VecDeque<ExecutionReport>>);
8132

@@ -201,6 +152,7 @@ impl UsageAgent {
201152
size: 0,
202153
map: HashMap::new(),
203154
operations: Vec::new(),
155+
subscription_operations: Vec::new(),
204156
};
205157

206158
// iterate over reports and check if they are valid
@@ -228,30 +180,42 @@ impl UsageAgent {
228180
let metadata: Option<Metadata> =
229181
if client_name.is_some() || client_version.is_some() {
230182
Some(Metadata {
231-
client: Some(ClientInfo {
232-
name: client_name,
233-
version: client_version,
183+
client: Some(Client {
184+
name: client_name.unwrap_or_default(),
185+
version: client_version.unwrap_or_default(),
234186
}),
235187
})
236188
} else {
237189
None
238190
};
239-
report.operations.push(Operation {
240-
operationMapKey: hash.clone(),
191+
report.operations.push(RequestOperation {
192+
operation_map_key: hash.clone(),
241193
timestamp: op.timestamp,
242194
execution: Execution {
243195
ok: op.ok,
244-
duration: op.duration.as_nanos(),
245-
errorsTotal: op.errors,
196+
/*
197+
The conversion from u128 (from op.duration.as_nanos()) to u64 using try_into().unwrap() can panic if the duration is longer than u64::MAX nanoseconds (over 584 years).
198+
While highly unlikely, it's safer to handle this potential overflow gracefully in library code to prevent panics.
199+
A safe alternative is to convert the Result to an Option and provide a fallback value on failure,
200+
effectively saturating at u64::MAX.
201+
*/
202+
duration: op
203+
.duration
204+
.as_nanos()
205+
.try_into()
206+
.ok()
207+
.unwrap_or(u64::MAX),
208+
errors_total: op.errors.try_into().unwrap(),
246209
},
247-
persistedDocumentHash: op.persisted_document_hash,
210+
persisted_document_hash: op
211+
.persisted_document_hash
212+
.map(PersistedDocumentHash),
248213
metadata,
249214
});
250-
if let std::collections::hash_map::Entry::Vacant(e) = report.map.entry(hash)
251-
{
215+
if let Entry::Vacant(e) = report.map.entry(ReportMapKey(hash)) {
252216
e.insert(OperationMapRecord {
253217
operation: operation.operation,
254-
operationName: non_empty_string(op.operation_name),
218+
operation_name: non_empty_string(op.operation_name),
255219
fields: operation.coordinates,
256220
});
257221
}
@@ -401,7 +365,7 @@ mod tests {
401365
let record = report.map.values().next().expect("No operation record");
402366
// operation
403367
assert!(record.operation.contains("mutation deleteProject"));
404-
assert_eq!(record.operationName.as_deref(), Some("deleteProject"));
368+
assert_eq!(record.operation_name.as_deref(), Some("deleteProject"));
405369
// fields
406370
let expected_fields = vec![
407371
"Mutation.deleteProject",
@@ -434,11 +398,11 @@ mod tests {
434398

435399
let operation = &operations[0];
436400
let key = report.map.keys().next().expect("No operation key");
437-
assert_eq!(&operation.operationMapKey, key);
401+
assert_eq!(operation.operation_map_key, key.0);
438402
assert_eq!(operation.timestamp, timestamp);
439-
assert_eq!(operation.execution.duration, duration.as_nanos());
403+
assert_eq!(operation.execution.duration, duration.as_nanos() as u64);
440404
assert_eq!(operation.execution.ok, true);
441-
assert_eq!(operation.execution.errorsTotal, 0);
405+
assert_eq!(operation.execution.errors_total, 0);
442406
true
443407
})
444408
.expect(1)
@@ -545,7 +509,7 @@ mod tests {
545509
operation_name: Some("deleteProject".to_string()),
546510
client_name: Some(GRAPHQL_CLIENT_NAME.to_string()),
547511
client_version: Some(GRAPHQL_CLIENT_VERSION.to_string()),
548-
timestamp,
512+
timestamp: timestamp.try_into().unwrap(),
549513
duration,
550514
ok: true,
551515
errors: 0,

0 commit comments

Comments
 (0)