GraphQL Metrics Extractor
Extract as much much detail as you want from GraphQL queries, served up from your Ruby app and the graphql
gem.
Compatible with the graphql-batch
gem, to extract batch-loaded fields resolution timings.
Installation
Add this line to your application's Gemfile:
gem 'graphql-metrics'
You can require it with in your code as needed with:
require 'graphql_metrics'
Or globally in the Gemfile with:
gem 'graphql-metrics', require: 'graphql_metrics'
And then execute:
$ bundle
Or install it yourself as:
$ gem install graphql-metrics
Usage
You can get started quickly with all features enabled by instrumenting your queries
with an extractor class (defined below) and with TimedBatchExecutor
passed as
a custom executor when initializing GraphQL::Batch
instrumentation if you're using it.
class Schema < GraphQL::Schema
query QueryRoot
mutation MutationRoot
use LoggingExtractor # Replace me with your own subclass of GraphQLMetrics::Extractor!
use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor # Optional.
end
Define your own extractor class, inheriting from GraphQLMetrics::Extractor
, and
implementing the methods below, as needed.
Here's an example of a simple extractor that logs out all GraphQL query details.
class LoggingExtractor < GraphQLMetrics::Instrumentation
def query_extracted(metrics, )
Rails.logger.debug({
query_string: metrics[:query_string], # "query Project { project(name: "GraphQL") { tagline } }"
operation_type: metrics[:operation_type], # "query"
operation_name: metrics[:operation_name], # "Project"
duration: metrics[:duration] # 0.1
})
end
def field_extracted(metrics, )
Rails.logger.debug({
type_name: metrics[:type_name], # "QueryRoot"
field_name: metrics[:field_name], # "project"
deprecated: metrics[:deprecated], # false
resolver_times: metrics[:resolver_times], # [0.1]
})
end
# NOTE: Applicable only if you set `use GraphQL::Batch, executor_class: GraphQLMetrics::TimedBatchExecutor`
# in your schema.
def batch_loaded_field_extracted(metrics, )
Rails.logger.debug({
key: metrics[:key], # "CommentLoader/Comment"
identifiers: metrics[:identifiers], # "Comment/_/string/_/symbol/Class/?"
times: metrics[:times], # [0.1, 0.2, 4]
perform_queue_sizes: metrics[:perform_queue_sizes], # [3]
})
end
def argument_extracted(metrics, )
Rails.logger.debug({
name: metrics[:name], # "post"
type: metrics[:type], # "postInput"
value_is_null: metrics[:value_is_null], # false
default_used: metrics[:default_used], # false
parent_input_type: metrics[:parent_input_type], # "PostInput"
field_name: metrics[:field_name], # "postCreate"
field_base_type: metrics[:field_base_type], # "MutationRoot"
})
end
def variable_extracted(metrics, )
Rails.logger.debug({
operation_name: metrics[:operation_name], # "MyMutation"
unwrapped_type_name: metrics[:unwrapped_type_name], # "PostInput"
type: metrics[:type], # "PostInput!"
default_value_type: metrics[:default_value_type], # "IMPLICIT_NULL"
provided_value: metrics[:provided_value], # false
default_used: metrics[:default_used], # false
used_in_operation: metrics[:used_in_operation], # true
})
end
# Define this if you want to do something with the query just before query logging.
def before_query_extracted(query, query_context)
Rails.logger.debug({
something_from_context: query_context[:something]
})
end
# Return something `truthy` if you want skip query extraction entirely, based on the query or
# for example its context.
def skip_extraction?(_query)
false
end
# Return something `truthy` if you want skip producing field resolution
# timing metrics. Applicable only if `field_extracted` is also defined.
def skip_field_resolution_timing?(_query, )
false
end
# Use or clear state after metrics extraction
def after_query_teardown(_query)
# Use or clear state after metrics extraction, i.e. Flush metrics to Datadog, Kafka etc.
# i.e. kafka.producer.produce('graphql_metrics', @collected_metrics); kafka.producer.deliver_messages
end
end
You can also define ad hoc query Extractors that can work with instances of GraphQL::Query, for example:
class TypeUsageExtractor < GraphQLMetrics::Extractor
attr_reader :types_used
def initialize
@types_used = Set.new
end
def field_extracted(metrics, )
@types_used << metrics[:type_name]
end
end
# ...
extractor = TypeUsageExtractor.new
extractor.extract!(query)
puts extractor.types_used
# => ["Comment", "Post", "QueryRoot"]
Note that resolver-timing related data like duration
in query_extracted
and resolver_times
in field_extracted
won't be available when using an ad hoc Extractor, since the query isn't actually being run; it's only analyzed.
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run bundle exec rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/graphql_metrics. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the GraphQLMetrics project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.