-
Notifications
You must be signed in to change notification settings - Fork 283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update Apollo usage reporting to include persisted query metrics #7166
Open
bonnici
wants to merge
27
commits into
dev
Choose a base branch
from
njm/P-416/pq-reporting
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
d3daf8b
Add PQ ID to context
bonnici bf3cfcb
Move signature and apollo op id generation into function
bonnici 587e45e
Refactor error handling
bonnici dd86724
Rename functions
bonnici 4ea39c6
PQ reporting and fixes
bonnici d0a1ea8
Lint fix
bonnici 84a6f40
Merge branch 'dev' into njm/P-416/pq-reporting
bonnici 4038ace
Fix test
bonnici adf43bf
Store PQ ID in usagereporting
bonnici 3fc8047
Merge branch 'dev' into njm/P-416/pq-reporting
bonnici 24c66da
Fix lint - include operation name and sig only in EmptyPlan errors
bonnici d8513dc
Fixes
bonnici d372dcd
Add changset
bonnici 9d817b6
Add tests
bonnici a96f8e9
lint fix
bonnici 35a1fa3
Merge branch 'dev' into njm/P-416/pq-reporting
bonnici a31bfbb
PR feedback
bonnici 6ddd998
Add missing dependency
bonnici 097c583
Merge branch 'dev' into njm/P-416/pq-reporting
bonnici 9743b81
Change UsageReporting to enum
bonnici aec51eb
Remove for_error func
bonnici 433e9ec
Fix changeset
bonnici 5a0e4a1
Small fixes
bonnici 6dbb2b4
Updater with_pq_id to always require a pq id
bonnici 2720134
Merge branch 'dev' into njm/P-416/pq-reporting
bonnici 94d1265
Fix for PQ with multiple operation IDs
bonnici 93702d5
More refactoring/fixing of op name/op sig/stats report key stuff
bonnici File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
### Brief but complete sentence that stands on its own ([PR #7166](https://github.com/apollographql/router/pull/7166)) | ||
|
||
Enables reporting of persisted query usage by PQ ID to Apollo. | ||
|
||
By [@bonnici](https://github.com/bonnici) in https://github.com/apollographql/router/pull/7166 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ use apollo_compiler::schema::ExtendedType; | |
use apollo_compiler::validation::Valid; | ||
use serde::Deserialize; | ||
use serde::Serialize; | ||
use sha2::Digest; | ||
|
||
use crate::json_ext::Object; | ||
use crate::json_ext::Value as JsonValue; | ||
|
@@ -165,15 +166,97 @@ impl AddAssign<ReferencedEnums> for AggregatedExtendedReferenceStats { | |
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] | ||
#[serde(rename_all = "camelCase")] | ||
pub(crate) struct UsageReporting { | ||
/// The `stats_report_key` is a unique identifier derived from schema and query. | ||
/// Metric data sent to Studio must be aggregated | ||
/// via grouped key of (`client_name`, `client_version`, `stats_report_key`). | ||
pub(crate) stats_report_key: String, | ||
/// The operation name, or None if there is no operation name | ||
pub(crate) operation_name: Option<String>, | ||
/// The normalized operation signature, or None if there is no valid signature | ||
pub(crate) operation_signature: Option<String>, | ||
/// The error key to use for the stats report, or None if there is no error | ||
pub(crate) error_key: Option<String>, | ||
/// The persisted query ID used to request this operation, or None if the query was not requested via PQ ID | ||
pub(crate) pq_id: Option<String>, | ||
/// a list of all types and fields referenced in the query | ||
#[serde(default)] | ||
pub(crate) referenced_fields_by_type: HashMap<String, ReferencedFieldsForType>, | ||
} | ||
|
||
impl UsageReporting { | ||
pub(crate) fn for_error(error_key: String) -> UsageReporting { | ||
UsageReporting { | ||
operation_name: None, | ||
operation_signature: None, | ||
error_key: Some(error_key), | ||
pq_id: None, | ||
referenced_fields_by_type: HashMap::new(), | ||
} | ||
} | ||
bonnici marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
pub(crate) fn with_pq_id(&self, pq_id: Option<String>) -> UsageReporting { | ||
let mut updated_usage_report = self.clone(); | ||
updated_usage_report.pq_id = pq_id; | ||
updated_usage_report | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be a bit cleaner with struct update syntax: pub(crate) fn with_pq_id(&self, pq_id: Option<String>) -> UsageReporting {
UsageReporting {
pq_id,
..self.clone()
}
} |
||
|
||
/// The `stats_report_key` is a unique identifier derived from schema and query. | ||
/// Metric data sent to Studio must be aggregated | ||
/// via grouped key of (`client_name`, `client_version`, `stats_report_key`). | ||
/// For errors, the report key is of the form "## <error name>\n". | ||
/// For operations requested by PQ, the report key is of the form "pq# <pq id>". | ||
/// For operations not requested by PQ, the report key is of the form "# <op name>\n<op sig>". | ||
/// Note that this combination of operation name and signature is sometimes referred to in code as | ||
/// "operation signature" even though it also contains the operation name as the first line. | ||
pub(crate) fn get_stats_report_key(&self) -> String { | ||
if let Some(error_key) = &self.error_key { | ||
format!("## {}\n", error_key) | ||
} else if let Some(persisted_query_id) = &self.pq_id { | ||
format!("pq# {}", persisted_query_id) | ||
} else { | ||
let stats_report_key_op_name = | ||
self.operation_name.as_deref().unwrap_or("-").to_string(); | ||
format!( | ||
"# {}\n{}", | ||
stats_report_key_op_name, | ||
self.get_operation_signature() | ||
) | ||
} | ||
} | ||
|
||
pub(crate) fn get_operation_signature(&self) -> String { | ||
self.operation_signature | ||
.as_deref() | ||
.unwrap_or("") | ||
.to_string() | ||
} | ||
bonnici marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
pub(crate) fn get_operation_id(&self) -> String { | ||
let sig_to_hash = if self.error_key.is_some() { | ||
// To match the logic of Apollo's error key handling, we need to handle error keys specially. | ||
// The correct string to hash in this case is e.g. "# # GraphQLParseFailure\n". | ||
format!("# # {}\n", self.error_key.as_ref().unwrap()) | ||
} else { | ||
self.get_stats_report_key() | ||
}; | ||
bonnici marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let mut hasher = sha1::Sha1::new(); | ||
hasher.update(sig_to_hash.as_bytes()); | ||
let result = hasher.finalize(); | ||
hex::encode(result) | ||
} | ||
|
||
pub(crate) fn get_operation_name(&self) -> String { | ||
if let Some(op_name) = &self.operation_name { | ||
op_name.clone() | ||
} else if let Some(err_key) = &self.error_key { | ||
format!("# {}", err_key) | ||
} else { | ||
"".to_string() | ||
} | ||
} | ||
bonnici marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
pub(crate) fn is_error(&self) -> bool { | ||
self.error_key.is_some() | ||
} | ||
} | ||
|
||
/// A list of fields that will be resolved for a given type | ||
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] | ||
#[serde(rename_all = "camelCase")] | ||
|
@@ -186,9 +269,10 @@ pub(crate) struct ReferencedFieldsForType { | |
pub(crate) is_interface: bool, | ||
} | ||
|
||
/// Generate a UsageReporting containing the stats_report_key (a normalized version of the operation signature) | ||
/// and referenced fields of an operation. The document used to generate the signature and for the references can be | ||
/// different to handle cases where the operation has been filtered, but we want to keep the same signature. | ||
/// Generate a UsageReporting containing the data required to generate a stats_report_key (either a normalized version of | ||
/// the operation signature or an error key or a PQ ID) and referenced fields of an operation. The document used to | ||
/// generate the signature and for the references can be different to handle cases where the operation has been filtered, | ||
/// but we want to keep the same signature. | ||
pub(crate) fn generate_usage_reporting( | ||
signature_doc: &ExecutableDocument, | ||
references_doc: &ExecutableDocument, | ||
|
@@ -362,12 +446,23 @@ struct UsageGenerator<'a> { | |
impl UsageGenerator<'_> { | ||
fn generate_usage_reporting(&mut self) -> UsageReporting { | ||
UsageReporting { | ||
stats_report_key: self.generate_stats_report_key(), | ||
operation_name: self.get_operation_name(), | ||
operation_signature: self.generate_normalized_signature(), | ||
error_key: None, | ||
pq_id: None, | ||
referenced_fields_by_type: self.generate_apollo_reporting_refs(), | ||
} | ||
} | ||
|
||
fn generate_stats_report_key(&mut self) -> String { | ||
fn get_operation_name(&self) -> Option<String> { | ||
self.signature_doc | ||
.operations | ||
.get(self.operation_name.as_deref()) | ||
.ok() | ||
.and_then(|operation| operation.name.as_ref().map(|node| node.to_string())) | ||
} | ||
|
||
fn generate_normalized_signature(&mut self) -> Option<String> { | ||
self.fragments_map.clear(); | ||
|
||
match self | ||
|
@@ -376,10 +471,10 @@ impl UsageGenerator<'_> { | |
.get(self.operation_name.as_deref()) | ||
.ok() | ||
{ | ||
None => "".to_string(), | ||
None => None, | ||
Some(operation) => { | ||
self.extract_signature_fragments(&operation.selection_set); | ||
self.format_operation_for_report(operation) | ||
Some(self.format_operation_signature_for_report(operation)) | ||
} | ||
} | ||
} | ||
|
@@ -410,15 +505,10 @@ impl UsageGenerator<'_> { | |
} | ||
} | ||
|
||
fn format_operation_for_report(&self, operation: &Node<Operation>) -> String { | ||
// The result in the name of the operation | ||
let op_name = match &operation.name { | ||
None => "-".into(), | ||
Some(node) => node.to_string(), | ||
}; | ||
let mut result = format!("# {}\n", op_name); | ||
fn format_operation_signature_for_report(&self, operation: &Node<Operation>) -> String { | ||
let mut result = String::new(); | ||
|
||
// Followed by a sorted list of fragments | ||
// The signature starts with a sorted list of fragments | ||
let mut sorted_fragments: Vec<_> = self.fragments_map.iter().collect(); | ||
sorted_fragments.sort_by_key(|&(k, _)| k); | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be crazy to split this into an enum? I feel like we're already splitting logic a lot here in the
for_error
case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started going down this route but it makes code a bit too complicated. Also there is a possible future where we'll want to be able to have an error that still has referenced fields by type, or maybe an operation name/signature (via query metadata). We'd need to update the reporting code, but if we do that we'd definitely need to change back from an enum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. I'm surprised the code ends up more complex that way. Every time I saw a
for_error()
in your code I was thinking "that'd look nice as aUsageReporting::Error()
instead". And I'd imagine the internal logic is basically just doingmatch
instead of the if/else on what attrs exist in the current code.Understood on the possible future state -- In my head it'd be easy to add those init params if we need them to the
error
enum, but I haven't worked with enums in practice so I'll trust your experience on all this 😄There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had another go and I think I figured it out