Append GraphQL operation name to caller_id
What does this MR do and why?
Today, meta.caller_id for every GraphQL request is GraphqlController#execute, no matter which operation ran. This makes it impossible to tell operations apart in logs, metrics, and downstream systems. For example, production logs show meta.caller_id = GraphqlController#execute while graphql.operation_name = CommitReferences.
The operation name is available today, but only after execution finishes, inside Gitlab::Graphql::Tracers::InstrumentationTracer (an ensure block). It is used there only for the GraphqlLogger line and the SLI metric. While the query is still executing, the active caller_id is the bare GraphqlController#execute. So anything that reads ApplicationContext during execution, including Gitaly calls, sees the generic value and cannot attribute the call to an operation.
This MR adds a before_action, only on :execute, that appends the operation name to caller_id before execution starts, for example GraphqlController#execute:getProjects. The base GraphqlController#execute prefix stays in place, so existing consumers that match on it keep working.
operationName is a client-supplied parameter, so we cannot use it as-is. It ends up as the endpoint_id Prometheus label in lib/gitlab/metrics/requests_rack_middleware.rb (the rails_request SLIs) and is forwarded to Gitaly as gRPC metadata. To keep this safe, we only accept operations already in the KnownOperations allowlist, via a new KnownOperations#from_operation_name lookup. The allowlist is generated at build time from the frontend's GraphQL query files, so values are bounded and character-safe. Any operation not in the allowlist keeps the base GraphqlController#execute.
Multiplex requests have no single operation, so they keep the base caller_id too. The frontend no longer uses multiplex batching.
The change is gated behind the graphql_caller_id_with_operation_name feature flag, which defaults off.
This revives !216756 (closed), which the author closed as redundant. That close was based on the GraphqlLogger line already having the operation name. That reasoning did not hold for the execution path or the request-level caller_id, which is what Gitaly attribution needs.
This is a corrective action from incident INC-13016, where responders could not attribute Gitaly traffic to a GraphQL caller. Tracked in #614721 and production-engineering work item gitlab-com/gl-infra/production-engineering#29554. A follow-up MR, !250312 (merged), forwards caller_id to Gitaly, and a Gitaly MR, gitaly!9092 (merged), logs it there.
Reviewer notes
Metric cardinality. For known GraphQL operations, the endpoint_id label on the rails_request SLI changes from a single GraphqlController#execute value to one value per known operation, for example GraphqlController#execute:getProjects. The allowlist bounds this, so there is no unbounded growth, but it is a change in the label set. Dashboards or alerts that match the exact string GraphqlController#execute for GraphQL will now catch only unknown operations; a prefix match on GraphqlController#execute still catches everything. The feature flag lets us roll this out carefully. This is the concern raised in the original rollout issue.
Attribution is best-effort, not trusted. operationName is declared by the client. The allowlist bounds the set of values, so there is no cardinality or injection risk, but it does not authenticate the caller. A client could define its own operation using an allowlisted name but an arbitrary body, and that request would be attributed under that name. So caller_id here is an observability hint, not a trust signal, and must never be used for authorization or any trust decision. This is the same trust level the existing GraphqlLogger caller_id already has. A caller can only relabel its own traffic, not impersonate another caller. Also, if operationName does not match an operation in the submitted document, the query fails validation and never executes, so no work happens under a mismatched name.
How to test on GDK
- The operation allowlist comes from a frontend build. On a fresh GDK it is often empty, so every operation resolves to
unknown. Check inrails console:Gitlab::Webpack::GraphqlKnownOperations.load.size. - If it is empty, build the frontend so
graphql_known_operations.ymlis produced. If the webpack dev server is enabled, make sure it is running and has finished compiling. Otherwise run a one-off build withyarn webpack. - Once the file exists, clear the memoized values in the console:
Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!thenGitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil). ConfirmGitlab::Webpack::GraphqlKnownOperations.load.sizeis greater than 0. - Enable the flag:
Feature.enable(:graphql_caller_id_with_operation_name). - Make a GraphQL request using an operation name present in the frontend. For example, open a page that issues that query, or send a request to
/api/graphqlwith a matchingoperationName. - Check
log/development.log(orlog/api_json.log) and confirmmeta.caller_idreadsGraphqlController#execute:<operation>instead of the plainGraphqlController#execute. - To check this without a full frontend build, inject a fake allowlist in the console, then look it up:
Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, Gitlab::Graphql::KnownOperations.new({ 'getProjects' => {} }))thenGitlab::Graphql::KnownOperations.default.from_operation_name('getProjects').namereturnsgetProjects.
MR acceptance checklist
Please refer to the MR acceptance checklist for review guidance.