mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
enhance(sdk-rs): use usage report json schema as a source of truth (#7405)
This commit is contained in:
parent
4183e55198
commit
24c099818e
7 changed files with 158 additions and 76 deletions
6
.changeset/dark-feet-heal.md
Normal file
6
.changeset/dark-feet-heal.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'hive-console-sdk-rs': patch
|
||||
---
|
||||
|
||||
Use the JSON Schema specification of the usage reports directly to generate Rust structs as a source
|
||||
of truth instead of manually written types
|
||||
105
configs/cargo/Cargo.lock
generated
105
configs/cargo/Cargo.lock
generated
|
|
@ -2619,6 +2619,7 @@ dependencies = [
|
|||
"md5",
|
||||
"mockito",
|
||||
"moka",
|
||||
"regress",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"reqwest-retry 0.8.0",
|
||||
|
|
@ -2629,6 +2630,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"typify",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4438,7 +4440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap 0.10.1",
|
||||
"once_cell",
|
||||
|
|
@ -4458,7 +4460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
|
|
@ -4760,6 +4762,16 @@ version = "0.8.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "regress"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.24"
|
||||
|
|
@ -5124,6 +5136,18 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.9.0"
|
||||
|
|
@ -5144,12 +5168,24 @@ checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
|
|||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"schemars_derive 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.0.4"
|
||||
|
|
@ -5196,6 +5232,10 @@ name = "semver"
|
|||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
|
|
@ -5300,6 +5340,18 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_tokenstream"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
|
|
@ -6266,6 +6318,53 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typify"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5bcc6f62eb1fa8aa4098f39b29f93dcb914e17158b76c50360911257aa629"
|
||||
dependencies = [
|
||||
"typify-impl",
|
||||
"typify-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typify-impl"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1eb359f7ffa4f9ebe947fa11a1b2da054564502968db5f317b7e37693cb2240"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regress",
|
||||
"schemars 0.8.22",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn 2.0.108",
|
||||
"thiserror 2.0.17",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typify-macro"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "911c32f3c8514b048c1b228361bebb5e6d73aeec01696e8cc0e82e2ffef8ab7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"schemars 0.8.22",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_tokenstream",
|
||||
"syn 2.0.108",
|
||||
"typify-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
|
|
|
|||
|
|
@ -363,6 +363,7 @@ target "apollo-router" {
|
|||
contexts = {
|
||||
router_pkg = "${PWD}/packages/libraries/router"
|
||||
sdk_rs_pkg = "${PWD}/packages/libraries/sdk-rs"
|
||||
usage_service = "${PWD}/packages/services/usage"
|
||||
config = "${PWD}/configs/cargo"
|
||||
}
|
||||
args = {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ COPY --from=router_pkg Cargo.toml /usr/src/router/
|
|||
COPY --from=sdk_rs_pkg Cargo.toml /usr/src/sdk-rs/
|
||||
COPY --from=config Cargo.lock /usr/src/router/
|
||||
|
||||
# Copy usage report schema
|
||||
# `usage.rs` uses it with the path `../../services/usage/usage-report-v2.schema.json`
|
||||
# So we need to place it accordingly
|
||||
COPY --from=usage_service usage-report-v2.schema.json /usr/services/usage/
|
||||
|
||||
WORKDIR /usr/src/sdk-rs
|
||||
# Get the dependencies cached, so we can use dummy input files so Cargo wont fail
|
||||
RUN echo 'fn main() { println!(""); }' > ./src/main.rs
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ serde_json = "1"
|
|||
moka = { version = "0.12.10", features = ["future", "sync"] }
|
||||
sha2 = { version = "0.10.8", features = ["std"] }
|
||||
tokio-util = "0.7.16"
|
||||
typify = "0.5.0"
|
||||
regress = "0.10.5"
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "1.7.0"
|
||||
|
|
|
|||
|
|
@ -3,65 +3,14 @@ use graphql_parser::schema::Document;
|
|||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
collections::{hash_map::Entry, HashMap, VecDeque},
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Report {
|
||||
size: usize,
|
||||
map: HashMap<String, OperationMapRecord>,
|
||||
operations: Vec<Operation>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct OperationMapRecord {
|
||||
operation: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
operationName: Option<String>,
|
||||
fields: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Operation {
|
||||
operationMapKey: String,
|
||||
timestamp: u64,
|
||||
execution: Execution,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
metadata: Option<Metadata>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
persistedDocumentHash: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Execution {
|
||||
ok: bool,
|
||||
duration: u128,
|
||||
errorsTotal: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Metadata {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
client: Option<ClientInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct ClientInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutionReport {
|
||||
pub schema: Arc<Document<'static, String>>,
|
||||
|
|
@ -76,6 +25,8 @@ pub struct ExecutionReport {
|
|||
pub persisted_document_hash: Option<String>,
|
||||
}
|
||||
|
||||
typify::import_types!(schema = "../../services/usage/usage-report-v2.schema.json");
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Buffer(Mutex<VecDeque<ExecutionReport>>);
|
||||
|
||||
|
|
@ -201,6 +152,7 @@ impl UsageAgent {
|
|||
size: 0,
|
||||
map: HashMap::new(),
|
||||
operations: Vec::new(),
|
||||
subscription_operations: Vec::new(),
|
||||
};
|
||||
|
||||
// iterate over reports and check if they are valid
|
||||
|
|
@ -228,30 +180,42 @@ impl UsageAgent {
|
|||
let metadata: Option<Metadata> =
|
||||
if client_name.is_some() || client_version.is_some() {
|
||||
Some(Metadata {
|
||||
client: Some(ClientInfo {
|
||||
name: client_name,
|
||||
version: client_version,
|
||||
client: Some(Client {
|
||||
name: client_name.unwrap_or_default(),
|
||||
version: client_version.unwrap_or_default(),
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
report.operations.push(Operation {
|
||||
operationMapKey: hash.clone(),
|
||||
report.operations.push(RequestOperation {
|
||||
operation_map_key: hash.clone(),
|
||||
timestamp: op.timestamp,
|
||||
execution: Execution {
|
||||
ok: op.ok,
|
||||
duration: op.duration.as_nanos(),
|
||||
errorsTotal: op.errors,
|
||||
/*
|
||||
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).
|
||||
While highly unlikely, it's safer to handle this potential overflow gracefully in library code to prevent panics.
|
||||
A safe alternative is to convert the Result to an Option and provide a fallback value on failure,
|
||||
effectively saturating at u64::MAX.
|
||||
*/
|
||||
duration: op
|
||||
.duration
|
||||
.as_nanos()
|
||||
.try_into()
|
||||
.ok()
|
||||
.unwrap_or(u64::MAX),
|
||||
errors_total: op.errors.try_into().unwrap(),
|
||||
},
|
||||
persistedDocumentHash: op.persisted_document_hash,
|
||||
persisted_document_hash: op
|
||||
.persisted_document_hash
|
||||
.map(PersistedDocumentHash),
|
||||
metadata,
|
||||
});
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = report.map.entry(hash)
|
||||
{
|
||||
if let Entry::Vacant(e) = report.map.entry(ReportMapKey(hash)) {
|
||||
e.insert(OperationMapRecord {
|
||||
operation: operation.operation,
|
||||
operationName: non_empty_string(op.operation_name),
|
||||
operation_name: non_empty_string(op.operation_name),
|
||||
fields: operation.coordinates,
|
||||
});
|
||||
}
|
||||
|
|
@ -401,7 +365,7 @@ mod tests {
|
|||
let record = report.map.values().next().expect("No operation record");
|
||||
// operation
|
||||
assert!(record.operation.contains("mutation deleteProject"));
|
||||
assert_eq!(record.operationName.as_deref(), Some("deleteProject"));
|
||||
assert_eq!(record.operation_name.as_deref(), Some("deleteProject"));
|
||||
// fields
|
||||
let expected_fields = vec![
|
||||
"Mutation.deleteProject",
|
||||
|
|
@ -434,11 +398,11 @@ mod tests {
|
|||
|
||||
let operation = &operations[0];
|
||||
let key = report.map.keys().next().expect("No operation key");
|
||||
assert_eq!(&operation.operationMapKey, key);
|
||||
assert_eq!(operation.operation_map_key, key.0);
|
||||
assert_eq!(operation.timestamp, timestamp);
|
||||
assert_eq!(operation.execution.duration, duration.as_nanos());
|
||||
assert_eq!(operation.execution.duration, duration.as_nanos() as u64);
|
||||
assert_eq!(operation.execution.ok, true);
|
||||
assert_eq!(operation.execution.errorsTotal, 0);
|
||||
assert_eq!(operation.execution.errors_total, 0);
|
||||
true
|
||||
})
|
||||
.expect(1)
|
||||
|
|
@ -545,7 +509,7 @@ mod tests {
|
|||
operation_name: Some("deleteProject".to_string()),
|
||||
client_name: Some(GRAPHQL_CLIENT_NAME.to_string()),
|
||||
client_version: Some(GRAPHQL_CLIENT_VERSION.to_string()),
|
||||
timestamp,
|
||||
timestamp: timestamp.try_into().unwrap(),
|
||||
duration,
|
||||
ok: true,
|
||||
errors: 0,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"size": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"map": {
|
||||
"type": "object",
|
||||
|
|
@ -41,7 +42,8 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"operationMapKey": {
|
||||
"type": "string"
|
||||
|
|
@ -55,10 +57,12 @@
|
|||
"type": "boolean"
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"errorsTotal": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["ok", "duration", "errorsTotal"]
|
||||
|
|
@ -101,7 +105,8 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"operationMapKey": {
|
||||
"type": "string"
|
||||
|
|
|
|||
Loading…
Reference in a new issue