apollographql / router

A configurable, high-performance routing runtime for Apollo Federation 🚀
https://www.apollographql.com/docs/router/
Other
807 stars 272 forks source link

Added getters for fields of the apollo_router::services::execution::QueryPlan struct #2707

Open EXPEylazzari opened 1 year ago

EXPEylazzari commented 1 year ago

Is your feature request related to a problem? No. It's really more of an improvement, but I filed it as a feature request given this requires code changes. Not sure if this was the right thing to do.

For local development, when designing a schema, and even after in production, it is useful to be able to quickly see the execution plan of a query that is being made against the router.

At this time, the QueryPlan struct derives the Debug trait (see here), so from a custom plugin, we can do something like:

fn execution_service(&self, service: execution::BoxService) -> execution::BoxService {
  ServiceBuilder::new()
    .map_request(move |request: execution::Request| {
      let query_plan = request.query_plan.as_ref();
      tracing::info!("Executing supergraph query plan {:?}", query_plan);
      request
    })
    .service(service)
    .boxed()
}

which generates something like this (using the sample supergraph schema from the Apollo Router doc):

2023-03-02T14:18:40.145288Z   INFO [trace_id=be3e9a03c6b5af2dbaf7530c64b47453] Executing supergraph query plan QueryPlan { usage_reporting: UsageReporting { stats_report_key: "# -\n{me{id name reviews{id}}}", referenced_fields_by_type: {"Query": ReferencedFieldsForType { field_names: ["me"], is_interface: false }, "User": ReferencedFieldsForType { field_names: ["id", "name", "reviews"], is_interface: false }, "Review": ReferencedFieldsForType { field_names: ["id"], is_interface: false }} }, root: Sequence { nodes: [Fetch(FetchNode { service_name: "accounts", requires: [], variable_usages: [], operation: "{me{__typename id name}}", operation_name: None, operation_kind: Query, id: None, input_rewrites: None, output_rewrites: None }), Flatten(FlattenNode { path: Path([Key("me")]), node: Fetch(FetchNode { service_name: "reviews", requires: [InlineFragment(InlineFragment { type_condition: Some("User"), selections: [Field(Field { alias: None, name: "__typename", selections: None }), Field(Field { alias: None, name: "id", selections: None })] })], variable_usages: [], operation: "query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{id}}}}", operation_name: None, operation_kind: Query, id: None, input_rewrites: None, output_rewrites: None }) })] }, formatted_query_plan: Some("QueryPlan {\n  Sequence {\n    Fetch(service: \"accounts\") {\n      {\n        me {\n          __typename\n          id\n          name\n        }\n      }\n    },\n    Flatten(path: \"me\") {\n      Fetch(service: \"reviews\") {\n        {\n          ... on User {\n            __typename\n            id\n          }\n        } =>\n        {\n          ... on User {\n            reviews {\n              id\n            }\n          }\n        }\n      },\n    },\n  },\n}"), query: Query:
  string: "query {\n\tme {\n\t\tid\n\t\tname\n\t\treviews {\n\t\t\tid\n\t\t}\n\t}\n\t\n}"
  fragments:
  operations:
    - name: None
      kind: Query
      selection_set: [
    Field {
        name: "me",
        alias: None,
        selection_set: Some(
            [
                Field {
                    name: "id",
                    alias: None,
                    selection_set: None,
                    field_type: NonNull(
                        Id,
                    ),
                    skip: No,
                    include: Yes,
                },
                Field {
                    name: "name",
                    alias: None,
                    selection_set: None,
                    field_type: String,
                    skip: No,
                    include: Yes,
                },
                Field {
                    name: "reviews",
                    alias: None,
                    selection_set: Some(
                        [
                            Field {
                                name: "id",
                                alias: None,
                                selection_set: None,
                                field_type: NonNull(
                                    Id,
                                ),
                                skip: No,
                                include: Yes,
                            },
                        ],
                    ),
                    field_type: List(
                        Named(
                            "Review",
                        ),
                    ),
                    skip: No,
                    include: Yes,
                },
            ],
        ),
        field_type: Named(
            "User",
        ),
        skip: No,
        include: Yes,
    },
]
      variables:
  subselections: {}
 }

or the equivalent log in JSON format:

