Skip to content

Commit 7743b23

Browse files
committed
Make generator do other DetailedTrace setup and support Redis, add tests, add docs
1 parent 34e44ac commit 7743b23

6 files changed

Lines changed: 174 additions & 32 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# frozen_string_literal: true
2+
require 'rails/generators/active_record'
3+
4+
module Graphql
5+
module Generators
6+
class DetailedTraceGenerator < ::Rails::Generators::Base
7+
include ::Rails::Generators::Migration
8+
desc "Install GraphQL::Tracing::DetailedTrace for your schema"
9+
source_root File.expand_path('../templates', __FILE__)
10+
11+
class_option :redis,
12+
type: :boolean,
13+
default: false,
14+
desc: "Use Redis for persistence instead of ActiveRecord"
15+
16+
def self.next_migration_number(dirname)
17+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
18+
end
19+
20+
def install_detailed_traces
21+
22+
schema_glob = File.expand_path("app/graphql/*_schema.rb", destination_root)
23+
schema_file = Dir.glob(schema_glob).first
24+
if !schema_file
25+
raise ArgumentError, "Failed to find schema definition file (checked: #{schema_glob.inspect})"
26+
end
27+
schema_file_match = /( *)class ([A-Za-z:]+) < GraphQL::Schema/.match(File.read(schema_file))
28+
schema_name = schema_file_match[2]
29+
indent = schema_file_match[1] + " "
30+
31+
if !options.redis?
32+
migration_template 'create_graphql_detailed_traces.erb', 'db/migrate/create_graphql_detailed_traces.rb'
33+
end
34+
35+
log :add_detailed_traces_plugin
36+
sentinel = /< GraphQL::Schema\s*\n/m
37+
code = <<-RUBY
38+
#{indent}use GraphQL::Tracing::DetailedTrace#{options.redis? ? ", redis: raise(\"TODO: pass a connection to a persistent redis database\")" : ""}, limit: 50
39+
40+
#{indent}# When this returns true, DetailedTrace will trace the query
41+
#{indent}# Could use `query.context`, `query.selected_operation_name`, `query.query_string` here
42+
#{indent}# Could call out to Flipper, etc
43+
#{indent}def self.detailed_trace?(query)
44+
#{indent} rand <= 0.000_1 # one in ten thousand
45+
#{indent}end
46+
47+
RUBY
48+
49+
in_root do
50+
inject_into_file schema_file, code, after: sentinel, force: false
51+
end
52+
53+
routes_source = File.read(File.expand_path("config/routes.rb", destination_root))
54+
already_has_dashboard = routes_source.include?("GraphQL::Dashboard") ||
55+
routes_source.include?("Schema.dashboard") ||
56+
routes_source.include?("GraphQL::Pro::Routes::Lazy")
57+
58+
if (!already_has_dashboard || behavior == :revoke)
59+
log :route, "GraphQL::Dashboard"
60+
shell.mute do
61+
route <<~RUBY
62+
# TODO: add authorization to this route and expose it in production
63+
# See https://graphql-ruby.org/pro/dashboard.html#authorizing-the-dashboard
64+
if Rails.env.development?
65+
mount GraphQL::Dashboard, at: "/graphql/dashboard", schema: #{schema_name.inspect}
66+
end
67+
68+
RUBY
69+
end
70+
end
71+
end
72+
end
73+
end
74+
end

lib/generators/graphql/detailed_traces_generator.rb

Lines changed: 0 additions & 20 deletions
This file was deleted.

