fix(graphql): fix and improuve query parsing for logs

This commit is contained in:
Paul Chavard 2021-11-11 10:31:01 +01:00
parent 71c54a226d
commit 758e7d68e6
3 changed files with 124 additions and 34 deletions

View file

@ -1,4 +1,6 @@
class API::V2::GraphqlController < API::V2::BaseController
include GraphqlOperationLogConcern
def execute
variables = ensure_hash(params[:variables])
@ -24,43 +26,10 @@ class API::V2::GraphqlController < API::V2::BaseController
super
payload.merge!({
graphql_operation: operation_log(params[:query], params[:operationName], params[:variables])
graphql_operation: operation_log(params[:query], params[:operationName], params[:variables]&.to_unsafe_h)
})
end
def operation_log(query, operation_name, variables)
return "NoQuery" if query.nil?
operation = GraphQL.parse(query).children.find do |node|
if node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
node.name == operation_name
end
end
return "InvalidQuery" if operation.nil?
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
message = operation.operation_type
if operation.name
message += ": #{operation.name} { "
end
message += operation.selections.map(&:name).join(', ')
message += " }"
if variables.present?
message += " "
message += variables.to_unsafe_h.flat_map do |(name, value)|
if name == "input"
value.map do |(name, value)|
"#{name}: \"#{value.to_s.truncate(10)}\""
end
else
"#{name}: \"#{value.to_s.truncate(10)}\""
end
end.join(', ')
end
message
end
def process_action(*args)
super
rescue ActionDispatch::Http::Parameters::ParseError => exception

View file

@ -0,0 +1,62 @@
module GraphqlOperationLogConcern
extend ActiveSupport::Concern
# This method parses GraphQL query and creates a short description of the query. It is useful for logging.
def operation_log(query, operation_name, variables)
return "NoQuery" if query.nil?
operation = parse_graphql_query(query, operation_name)
return "InvalidQuery" if operation.nil?
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
message = "#{operation.operation_type}: "
message += if operation.name.present?
"#{operation.name} { "
else
"{ "
end
message += operation.selections.map(&:name).join(', ')
message += " } "
message += if variables.present?
variables.flat_map do |(name, value)|
format_graphql_variable(name, value)
end
else
operation.selections.flat_map(&:arguments).flat_map do |argument|
format_graphql_variable(argument.name, argument.value)
end
end.join(', ')
message.strip
end
private
def parse_graphql_query(query, operation_name)
operations = GraphQL.parse(query).children.filter do |node|
node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
end
if operations.size == 1
operations.first
else
operations.find { |node| node.name == operation_name }
end
rescue
nil
end
def format_graphql_variable(name, value)
if value.is_a?(Hash)
value.map do |(name, value)|
format_graphql_variable(name, value)
end
elsif value.is_a?(GraphQL::Language::Nodes::InputObject)
value.arguments.map do |argument|
format_graphql_variable(argument.name, argument.value)
end
else
"#{name}: \"#{value.to_s.truncate(10)}\""
end
end
end

View file

@ -0,0 +1,59 @@
RSpec.describe GraphqlOperationLogConcern, type: :controller do
class TestController < ActionController::Base
include GraphqlOperationLogConcern
end
controller TestController do
end
describe '#operation_log' do
let(:query) { nil }
let(:variables) { nil }
let(:operation_name) { nil }
subject { controller.operation_log(query, operation_name, variables) }
context 'with no query' do
it { expect(subject).to eq('NoQuery') }
end
context 'with invalid query' do
let(:query) { 'query { demarche {} }' }
it { expect(subject).to eq('InvalidQuery') }
end
context 'with two queries' do
let(:query) { 'query demarche { demarche } query dossier { dossier }' }
let(:operation_name) { 'dossier' }
it { expect(subject).to eq('query: dossier { dossier }') }
end
context 'with arguments' do
let(:query) { 'query demarche { demarche(number: 123) { id } }' }
it { expect(subject).to eq('query: demarche { demarche } number: "123"') }
end
context 'with variables' do
let(:query) { 'query { demarche(number: 123) { id } }' }
let(:variables) { { number: 124 } }
it { expect(subject).to eq('query: { demarche } number: "124"') }
end
context 'with mutation and arguments' do
let(:query) { 'mutation { passerDossierEnInstruction(input: { number: 123 }) { id } }' }
it { expect(subject).to eq('mutation: { passerDossierEnInstruction } number: "123"') }
end
context 'with mutation and variables' do
let(:query) { 'mutation { passerDossierEnInstruction(input: { number: 123 }) { id } }' }
let(:variables) { { input: { number: 124 } } }
it { expect(subject).to eq('mutation: { passerDossierEnInstruction } number: "124"') }
end
end
end