Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
76dad36
Add Rails engine-based dashboard with Perfetto trace listing
rmosolgo Feb 19, 2025
5037617
Add memory and redis backends
rmosolgo Feb 19, 2025
6ef72dc
Add icons and dark mode
rmosolgo Feb 19, 2025
a72de98
update tests, statics
rmosolgo Feb 19, 2025
96470ee
Add tests for memory and redis storage
rmosolgo Feb 19, 2025
93ff323
improve Redis CI setup
rmosolgo Feb 19, 2025
68129a9
Add PerfettoSampler tests
rmosolgo Feb 19, 2025
d020c1d
Add test coverage
rmosolgo Feb 19, 2025
1cf44ac
Fix a couple of tests
rmosolgo Feb 19, 2025
e15a9d4
Try running system tests with a ci gemfile
rmosolgo Feb 20, 2025
652b38a
Try different gemfile setup
rmosolgo Feb 20, 2025
d556cd0
try bin/rails
rmosolgo Feb 20, 2025
2245b02
Try relative gemfile path
rmosolgo Feb 20, 2025
ad24c1c
Merge branch 'master' into dashboard-engine
rmosolgo Feb 20, 2025
56113c9
Merge branch 'master' into dashboard-engine
rmosolgo Feb 20, 2025
06db592
Clear any leftover flow stack
rmosolgo Feb 21, 2025
1bd193e
Merge branch 'master' into dashboard-engine
rmosolgo Feb 21, 2025
1d11253
Add CSP rules
rmosolgo Feb 22, 2025
735eed9
Start adding tests
rmosolgo Feb 24, 2025
86b526e
Add tests for Dashboard controllers
rmosolgo Feb 24, 2025
bf7cf65
Update Redis backend
rmosolgo Feb 24, 2025
ae7eceb
Add limit: option
rmosolgo Feb 24, 2025
9de189a
Update error message test
rmosolgo Feb 24, 2025
3b5566a
fix dashboard tests
rmosolgo Feb 24, 2025
ace88db
Rename to DetailedTrace, write docs
rmosolgo Feb 24, 2025
c5415bf
Add test for multiplex
rmosolgo Feb 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions guides/css/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,6 @@ a:hover, a:hover code {
overflow-x: scroll;
}

.monitoring-img-group {
display: flex;
flex-direction: row;
margin-bottom: 20px;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
}