lib/graphql/tracing/detailed_trace.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
module GraphQL
99
module Tracing
10-
# `DetailedTrace` can make detailed profiles for a subset of production traffic.
10+
# `DetailedTrace` can make detailed profiles for a subset of production traffic. Install it in Rails with `rails generate graphql:detailed_trace`.
1111
#
1212
# When `MySchema.detailed_trace?(query)` returns `true`, a profiler-specific `trace_mode: ...` will be used for the query,
1313
# overriding the one in `context[:trace_mode]`.
@@ -16,10 +16,19 @@ module Tracing
1616
# this behavior by extending {DetailedTrace} and overriding {#inspect_object}. You can opt out of debug annotations
1717
# entirely with `use ..., debug: false` or for a single query with `context: { detailed_trace_debug: false }`.
1818
#
19-
# __Redis__: The sampler stores its results in a provided Redis database. Depending on your needs,
20-
# You can configure this database to retain all data (persistent) or to expire data according to your rules.
19+
# You can store saved traces in two ways:
20+
#
21+
# - __ActiveRecord__: With `rails generate graphql:detailed_trace`, a new migration will be added to your app.
22+
# That table will be used to store trace data.
23+
#
24+
# - __Redis__: Pass `redis: ...` to save trace data to a Redis database. Depending on your needs,
25+
# you can configure this database to retain all data (persistent) or to expire data according to your rules.
26+
#
2127
# If you need to save traces indefinitely, you can download them from Perfetto after opening them there.
2228
#
29+
# @example Installing with Rails
30+
# rails generate graphql:detailed_trace # optional: --redis
31+
#
2332
# @example Adding the sampler to your schema
2433
# class MySchema < GraphQL::Schema
2534
# # Add the sampler:
@@ -56,13 +65,14 @@ class DetailedTrace
5665
# @param redis [Redis] If provided, profiles will be stored in Redis for later review
5766
# @param limit [Integer] A maximum number of profiles to store
5867
# @param debug [Boolean] if `false`, it won't create `debug` annotations in Perfetto traces (reduces overhead)
59-
def self.use(schema, trace_mode: :profile_sample, memory: false, debug: debug?, redis: nil, limit: nil)
68+
# @param model_class [Class<ActiveRecord::Base>] Overrides {ActiveRecordBackend::GraphqlDetailedTrace} if present
69+
def self.use(schema, trace_mode: :profile_sample, memory: false, debug: debug?, redis: nil, limit: nil, model_class: nil)
6070
storage = if redis
6171
RedisBackend.new(redis: redis, limit: limit)
6272
elsif memory
6373
MemoryBackend.new(limit: limit)
6474
elsif defined?(ActiveRecord)
65-
ActiveRecordBackend.new(limit: limit)
75+
ActiveRecordBackend.new(limit: limit, model_class: model_class)
6676
else
6777
raise ArgumentError, "To store traces, install ActiveRecord or provide `redis: ...`"
6878
end

lib/graphql/tracing/detailed_trace/active_record_backend.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ class ActiveRecordBackend
77
class GraphqlDetailedTrace < ActiveRecord::Base
88
end
99

10-
def initialize(limit: nil)
10+
def initialize(limit: nil, model_class: nil)
1111
@limit = limit
12+
@model_class = model_class || GraphqlDetailedTrace
1213
end
1314

1415
def traces(last:, before:)
15-
gdts = GraphqlDetailedTrace.all.order("begin_ms DESC")
16+
gdts = @model_class.all.order("begin_ms DESC")
1617
if before
1718
gdts = gdts.where("begin_ms < ?", before)
1819
end
@@ -23,16 +24,16 @@ def traces(last:, before:)
2324
end
2425

2526
def delete_trace(id)
26-
GraphqlDetailedTrace.where(id: id).destroy_all
27+
@model_class.where(id: id).destroy_all
2728
nil
2829
end
2930

3031
def delete_all_traces
31-
GraphqlDetailedTrace.all.destroy_all
32+
@model_class.all.destroy_all
3233
end
3334

3435
def find_trace(id)
35-
gdt = GraphqlDetailedTrace.find_by(id: id)
36+
gdt = @model_class.find_by(id: id)
3637
if gdt
3738
record_to_stored_trace(gdt)
3839
else
@@ -41,14 +42,14 @@ def find_trace(id)
4142
end
4243

