Skip to content

Commit 46f8b69

Browse files
authored
Merge pull request #5244 from rmosolgo/dashboard-engine
Add a Rails::Engine-based dashboard with trace viewer
2 parents df21fea + c5415bf commit 46f8b69

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1088
-242
lines changed

guides/css/main.scss

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -276,16 +276,6 @@ a:hover, a:hover code {
276276
overflow-x: scroll;
277277
}
278278

279-
.monitoring-img-group {
280-
display: flex;
281-
flex-direction: row;
282-
margin-bottom: 20px;
283-
flex-wrap: wrap;
284-
justify-content: space-around;
285-
align-items: center;
286-
}
287-
288-
289279
.guides-toc {
290280
ul {
291281
list-style: none;

guides/queries/appoptics_example.png

-249 KB
Binary file not shown.

guides/queries/appsignal_example.png

-159 KB
Binary file not shown.

guides/queries/new_relic_example.png

-156 KB
Binary file not shown.

guides/queries/scout_example.png

-75.3 KB
Binary file not shown.

guides/queries/sentry_example.png

-174 KB
Binary file not shown.

guides/queries/skylight_example.png

-19.9 KB
Binary file not shown.

guides/queries/tracing.md

Lines changed: 16 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -36,189 +36,25 @@ For a full list of methods and their arguments, see {{ "GraphQL::Tracing::Trace"
3636

3737
By default, GraphQL-Ruby makes a new trace instance when it runs a query. You can pass an existing instance as `context: { trace: ... }`. Also, `GraphQL.parse( ..., trace: ...)` accepts a trace instance.
3838

39-
## Trace Modes
39+
## Detailed Traces
4040

41-
You can attach a trace module to run only in some circumstances by using `mode:`. For example, to add detailed tracing for only some requests:
41+
You can capture detailed traces of query execution with {{ "Tracing::DetailedTrace" | api_doc }}. They can be viewed in Google's [Perfetto Trace Viewer](https://ui.perfetto.dev). They include a per-Fiber breakdown with links between fields and Dataloader sources.
4242

43-
```ruby
44-
trace_with DetailedTrace, mode: :detailed_metrics
45-
```
46-
47-
Then, to opt into that trace, use `context: { trace_mode: :detailed_metrics, ... }` when executing queries.
48-
49-
Any custom trace modes _also_ include the default `trace_with ...` modules (that is, those added _without_ any particular `mode: ...` configuration).
50-
51-
## Perfetto Traces
52-
53-
For detailed profiles of complex queries, try {{ "Tracing::PerfettoTrace" | api_doc }}. Its trace can be viewed in Google's [Perfetto Trace Viewer](https://ui.perfetto.dev). They include a per-Fiber breakdown with links between fields and Dataloader sources.
54-
55-
<div class="monitoring-img-group">
56-
{{ "/queries/perfetto_example.png" | link_to_img:"GraphQL-Ruby Dataloader Perfetto Trace" }}
57-
</div>
43+
{{ "/queries/perfetto_example.png" | link_to_img:"GraphQL-Ruby Dataloader Perfetto Trace" }}
5844

59-
## ActiveSupport::Notifications
60-
61-
You can emit events to `ActiveSupport::Notifications` with an experimental tracer, `ActiveSupportNotificationsTrace`.
62-
63-
To enable it, install the tracer:
64-
65-
```ruby
66-
# Send execution events to ActiveSupport::Notifications
67-
class MySchema < GraphQL::Schema
68-
trace_with(GraphQL::Tracing::ActiveSupportNotificationsTrace)
69-
end
70-
```
71-
72-
## Monitoring
73-
74-
Several monitoring platforms are supported out-of-the box by GraphQL-Ruby (see platforms below).
75-
76-
Leaf fields are _not_ monitored (to avoid high cardinality in the metrics service).
77-
78-
## AppOptics
79-
80-
[AppOptics](https://appoptics.com/) instrumentation will be automatic starting
81-
with appoptics_apm-4.11.0.gem. For earlier gem versions please add appoptics_apm
82-
tracing as follows:
83-
84-
```ruby
85-
require 'appoptics_apm'
86-
87-
class MySchema < GraphQL::Schema
88-
trace_with GraphQL::Tracing::AppOpticsTrace
89-
end
90-
```
91-
<div class="monitoring-img-group">
92-
{{ "/queries/appoptics_example.png" | link_to_img:"appoptics monitoring" }}
93-
</div>
94-
95-
## Appsignal
96-
97-
To add [AppSignal](https://appsignal.com/) instrumentation:
98-
99-
```ruby
100-
class MySchema < GraphQL::Schema
101-
trace_with GraphQL::Tracing::AppsignalTrace
102-
end
103-
```
104-
105-
<div class="monitoring-img-group">
106-
{{ "/queries/appsignal_example.png" | link_to_img:"appsignal monitoring" }}
107-
</div>
108-
109-
## New Relic
110-
111-
To add [New Relic](https://newrelic.com/) instrumentation:
112-
113-
```ruby
114-
class MySchema < GraphQL::Schema
115-
trace_with GraphQL::Tracing::NewRelicTrace
116-
# Optional, use the operation name to set the new relic transaction name:
117-
# trace_with GraphQL::Tracing::NewRelicTrace, set_transaction_name: true
118-
end
119-
```
120-
121-
122-
<div class="monitoring-img-group">
123-
{{ "/queries/new_relic_example.png" | link_to_img:"new relic monitoring" }}
124-
</div>
125-
126-
## Scout
127-
128-
To add [Scout APM](https://scoutapp.com/) instrumentation:
129-
130-
```ruby
131-
class MySchema < GraphQL::Schema
132-
trace_with GraphQL::Tracing::ScoutTrace
133-
end
134-
```
45+
Learn how to set it up in the {{ "Tracing::DetailedTrace" | api_doc }} docs.
13546

136-
<div class="monitoring-img-group">
137-
{{ "/queries/scout_example.png" | link_to_img:"scout monitoring" }}
138-
</div>
47+
## External Monitoring Platforms
13948

140-
## Skylight
141-
142-
To add [Skylight](https://www.skylight.io) instrumentation, you may either enable the [GraphQL probe](https://www.skylight.io/support/getting-more-from-skylight#graphql) or use [ActiveSupportNotificationsTracing](/queries/tracing.html#activesupportnotifications).
143-
144-
```ruby
145-
# config/application.rb
146-
config.skylight.probes << "graphql"
147-
```
148-
149-
<div class="monitoring-img-group">
150-
{{ "/queries/skylight_example.png" | link_to_img:"skylight monitoring" }}
151-
</div>
152-
153-
GraphQL instrumentation for Skylight is available in versions >= 4.2.0.
154-
155-
## Datadog
156-
157-
To add [Datadog](https://www.datadoghq.com) instrumentation:
158-
159-
```ruby
160-
class MySchema < GraphQL::Schema
161-
trace_with GraphQL::Tracing::DataDogTrace
162-
end
163-
```
164-
165-
For more details about Datadog's tracing API, check out the [Ruby documentation](https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md) or the [APM documentation](https://docs.datadoghq.com/tracing/) for more product information.
166-
167-
## Prometheus
168-
169-
To add [Prometheus](https://prometheus.io) instrumentation:
170-
171-
```ruby
172-
require 'prometheus_exporter/client'
173-
174-
class MySchema < GraphQL::Schema
175-
trace_with GraphQL::Tracing::PrometheusTrace
176-
end
177-
```
178-
179-
The PrometheusExporter server must be run with a custom type collector that extends
180-
`GraphQL::Tracing::PrometheusTracing::GraphQLCollector`:
181-
182-
```ruby
183-
# lib/graphql_collector.rb
184-
if defined?(PrometheusExporter::Server)
185-
require 'graphql/tracing'
186-
187-
class GraphQLCollector < GraphQL::Tracing::PrometheusTrace::GraphQLCollector
188-
end
189-
end
190-
```
191-
192-
```sh
193-
bundle exec prometheus_exporter -a lib/graphql_collector.rb
194-
```
195-
196-
## Sentry
197-
198-
To add [Sentry](https://sentry.io) instrumentation:
199-
200-
```ruby
201-
class MySchema < GraphQL::Schema
202-
trace_with GraphQL::Tracing::SentryTrace
203-
end
204-
```
205-
206-
<div class="monitoring-img-group">
207-
{{ "/queries/sentry_example.png" | link_to_img:"sentry monitoring" }}
208-
</div>
209-
210-
211-
## Statsd
212-
213-
You can add Statsd instrumentation by initializing a statsd client and passing it to {{ "GraphQL::Tracing::StatsdTrace" | api_doc }}:
214-
215-
```ruby
216-
$statsd = Statsd.new 'localhost', 9125
217-
# ...
218-
219-
class MySchema < GraphQL::Schema
220-
use GraphQL::Tracing::StatsdTrace, statsd: $statsd
221-
end
222-
```
49+
There integrations for GraphQL-Ruby with several other monitoring systems:
22350

224-
Any Statsd client that implements `.time(name) { ... }` will work.
51+
- `ActiveSupport::Notifications`: See {{ "Tracing::ActiveSupportNotificationsTrace" | api_doc }}.
52+
- [AppOptics](https://appoptics.com/) instrumentation is automatic in `appoptics_apm` v4.11.0+.
53+
- [AppSignal](https://appsignal.com/): See {{ "Tracing::AppsignalTrace" | api_doc }}.
54+
- [Datadog](https://www.datadoghq.com): See {{ "Tracing::DataDogTrace" | api_doc }}.
55+
- [NewRelic](https://newrelic.com/): See {{ "Tracing::NewRelicTrace" | api_doc }}.
56+
- [Prometheus](https://prometheus.io): See {{ "Tracing::PrometheusTrace" | api_doc }}.
57+
- [Scout APM](https://scoutapp.com/): See {{ "Tracing::ScoutTrace" | api_doc }}.
58+
- [Sentry](https://sentry.io): See {{ "Tracing::SentryTrace" | api_doc }}.
59+
- [Skylight](https://www.skylight.io): either enable the [GraphQL probe](https://www.skylight.io/support/getting-more-from-skylight#graphql) or use {{ "Tracing::ActiveSupportNotificationsTrace" | api_doc }}.
60+
- Statsd: See {{ "Tracing::StatsdTrace" | api_doc }}.

lib/graphql.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ class << self
125125
autoload :LoadApplicationObjectFailedError, "graphql/load_application_object_failed_error"
126126
autoload :Testing, "graphql/testing"
127127
autoload :Current, "graphql/current"
128+
if defined?(::Rails::Engine)
129+
autoload :Dashboard, 'graphql/dashboard'
130+
end
128131
end
129132

130133
require "graphql/version"

lib/graphql/dashboard.rb

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# frozen_string_literal: true
2+
require 'rails/engine'
3+
4+
module Graphql
5+
# `GraphQL::Dashboard` is a `Rails::Engine`-based dashboard for viewing metadata about your GraphQL schema.
6+
#
7+
# Pass the class name of your schema when mounting it.
8+
# @see GraphQL::Tracing::DetailedTrace DetailedTrace for viewing production traces in the Dashboard
9+
#
10+
# @example Mounting the Dashboard in your app
11+
# mount GraphQL::Dashboard, at: "graphql_dashboard", schema: "MySchema"
12+
#
13+
# @example Authenticating the Dashboard with HTTP Basic Auth
14+
# # config/initializers/graphql_dashboard.rb
15+
# GraphQL::Dashboard.middleware.use(Rack::Auth::Basic) do |username, password|
16+
# # Compare the provided username/password to an application setting:
17+
# ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, username) &&
18+
# ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, password)
19+
# end
20+
#
21+
# @example Custom Rails authentication
22+
# # config/initializers/graphql_dashboard.rb
23+
# ActiveSupport.on_load(:graphql_dashboard_application_controller) do
24+
# # context here is GraphQL::Dashboard::ApplicationController
25+
#
26+
# before_action do
27+
# raise ActionController::RoutingError.new('Not Found') unless current_user&.admin?
28+
# end
29+
#
30+
# def current_user
31+
# # load current user
32+
# end
33+
# end
34+
#
35+
class Dashboard < Rails::Engine
36+
engine_name "graphql_dashboard"
37+
isolate_namespace(Graphql::Dashboard)
38+
routes.draw do
39+
root "landings#show"
40+
resources :statics, only: :show, constraints: { id: /[0-9A-Za-z\-.]+/ }
41+
delete "/traces/delete_all", to: "traces#delete_all", as: :traces_delete_all
42+
resources :traces, only: [:index, :show, :destroy]
43+
end
44+
45+
class ApplicationController < ActionController::Base
46+
protect_from_forgery with: :exception
47+
prepend_view_path(File.join(__FILE__, "../dashboard/views"))
48+
49+
content_security_policy do |policy|
50+
policy.default_src(:self) if policy.default_src(*policy.default_src).blank?
51+
policy.connect_src(:self) if policy.connect_src(*policy.connect_src).blank?
52+
policy.base_uri(:none) if policy.base_uri(*policy.base_uri).blank?
53+
policy.font_src(:self) if policy.font_src(*policy.font_src).blank?
54+
policy.img_src(:self, :data) if policy.img_src(*policy.img_src).blank?
55+
policy.object_src(:none) if policy.object_src(*policy.object_src).blank?
56+
policy.script_src(:self) if policy.script_src(*policy.script_src).blank?
57+
policy.style_src(:self) if policy.style_src(*policy.style_src).blank?
58+
policy.form_action(:self) if policy.form_action(*policy.form_action).blank?
59+
policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
60+
end
61+
62+
def schema_class
63+
@schema_class ||= begin
64+
schema_param = request.query_parameters["schema"] || params[:schema]
65+
case schema_param
66+
when Class
67+
schema_param
68+
when String
69+
schema_param.constantize
70+
else
71+
raise "Missing `params[:schema]`, please provide a class or string to `mount GraphQL::Dashboard, schema: ...`"
72+
end
73+
end
74+
end
75+
helper_method :schema_class
76+
end
77+
78+
class LandingsController < ApplicationController
79+
def show
80+
end
81+
end
82+
83+
class TracesController < ApplicationController
84+
def index
85+
@detailed_trace_installed = !!schema_class.detailed_trace
86+
if @detailed_trace_installed
87+
@last = params[:last]&.to_i || 50
88+
@before = params[:before]&.to_i
89+
@traces = schema_class.detailed_trace.traces(last: @last, before: @before)
90+
end
91+
end
92+
93+
def show
94+
trace = schema_class.detailed_trace.find_trace(params[:id].to_i)
95+
send_data(trace.trace_data)
96+
end
97+
98+
def destroy
99+
schema_class.detailed_trace.delete_trace(params[:id])
100+
head :no_content
101+
end
102+
103+
def delete_all
104+
schema_class.detailed_trace.delete_all_traces
105+
head :no_content
106+
end
107+
end
108+
109+
class StaticsController < ApplicationController
110+
skip_after_action :verify_same_origin_request
111+
# Use an explicit list of files to avoid any chance of reading other files from disk
112+
STATICS = {}
113+
114+
[
115+
"icon.png",
116+
"header-icon.png",
117+
"dashboard.css",
118+
"dashboard.js",
119+
"bootstrap-5.3.3.min.css",
120+
"bootstrap-5.3.3.min.js",
121+
].each do |static_file|
122+
STATICS[static_file] = File.expand_path("../dashboard/statics/#{static_file}", __FILE__)
123+
end
124+
125+
def show
126+
expires_in 1.year, public: true
127+
if (filepath = STATICS[params[:id]])
128+
render file: filepath
129+
else
130+
head :not_found
131+
end
132+
end
133+
end
134+
end
135+
end
136+
137+
# Rails expects the engine to be called `Graphql::Dashboard`,
138+
# but `GraphQL::Dashboard` is consistent with this gem's naming.
139+
# So define both constants to refer to the same class.
140+
GraphQL::Dashboard = Graphql::Dashboard
141+
142+
ActiveSupport.run_load_hooks(:graphql_dashboard_application_controller, GraphQL::Dashboard::ApplicationController)

lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#header-icon {
2+
max-height: 2em;
3+
}

0 commit comments

Comments
 (0)