.guides-toc {
ul {
list-style: none;
Expand Down
Binary file removed guides/queries/appoptics_example.png
Binary file not shown.
Binary file removed guides/queries/appsignal_example.png
Binary file not shown.
Binary file removed guides/queries/new_relic_example.png
Binary file not shown.
Binary file removed guides/queries/scout_example.png
Binary file not shown.
Binary file removed guides/queries/sentry_example.png
Binary file not shown.
Binary file removed guides/queries/skylight_example.png
Binary file not shown.
196 changes: 16 additions & 180 deletions guides/queries/tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,189 +36,25 @@ For a full list of methods and their arguments, see {{ "GraphQL::Tracing::Trace"

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.

## Trace Modes
## Detailed Traces

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:
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.

```ruby
trace_with DetailedTrace, mode: :detailed_metrics
```

Then, to opt into that trace, use `context: { trace_mode: :detailed_metrics, ... }` when executing queries.

Any custom trace modes _also_ include the default `trace_with ...` modules (that is, those added _without_ any particular `mode: ...` configuration).

## Perfetto Traces

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.

<div class="monitoring-img-group">
{{ "/queries/perfetto_example.png" | link_to_img:"GraphQL-Ruby Dataloader Perfetto Trace" }}
</div>
{{ "/queries/perfetto_example.png" | link_to_img:"GraphQL-Ruby Dataloader Perfetto Trace" }}

## ActiveSupport::Notifications

You can emit events to `ActiveSupport::Notifications` with an experimental tracer, `ActiveSupportNotificationsTrace`.

To enable it, install the tracer:

```ruby
# Send execution events to ActiveSupport::Notifications
class MySchema < GraphQL::Schema
trace_with(GraphQL::Tracing::ActiveSupportNotificationsTrace)
end
```

## Monitoring

Several monitoring platforms are supported out-of-the box by GraphQL-Ruby (see platforms below).

Leaf fields are _not_ monitored (to avoid high cardinality in the metrics service).

## AppOptics

[AppOptics](https://appoptics.com/) instrumentation will be automatic starting
with appoptics_apm-4.11.0.gem. For earlier gem versions please add appoptics_apm
tracing as follows:

```ruby
require 'appoptics_apm'

class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::AppOpticsTrace
end
```
<div class="monitoring-img-group">
{{ "/queries/appoptics_example.png" | link_to_img:"appoptics monitoring" }}
</div>

## Appsignal

To add [AppSignal](https://appsignal.com/) instrumentation:

```ruby
class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::AppsignalTrace
end
```

<div class="monitoring-img-group">
{{ "/queries/appsignal_example.png" | link_to_img:"appsignal monitoring" }}
</div>

## New Relic

To add [New Relic](https://newrelic.com/) instrumentation:

```ruby
class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::NewRelicTrace
# Optional, use the operation name to set the new relic transaction name:
# trace_with GraphQL::Tracing::NewRelicTrace, set_transaction_name: true
end
```


<div class="monitoring-img-group">
{{ "/queries/new_relic_example.png" | link_to_img:"new relic monitoring" }}
</div>

## Scout

To add [Scout APM](https://scoutapp.com/) instrumentation:

```ruby
class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::ScoutTrace
end
```
Learn how to set it up in the {{ "Tracing::DetailedTrace" | api_doc }} docs.

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

## Skylight

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).

```ruby
# config/application.rb
config.skylight.probes << "graphql"
```

<div class="monitoring-img-group">
{{ "/queries/skylight_example.png" | link_to_img:"skylight monitoring" }}
</div>

GraphQL instrumentation for Skylight is available in versions >= 4.2.0.

## Datadog

To add [Datadog](https://www.datadoghq.com) instrumentation:

```ruby
class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::DataDogTrace
end
```

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.

## Prometheus

To add [Prometheus](https://prometheus.io) instrumentation:

```ruby
require 'prometheus_exporter/client'

class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::PrometheusTrace
end
```

The PrometheusExporter server must be run with a custom type collector that extends
`GraphQL::Tracing::PrometheusTracing::GraphQLCollector`:

```ruby
# lib/graphql_collector.rb
if defined?(PrometheusExporter::Server)
require 'graphql/tracing'

class GraphQLCollector < GraphQL::Tracing::PrometheusTrace::GraphQLCollector
end
end
```

```sh
bundle exec prometheus_exporter -a lib/graphql_collector.rb
```

## Sentry

To add [Sentry](https://sentry.io) instrumentation:

```ruby
class MySchema < GraphQL::Schema
trace_with GraphQL::Tracing::SentryTrace
end
```

<div class="monitoring-img-group">
{{ "/queries/sentry_example.png" | link_to_img:"sentry monitoring" }}
</div>


## Statsd

You can add Statsd instrumentation by initializing a statsd client and passing it to {{ "GraphQL::Tracing::StatsdTrace" | api_doc }}:

```ruby
$statsd = Statsd.new 'localhost', 9125
# ...

class MySchema < GraphQL::Schema
use GraphQL::Tracing::StatsdTrace, statsd: $statsd
end
```
There integrations for GraphQL-Ruby with several other monitoring systems:

Any Statsd client that implements `.time(name) { ... }` will work.
- `ActiveSupport::Notifications`: See {{ "Tracing::ActiveSupportNotificationsTrace" | api_doc }}.
- [AppOptics](https://appoptics.com/) instrumentation is automatic in `appoptics_apm` v4.11.0+.
- [AppSignal](https://appsignal.com/): See {{ "Tracing::AppsignalTrace" | api_doc }}.
- [Datadog](https://www.datadoghq.com): See {{ "Tracing::DataDogTrace" | api_doc }}.
- [NewRelic](https://newrelic.com/): See {{ "Tracing::NewRelicTrace" | api_doc }}.
- [Prometheus](https://prometheus.io): See {{ "Tracing::PrometheusTrace" | api_doc }}.
- [Scout APM](https://scoutapp.com/): See {{ "Tracing::ScoutTrace" | api_doc }}.
- [Sentry](https://sentry.io): See {{ "Tracing::SentryTrace" | api_doc }}.
- [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 }}.
- Statsd: See {{ "Tracing::StatsdTrace" | api_doc }}.
3 changes: 3 additions & 0 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ class << self
autoload :LoadApplicationObjectFailedError, "graphql/load_application_object_failed_error"
autoload :Testing, "graphql/testing"
autoload :Current, "graphql/current"
if defined?(::Rails::Engine)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

module GraphQL missing tests for lines 15, 16, 17, 18, 63, 67, 68, 72, 128, 128, 128 (coverage: 0.88)

autoload :Dashboard, 'graphql/dashboard'
end
end

require "graphql/version"
Expand Down
142 changes: 142 additions & 0 deletions lib/graphql/dashboard.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# frozen_string_literal: true
require 'rails/engine'

module Graphql
# `GraphQL::Dashboard` is a `Rails::Engine`-based dashboard for viewing metadata about your GraphQL schema.
#
# Pass the class name of your schema when mounting it.
# @see GraphQL::Tracing::DetailedTrace DetailedTrace for viewing production traces in the Dashboard
#
# @example Mounting the Dashboard in your app
# mount GraphQL::Dashboard, at: "graphql_dashboard", schema: "MySchema"
#
# @example Authenticating the Dashboard with HTTP Basic Auth
# # config/initializers/graphql_dashboard.rb
# GraphQL::Dashboard.middleware.use(Rack::Auth::Basic) do |username, password|
# # Compare the provided username/password to an application setting:
# ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, username) &&
# ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, password)
# end
#
# @example Custom Rails authentication
# # config/initializers/graphql_dashboard.rb
# ActiveSupport.on_load(:graphql_dashboard_application_controller) do
# # context here is GraphQL::Dashboard::ApplicationController
#
# before_action do
# raise ActionController::RoutingError.new('Not Found') unless current_user&.admin?
# end
#
# def current_user
# # load current user
# end
# end
#
class Dashboard < Rails::Engine
engine_name "graphql_dashboard"
isolate_namespace(Graphql::Dashboard)
routes.draw do
root "landings#show"
resources :statics, only: :show, constraints: { id: /[0-9A-Za-z\-.]+/ }
delete "/traces/delete_all", to: "traces#delete_all", as: :traces_delete_all
resources :traces, only: [:index, :show, :destroy]
end

class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
prepend_view_path(File.join(__FILE__, "../dashboard/views"))

content_security_policy do |policy|
policy.default_src(:self) if policy.default_src(*policy.default_src).blank?
policy.connect_src(:self) if policy.connect_src(*policy.connect_src).blank?
policy.base_uri(:none) if policy.base_uri(*policy.base_uri).blank?
policy.font_src(:self) if policy.font_src(*policy.font_src).blank?
policy.img_src(:self, :data) if policy.img_src(*policy.img_src).blank?
policy.object_src(:none) if policy.object_src(*policy.object_src).blank?
policy.script_src(:self) if policy.script_src(*policy.script_src).blank?
policy.style_src(:self) if policy.style_src(*policy.style_src).blank?
policy.form_action(:self) if policy.form_action(*policy.form_action).blank?
policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
end

def schema_class
@schema_class ||= begin
schema_param = request.query_parameters["schema"] || params[:schema]
case schema_param
when Class
schema_param
when String
schema_param.constantize
else
raise "Missing `params[:schema]`, please provide a class or string to `mount GraphQL::Dashboard, schema: ...`"
end
end
end
helper_method :schema_class
end

class LandingsController < ApplicationController
def show
end
end

class TracesController < ApplicationController
def index
@detailed_trace_installed = !!schema_class.detailed_trace
if @detailed_trace_installed
@last = params[:last]&.to_i || 50
@before = params[:before]&.to_i
@traces = schema_class.detailed_trace.traces(last: @last, before: @before)
end
end

def show
trace = schema_class.detailed_trace.find_trace(params[:id].to_i)
send_data(trace.trace_data)
end

def destroy
schema_class.detailed_trace.delete_trace(params[:id])
head :no_content
end

def delete_all
schema_class.detailed_trace.delete_all_traces
head :no_content
end
end

class StaticsController < ApplicationController
skip_after_action :verify_same_origin_request
# Use an explicit list of files to avoid any chance of reading other files from disk
STATICS = {}

[
"icon.png",
"header-icon.png",
"dashboard.css",
"dashboard.js",
"bootstrap-5.3.3.min.css",
"bootstrap-5.3.3.min.js",
].each do |static_file|
STATICS[static_file] = File.expand_path("../dashboard/statics/#{static_file}", __FILE__)
end

def show
expires_in 1.year, public: true
if (filepath = STATICS[params[:id]])
render file: filepath
else
head :not_found
end
end
end
end
end

# Rails expects the engine to be called `Graphql::Dashboard`,
# but `GraphQL::Dashboard` is consistent with this gem's naming.
# So define both constants to refer to the same class.
GraphQL::Dashboard = Graphql::Dashboard

ActiveSupport.run_load_hooks(:graphql_dashboard_application_controller, GraphQL::Dashboard::ApplicationController)
6 changes: 6 additions & 0 deletions lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions lib/graphql/dashboard/statics/dashboard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#header-icon {
max-height: 2em;
}
Loading
Loading