4344
def save_trace(operation_name, duration_ms, begin_ms, trace_data)
44-
gdt = GraphqlDetailedTrace.create!(
45+
gdt = @model_class.create!(
4546
begin_ms: begin_ms,
4647
operation_name: operation_name,
4748
duration_ms: duration_ms,
4849
trace_data: Base64.encode64(trace_data),
4950
)
5051
if @limit
51-
GraphqlDetailedTrace
52+
@model_class
5253
.where("id NOT IN(SELECT id FROM graphql_detailed_traces ORDER BY begin_ms DESC LIMIT ?)", @limit)
5354
.delete_all
5455
end

spec/graphql/tracing/detailed_trace/active_record_backend_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,21 @@
88
def new_backend(**kwargs)
99
GraphQL::Tracing::DetailedTrace::ActiveRecordBackend.new(**kwargs)
1010
end
11+
12+
class DummyModel
13+
def self.find_by(id:)
14+
OpenStruct.new(
15+
trace_data: Base64.encode64("DummyModel##{id}")
16+
)
17+
end
18+
end
19+
20+
it "can use a custom model class" do
21+
schema = Class.new(GraphQL::Schema) do
22+
use GraphQL::Tracing::DetailedTrace, model_class: DummyModel
23+
end
24+
25+
assert_equal "DummyModel#1234", schema.detailed_trace.find_trace(1234).trace_data
26+
end
1127
end
1228
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
require "spec_helper"
3+
require "generators/graphql/install_generator"
4+
require "generators/graphql/detailed_trace_generator"
5+
6+
class GraphQLGeneratorsDetailedTraceGeneratorTest < Rails::Generators::TestCase
7+
tests Graphql::Generators::DetailedTraceGenerator
8+
destination File.expand_path("../../../tmp/dummy", File.dirname(__FILE__))
9+
10+
setup do
11+
prepare_destination
12+
FileUtils.cd(File.join(destination_root, '..')) do
13+
`rails new dummy --skip-active-record --skip-test-unit --skip-spring --skip-bundle --skip-webpack-install`
14+
Graphql::Generators::InstallGenerator.start(["--skip-graphiql"], { destination_root: destination_root })
15+
end
16+
end
17+
18+
test "it creates a migration, installs a route, and adds schema configuration" do
19+
run_generator
20+
assert_migration "db/migrate/create_graphql_detailed_traces"
21+
assert_file "app/graphql/dummy_schema.rb" do |content|
22+
assert_includes content, "
23+
use GraphQL::Tracing::DetailedTrace, limit: 50
24+
25+
# When this returns true, DetailedTrace will trace the query
26+
# Could use `query.context`, `query.selected_operation_name`, `query.query_string` here
27+
# Could call out to Flipper, etc
28+
def self.detailed_trace?(query)
29+
rand <= 0.000_1 # one in ten thousand
30+
end
31+
"
32+
end
33+
34+
assert_file "config/routes.rb" do |content|
35+
assert_includes content, "mount GraphQL::Dashboard, at: \"/graphql/dashboard\", schema: \"DummySchema\""
36+
end
37+
end
38+
39+
test "it doesn't duplicate dashboard setup" do
40+
routes_path = File.expand_path("config/routes.rb", destination_root)
41+
existing_routes = File.read(routes_path)
42+
new_routes = existing_routes.sub("draw do\n", "draw do\n mount DummySchema.dashboard\n")
43+
File.write(routes_path, new_routes)
44+
run_generator
45+
assert_file "config/routes.rb" do |content|
46+
refute_includes content, "mount GraphQL::Dashboard"
47+
end
48+
end
49+
50+
test "it sets up Redis, too" do
51+
run_generator(["--redis"])
52+
assert_no_migration "db/migrate/create_graphql_detailed_traces"
53+
assert_file "app/graphql/dummy_schema.rb" do |content|
54+
assert_includes content, "use GraphQL::Tracing::DetailedTrace, redis: raise(\"TODO: pass a connection to a persistent redis database\"), limit: 50\n\n"
55+
end
56+
57+
assert_file "config/routes.rb" do |content|
58+
assert_includes content, "mount GraphQL::Dashboard, at: \"/graphql/dashboard\", schema: \"DummySchema\""
59+
end
60+
end
61+
end

0 commit comments

Comments
 (0)