{"timestamp":"2023-03-02T14:30:32.710987Z","level":"INFO","message":"Executing supergraph query plan QueryPlan { usage_reporting: UsageReporting { stats_report_key: \"# -\\n{me{id name reviews{id}}}\", referenced_fields_by_type: {\"Review\": ReferencedFieldsForType { field_names: [\"id\"], is_interface: false }, \"Query\": ReferencedFieldsForType { field_names: [\"me\"], is_interface: false }, \"User\": ReferencedFieldsForType { field_names: [\"id\", \"name\", \"reviews\"], is_interface: false }} }, root: Sequence { nodes: [Fetch(FetchNode { service_name: \"accounts\", requires: [], variable_usages: [], operation: \"{me{__typename id name}}\", operation_name: None, operation_kind: Query, id: None, input_rewrites: None, output_rewrites: None }), Flatten(FlattenNode { path: Path([Key(\"me\")]), node: Fetch(FetchNode { service_name: \"reviews\", requires: [InlineFragment(InlineFragment { type_condition: Some(\"User\"), selections: [Field(Field { alias: None, name: \"__typename\", selections: None }), Field(Field { alias: None, name: \"id\", selections: None })] })], variable_usages: [], operation: \"query($representations:[_Any!]!){_entities(representations:$representations){...on User{reviews{id}}}}\", operation_name: None, operation_kind: Query, id: None, input_rewrites: None, output_rewrites: None }) })] }, formatted_query_plan: Some(\"QueryPlan {\\n  Sequence {\\n    Fetch(service: \\\"accounts\\\") {\\n      {\\n        me {\\n          __typename\\n          id\\n          name\\n        }\\n      }\\n    },\\n    Flatten(path: \\\"me\\\") {\\n      Fetch(service: \\\"reviews\\\") {\\n        {\\n          ... on User {\\n            __typename\\n            id\\n          }\\n        } =>\\n        {\\n          ... on User {\\n            reviews {\\n              id\\n            }\\n          }\\n        }\\n      },\\n    },\\n  },\\n}\"), query: Query:\n  string: \"query {\\n\\tme {\\n\\t\\tid\\n\\t\\tname\\n\\t\\treviews {\\n\\t\\t\\tid\\n\\t\\t}\\n\\t}\\n\\t\\n}\"\n  fragments:\n  operations:\n    - name: None\n      kind: Query\n      selection_set: [\n    Field {\n        name: \"me\",\n        alias: None,\n        selection_set: Some(\n            [\n                Field {\n                    name: \"id\",\n                    alias: None,\n                    selection_set: None,\n                    field_type: NonNull(\n                        Id,\n                    ),\n                    skip: No,\n                    include: Yes,\n                },\n                Field {\n                    name: \"name\",\n                    alias: None,\n                    selection_set: None,\n                    field_type: String,\n                    skip: No,\n                    include: Yes,\n                },\n                Field {\n                    name: \"reviews\",\n                    alias: None,\n                    selection_set: Some(\n                        [\n                            Field {\n                                name: \"id\",\n                                alias: None,\n                                selection_set: None,\n                                field_type: NonNull(\n                                    Id,\n                                ),\n                                skip: No,\n                                include: Yes,\n                            },\n                        ],\n                    ),\n                    field_type: List(\n                        Named(\n                            \"Review\",\n                        ),\n                    ),\n                    skip: No,\n                    include: Yes,\n                },\n            ],\n        ),\n        field_type: Named(\n            \"User\",\n        ),\n        skip: No,\n        include: Yes,\n    },\n]\n      variables:\n  subselections: {}\n }","span":{"name":"my_custom_span"},"spans":[{"http.flavor":"HTTP/1.1","http.method":"POST","http.route":"/","otel.kind":"SERVER","name":"request"},{"apollo_private.http.request_headers":"{}","client.name":"","client.version":"","http.flavor":"HTTP/1.1","http.method":"POST","http.route":"/","otel.kind":"INTERNAL","trace_id":"37b85de01f9a8156f4731d462fff7f41","name":"router"},{"name":"my_custom_span"}]}

As you can see, it's quite chatty.

The formatted_query_plan field of the QueryPlan is logged in the above, but it would be nice to be able to only log that one field. Unfortunately, it is declared pub(crate), like all the other fields of the struct (see here). So there's simply no way for custom plugin code to access it.

Describe the solution you'd like It would be nice to simply add getter for the individual fields of the QueryPlan struct, such that plugin code can use them and log them as they see fit. That is, return them in such a way that we can't use the getter to then mutate the internal state of the QueryPlan. It is strictly for read purposes.

Describe alternatives you've considered None, given the fields are private, we can't access them from plugin code.

Additional context None

utay commented 1 year ago

Hi, I was about to open a similar issue :) My use case is to analyze the query plan and forbid some queries depending on it because subgraphs are located in different regions. It would be ideal if the QueryPlan root node and the PlanNode enum were public or had getters

dgrijalva commented 1 year ago

I would also like to see this happen. My use case is to produce some instrumentation around query behaviors, such as looking at the depth and total number of calls a query expands to.

As a workaround, I think I can "serialize" the plan into something like serde_json::Value, then re-hydrate it back into something useful. A simple getter for the root node (&PlanNode) would be far nicer, both for efficiency and correctness.

This should allow a lot of custom behavior without allowing the user to accidentally mess up the query plan, as which appears to be the reasoning for #1558.