lib/graphql_metrics/extractor.rb in graphql-metrics-1.1.5 vs lib/graphql_metrics/extractor.rb in graphql-metrics-2.0.0

- old
+ new

@@ -1,120 +1,79 @@ # frozen_string_literal: true module GraphQLMetrics class Extractor - CONTEXT_NAMESPACE = :extracted_metrics - TIMING_CACHE_KEY = :timing_cache - START_TIME_KEY = :query_start_time + class DummyInstrumentor + def after_query_start_and_end_time + [nil, nil] + end + def after_query_resolver_times(_ast_node) + [] + end + + def ctx_namespace + {} + end + end + EXPLICIT_NULL = 'EXPLICIT_NULL' IMPLICIT_NULL = 'IMPLICIT_NULL' NON_NULL = 'NON_NULL' - attr_reader :query, :ctx_namespace + attr_reader :query - def self.use(schema_definition) - extractor = self.new - return unless extractor.extractor_defines_any_visitors? - - extractor.setup_instrumentation(schema_definition) + def initialize(instrumentor = DummyInstrumentor.new) + @instrumentor = instrumentor end - def use(schema_definition) - return unless extractor_defines_any_visitors? - setup_instrumentation(schema_definition) + def instrumentor + @instrumentor ||= DummyInstrumentor.new end - def setup_instrumentation(schema_definition) - schema_definition.instrument(:query, self) - schema_definition.instrument(:field, self) - end - - def before_query(query) - return unless extractor_defines_any_visitors? - - ns = query.context.namespace(CONTEXT_NAMESPACE) - ns[TIMING_CACHE_KEY] = {} - ns[START_TIME_KEY] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - rescue StandardError => ex - handle_extraction_exception(ex) - end - - def after_query(query) - return unless extractor_defines_any_visitors? - + def extract!(query) @query = query + return unless query.valid? - return if respond_to?(:skip_extraction?) && skip_extraction?(query) - return unless @ctx_namespace = query.context.namespace(CONTEXT_NAMESPACE) return unless query.irep_selection - before_query_extracted(query, query.context) if respond_to?(:before_query_extracted) extract_query + used_variables = extract_used_variables + query.operations.each_value do |operation| - extract_variables(operation) + extract_variables(operation, used_variables) end extract_node(query.irep_selection) extract_batch_loaders - - after_query_teardown(query) if respond_to?(:after_query_teardown) - rescue StandardError => ex - handle_extraction_exception(ex) end - def instrument(type, field) - return field unless respond_to?(:field_extracted) - return field if type.introspection? - - old_resolve_proc = field.resolve_proc - new_resolve_proc = ->(obj, args, ctx) do - start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - result = old_resolve_proc.call(obj, args, ctx) - - begin - next result if respond_to?(:skip_field_resolution_timing?) && - skip_field_resolution_timing?(query, ctx) - - end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - - ns = ctx.namespace(CONTEXT_NAMESPACE) - - ns[TIMING_CACHE_KEY][ctx.ast_node] ||= [] - ns[TIMING_CACHE_KEY][ctx.ast_node] << end_time - start_time - - result - rescue StandardError => ex - handle_extraction_exception(ex) - result - end + def extractor_defines_any_visitors? + [self, instrumentor].any? do |extractor_definer| + extractor_definer.respond_to?(:query_extracted) || + extractor_definer.respond_to?(:field_extracted) || + extractor_definer.respond_to?(:argument_extracted) || + extractor_definer.respond_to?(:variable_extracted) || + extractor_definer.respond_to?(:batch_loaded_field_extracted) || + extractor_definer.respond_to?(:before_query_extracted) end - - field.redefine { resolve(new_resolve_proc) } end - def extractor_defines_any_visitors? - respond_to?(:query_extracted) || - respond_to?(:field_extracted) || - respond_to?(:argument_extracted) || - respond_to?(:variable_extracted) || - respond_to?(:batch_loaded_field_extracted) || - respond_to?(:before_query_extracted) - end - def handle_extraction_exception(ex) raise ex end + private + def extract_batch_loaders - return unless respond_to?(:batch_loaded_field_extracted) + return unless batch_loaded_field_extracted_method = extraction_method(:batch_loaded_field_extracted) TimedBatchExecutor.timings.each do |key, resolve_meta| key, identifiers = TimedBatchExecutor.serialize_loader_key(key) - batch_loaded_field_extracted( + batch_loaded_field_extracted_method.call( { key: key, identifiers: identifiers, times: resolve_meta[:times], perform_queue_sizes: resolve_meta[:perform_queue_sizes], @@ -129,23 +88,21 @@ ensure TimedBatchExecutor.clear_timings end def extract_query - return unless respond_to?(:query_extracted) + return unless query_extracted_method = extraction_method(:query_extracted) - start_time = ctx_namespace[START_TIME_KEY] - return unless start_time + start_time, end_time = instrumentor.after_query_start_and_end_time + duration = start_time && end_time ? end_time - start_time : nil - end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - - query_extracted( + query_extracted_method.call( { query_string: query.document.to_query_string, operation_type: query.selected_operation.operation_type, operation_name: query.selected_operation_name, - duration: end_time - start_time + duration: duration }, { query: query, start_time: start_time, end_time: end_time @@ -154,35 +111,37 @@ rescue StandardError => ex handle_extraction_exception(ex) end def extract_field(irep_node) - return unless respond_to?(:field_extracted) + return unless field_extracted_method = extraction_method(:field_extracted) return unless irep_node.definition - field_extracted( + resolver_times = instrumentor.after_query_resolver_times(irep_node.ast_node) + + field_extracted_method.call( { type_name: irep_node.owner_type.name, field_name: irep_node.definition.name, deprecated: irep_node.definition.deprecation_reason.present?, - resolver_times: ctx_namespace.dig(TIMING_CACHE_KEY, irep_node.ast_node) || [], + resolver_times: resolver_times || [], }, { irep_node: irep_node, query: query, - ctx_namespace: ctx_namespace + ctx_namespace: instrumentor.ctx_namespace } ) rescue StandardError => ex handle_extraction_exception(ex) end def extract_argument(value, irep_node, types) - return unless respond_to?(:argument_extracted) + return unless argument_extracted_method = extraction_method(:argument_extracted) - argument_extracted( + argument_extracted_method.call( { name: value.definition.expose_as, type: value.definition.type.unwrap.to_s, value_is_null: value.value.nil?, default_used: value.default_used?, @@ -198,12 +157,12 @@ ) rescue StandardError => ex handle_extraction_exception(ex) end - def extract_variables(operation) - return unless respond_to?(:variable_extracted) + def extract_variables(operation, used_variables) + return unless variable_extracted_method = extraction_method(:variable_extracted) operation.variables.each do |variable| value_provided = query.provided_variables.key?(variable.name) default_value_type = case variable.default_value @@ -215,28 +174,33 @@ NON_NULL end default_used = !value_provided && default_value_type != IMPLICIT_NULL - variable_extracted( + variable_extracted_method.call( { operation_name: operation.name, unwrapped_type_name: unwrapped_type(variable.type), type: variable.type.to_query_string, default_value_type: default_value_type, provided_value: value_provided, - default_used: default_used + default_used: default_used, + used_in_operation: used_variables.include?(variable.name) }, { query: query } ) end rescue StandardError => ex handle_extraction_exception(ex) end + def extract_used_variables + query.irep_selection.ast_node.variables.each_with_object(Set.new) { |v, set| set << v.name } + end + def extract_arguments(irep_node) return unless irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field) traverse_arguments(irep_node.arguments.argument_values.values, irep_node) rescue GraphQL::ExecutionError @@ -289,8 +253,25 @@ else type.name end rescue StandardError => ex handle_extraction_exception(ex) + end + + def extraction_method(method_name) + @extraction_method_cache ||= {} + return @extraction_method_cache[method_name] if @extraction_method_cache.has_key?(method_name) + + method = if respond_to?(method_name) + method(method_name) + elsif instrumentor && instrumentor.respond_to?(method_name) + instrumentor.method(method_name) + else + nil + end + + method.tap do |method| + @extraction_method_cache[method_name] = method + end end end end