Commit 63f9259d authored by Sean McGivern's avatar Sean McGivern

Merge branch 'ee-33929-allow-to-enable-perf-bar-for-a-group' into 'master'

[EE] Allow to enable the performance bar per user or Feature group

See merge request !2344
parents 30345c81 993cb1f4
...@@ -62,7 +62,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; ...@@ -62,7 +62,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
if (Cookies.get(performanceBarCookieName) === 'true') { if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' }); Cookies.remove(performanceBarCookieName, { path: '/' });
} else { } else {
Cookies.set(performanceBarCookieName, true, { path: '/' }); Cookies.set(performanceBarCookieName, 'true', { path: '/' });
} }
gl.utils.refreshCurrentPage(); gl.utils.refreshCurrentPage();
}; };
......
...@@ -127,6 +127,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -127,6 +127,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:metrics_port, :metrics_port,
:metrics_sample_interval, :metrics_sample_interval,
:metrics_timeout, :metrics_timeout,
:performance_bar_allowed_group_id,
:performance_bar_enabled,
:recaptcha_enabled, :recaptcha_enabled,
:recaptcha_private_key, :recaptcha_private_key,
:recaptcha_site_key, :recaptcha_site_key,
......
...@@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base ...@@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
include SentryHelper include SentryHelper
include WorkhorseHelper include WorkhorseHelper
include EnforcesTwoFactorAuthentication include EnforcesTwoFactorAuthentication
include Peek::Rblineprof::CustomControllerHelpers include WithPerformanceBar
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user_from_rss_token! before_action :authenticate_user_from_rss_token!
...@@ -68,21 +68,6 @@ class ApplicationController < ActionController::Base ...@@ -68,21 +68,6 @@ class ApplicationController < ActionController::Base
end end
end end
def peek_enabled?
return false unless Gitlab::PerformanceBar.enabled?
return false unless current_user
if RequestStore.active?
if RequestStore.store.key?(:peek_enabled)
RequestStore.store[:peek_enabled]
else
RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present?
end
else
cookies[:perf_bar_enabled].present?
end
end
protected protected
# This filter handles both private tokens and personal access tokens # This filter handles both private tokens and personal access tokens
......
module WithPerformanceBar
extend ActiveSupport::Concern
included do
include Peek::Rblineprof::CustomControllerHelpers
end
def peek_enabled?
return false unless Gitlab::PerformanceBar.enabled?(current_user)
if RequestStore.active?
RequestStore.fetch(:peek_enabled) { cookies[:perf_bar_enabled].present? }
else
cookies[:perf_bar_enabled].present?
end
end
end
...@@ -247,6 +247,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -247,6 +247,7 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil, koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
performance_bar_allowed_group_id: nil,
plantuml_enabled: false, plantuml_enabled: false,
plantuml_url: nil, plantuml_url: nil,
recaptcha_enabled: false, recaptcha_enabled: false,
...@@ -383,6 +384,48 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -383,6 +384,48 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) }) super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end end
def performance_bar_allowed_group_id=(group_full_path)
group_full_path = nil if group_full_path.blank?
if group_full_path.nil?
if group_full_path != performance_bar_allowed_group_id
super(group_full_path)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
return
end
group = Group.find_by_full_path(group_full_path)
if group
if group.id != performance_bar_allowed_group_id
super(group.id)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
else
super(nil)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
end
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
# Return true if the Performance Bar is enabled for a given group
def performance_bar_enabled
performance_bar_allowed_group_id.present?
end
# - If `enable` is true, we early return since the actual attribute that holds
# the enabling/disabling is `performance_bar_allowed_group_id`
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
def performance_bar_enabled=(enable)
return if enable
self.performance_bar_allowed_group_id = nil
end
# Choose one of the available repository storage options. Currently all have # Choose one of the available repository storage options. Currently all have
# equal weighting. # equal weighting.
def pick_repository_storage def pick_repository_storage
......
...@@ -360,6 +360,22 @@ ...@@ -360,6 +360,22 @@
%strong.cred WARNING: %strong.cred WARNING:
Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory. Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory.
%fieldset
%legend Profiling - Performance Bar
%p
Enable the Performance Bar for a given group.
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :performance_bar_enabled do
= f.check_box :performance_bar_enabled
Enable the Performance Bar
.form-group
= f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
%fieldset %fieldset
%legend Background Jobs %legend Background Jobs
%p %p
......
---
title: Allow to enable the performance bar per user or Feature group
merge_request: 12362
author:
class AddPerformanceBarAllowedGroupIdToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :performance_bar_allowed_group_id, :integer
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170706121518) do ActiveRecord::Schema.define(version: 20170706151212) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -148,6 +148,7 @@ ActiveRecord::Schema.define(version: 20170706121518) do ...@@ -148,6 +148,7 @@ ActiveRecord::Schema.define(version: 20170706121518) do
t.string "slack_app_id" t.string "slack_app_id"
t.string "slack_app_secret" t.string "slack_app_secret"
t.string "slack_app_verification_token" t.string "slack_app_verification_token"
t.integer "performance_bar_allowed_group_id"
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
......
...@@ -193,6 +193,7 @@ have access to GitLab administration tools and settings. ...@@ -193,6 +193,7 @@ have access to GitLab administration tools and settings.
- [Operations](administration/operations.md): Keeping GitLab up and running. - [Operations](administration/operations.md): Keeping GitLab up and running.
- [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates. - [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates.
- [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests. - [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
- [Performance Bar](administration/monitoring/performance/performance_bar.md): Get performance information for the current page.
### Customization ### Customization
......
# Performance Bar
A Performance Bar can be displayed, to dig into the performance of a page. When
activated, it looks as follows:
![Performance Bar](img/performance_bar.png)
It allows you to:
- see the current host serving the page
- see the timing of the page (backend, frontend)
- the number of DB queries, the time it took, and the detail of these queries
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
- the number of calls to Redis, and the time it took
- the number of background jobs created by Sidekiq, and the time it took
- the number of Ruby GC calls, and the time it took
- profile the code used to generate the page, line by line
![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png)
## Enable the Performance Bar via the Admin panel
GitLab Performance Bar is disabled by default. To enable it for a given group,
navigate to the Admin area in **Settings > Profiling - Performance Bar**
(`/admin/application_settings`).
The only required setting you need to set is the full path of the group that
will be allowed to display the Performance Bar.
Make sure _Enable the Performance Bar_ is checked and hit
**Save** to save the changes.
---
![GitLab Performance Bar Admin Settings](img/performance_bar_configuration_settings.png)
---
...@@ -17,6 +17,7 @@ following locations: ...@@ -17,6 +17,7 @@ following locations:
- [Deploy Keys](deploy_keys.md) - [Deploy Keys](deploy_keys.md)
- [Environments](environments.md) - [Environments](environments.md)
- [Events](events.md) - [Events](events.md)
- [Feature flags](features.md)
- [Gitignores templates](templates/gitignores.md) - [Gitignores templates](templates/gitignores.md)
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md) - [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md) - [Groups](groups.md)
......
# Features API # Features flags API
All methods require administrator authorization. All methods require administrator authorization.
...@@ -61,7 +61,8 @@ POST /features/:name ...@@ -61,7 +61,8 @@ POST /features/:name
| `feature_group` | string | no | A Feature group name | | `feature_group` | string | no | A Feature group name |
| `user` | string | no | A GitLab username | | `user` | string | no | A GitLab username |
Note that `feature_group` and `user` are mutually exclusive. Note that you can enable or disable a feature for both a `feature_group` and a
`user` with a single API call.
```bash ```bash
curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library
......
...@@ -3,5 +3,19 @@ ...@@ -3,5 +3,19 @@
Starting from GitLab 9.3 we support feature flags via Starting from GitLab 9.3 we support feature flags via
[Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature` [Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature`
class (defined in `lib/feature.rb`) in your code to get, set and list feature class (defined in `lib/feature.rb`) in your code to get, set and list feature
flags. During runtime you can set the values for the gates via the flags.
[admin API](../api/features.md).
During runtime you can set the values for the gates via the
[features API](../api/features.md) (accessible to admins only).
## Feature groups
Starting from GitLab 9.4 we support feature groups via
[Flipper groups](https://github.com/jnunemaker/flipper/blob/v0.10.2/docs/Gates.md#2-group).
Feature groups must be defined statically in `lib/feature.rb` (in the
`.register_feature_groups` method), but their implementation can obviously be
dynamic (querying the DB etc.).
Once defined in `lib/feature.rb`, you will be able to activate a
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
...@@ -14,14 +14,12 @@ module API ...@@ -14,14 +14,12 @@ module API
end end
end end
def gate_target(params) def gate_targets(params)
if params[:feature_group] targets = []
Feature.group(params[:feature_group]) targets << Feature.group(params[:feature_group]) if params[:feature_group]
elsif params[:user] targets << User.find_by_username(params[:user]) if params[:user]
User.find_by_username(params[:user])
else targets
gate_value(params)
end
end end
end end
...@@ -42,18 +40,25 @@ module API ...@@ -42,18 +40,25 @@ module API
requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time'
optional :feature_group, type: String, desc: 'A Feature group name' optional :feature_group, type: String, desc: 'A Feature group name'
optional :user, type: String, desc: 'A GitLab username' optional :user, type: String, desc: 'A GitLab username'
mutually_exclusive :feature_group, :user
end end
post ':name' do post ':name' do
feature = Feature.get(params[:name]) feature = Feature.get(params[:name])
target = gate_target(params) targets = gate_targets(params)
value = gate_value(params) value = gate_value(params)
case value case value
when true when true
feature.enable(target) if targets.present?
targets.each { |target| feature.enable(target) }
else
feature.enable
end
when false when false
feature.disable(target) if targets.present?
targets.each { |target| feature.disable(target) }
else
feature.disable
end
else else
feature.enable_percentage_of_time(value) feature.enable_percentage_of_time(value)
end end
......
module Gitlab module Gitlab
module PerformanceBar module PerformanceBar
def self.enabled? include Gitlab::CurrentSettings
Rails.env.development? || Feature.enabled?('gitlab_performance_bar')
ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids'.freeze
def self.enabled?(user = nil)
return false unless user && allowed_group_id
allowed_user_ids.include?(user.id)
end
def self.allowed_group_id
current_application_settings.performance_bar_allowed_group_id
end
def self.allowed_user_ids
Rails.cache.fetch(ALLOWED_USER_IDS_KEY) do
group = Group.find_by_id(allowed_group_id)
if group
GroupMembersFinder.new(group).execute.pluck(:user_id)
else
[]
end
end
end
def self.expire_allowed_user_ids_cache
Rails.cache.delete(ALLOWED_USER_IDS_KEY)
end end
end end
end end
...@@ -33,22 +33,24 @@ describe 'User can display performance bar', :js do ...@@ -33,22 +33,24 @@ describe 'User can display performance bar', :js do
end end
end end
let(:group) { create(:group) }
context 'when user is logged-out' do context 'when user is logged-out' do
before do before do
visit root_path visit root_path
end end
context 'when the gitlab_performance_bar feature is disabled' do context 'when the performance_bar feature is disabled' do
before do before do
Feature.disable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: nil)
end end
it_behaves_like 'performance bar is disabled' it_behaves_like 'performance bar is disabled'
end end
context 'when the gitlab_performance_bar feature is enabled' do context 'when the performance_bar feature is enabled' do
before do before do
Feature.enable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: group.id)
end end
it_behaves_like 'performance bar is disabled' it_behaves_like 'performance bar is disabled'
...@@ -57,22 +59,25 @@ describe 'User can display performance bar', :js do ...@@ -57,22 +59,25 @@ describe 'User can display performance bar', :js do
context 'when user is logged-in' do context 'when user is logged-in' do
before do before do
gitlab_sign_in(create(:user)) user = create(:user)
gitlab_sign_in(user)
group.add_guest(user)
visit root_path visit root_path
end end
context 'when the gitlab_performance_bar feature is disabled' do context 'when the performance_bar feature is disabled' do
before do before do
Feature.disable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: nil)
end end
it_behaves_like 'performance bar is disabled' it_behaves_like 'performance bar is disabled'
end end
context 'when the gitlab_performance_bar feature is enabled' do context 'when the performance_bar feature is enabled' do
before do before do
Feature.enable('gitlab_performance_bar') stub_application_setting(performance_bar_allowed_group_id: group.id)
end end
it_behaves_like 'performance bar is enabled' it_behaves_like 'performance bar is enabled'
......
require 'spec_helper'
describe Gitlab::PerformanceBar do
shared_examples 'allowed user IDs are cached' do
before do
# Warm the Redis cache
described_class.enabled?(user)
end
it 'caches the allowed user IDs in cache', :caching do
expect do
expect(described_class.enabled?(user)).to be_truthy
end.not_to exceed_query_limit(0)
end
end
describe '.enabled?' do
let(:user) { create(:user) }
before do
stub_application_setting(performance_bar_allowed_group_id: -1)
end
it 'returns false when given user is nil' do
expect(described_class.enabled?(nil)).to be_falsy
end
it 'returns false when allowed_group_id is nil' do
expect(described_class).to receive(:allowed_group_id).and_return(nil)
expect(described_class.enabled?(user)).to be_falsy
end
context 'when allowed group ID does not exist' do
it 'returns false' do
expect(described_class.enabled?(user)).to be_falsy
end
end
context 'when allowed group exists' do
let!(:my_group) { create(:group, path: 'my-group') }
before do
stub_application_setting(performance_bar_allowed_group_id: my_group.id)
end
context 'when user is not a member of the allowed group' do
it 'returns false' do
expect(described_class.enabled?(user)).to be_falsy
end
it_behaves_like 'allowed user IDs are cached'
end
context 'when user is a member of the allowed group' do
before do
my_group.add_developer(user)
end
it 'returns true' do
expect(described_class.enabled?(user)).to be_truthy
end
it_behaves_like 'allowed user IDs are cached'
end
end
context 'when allowed group is nested', :nested_groups do
let!(:nested_my_group) { create(:group, parent: create(:group, path: 'my-org'), path: 'my-group') }
before do
create(:group, path: 'my-group')
nested_my_group.add_developer(user)
stub_application_setting(performance_bar_allowed_group_id: nested_my_group.id)
end
it 'returns the nested group' do
expect(described_class.enabled?(user)).to be_truthy
end
end
context 'when a nested group has the same path', :nested_groups do
before do
create(:group, :nested, path: 'my-group').add_developer(user)
end
it 'returns false' do
expect(described_class.enabled?(user)).to be_falsy
end
end
end
end
...@@ -233,6 +233,160 @@ describe ApplicationSetting, models: true do ...@@ -233,6 +233,160 @@ describe ApplicationSetting, models: true do
end end
end end
describe 'performance bar settings' do
describe 'performance_bar_allowed_group_id=' do
context 'with a blank path' do
before do
setting.performance_bar_allowed_group_id = create(:group).full_path
end
it 'persists nil for a "" path and clears allowed user IDs cache' do
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = ''
expect(setting.performance_bar_allowed_group_id).to be_nil
end
end
context 'with an invalid path' do
it 'does not persist an invalid group path' do
setting.performance_bar_allowed_group_id = 'foo'
expect(setting.performance_bar_allowed_group_id).to be_nil
end
end
context 'with a path to an existing group' do
let(:group) { create(:group) }
it 'persists a valid group path and clears allowed user IDs cache' do
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = group.full_path
expect(setting.performance_bar_allowed_group_id).to eq(group.id)
end
context 'when the given path is the same' do
context 'with a blank path' do
before do
setting.performance_bar_allowed_group_id = nil
end
it 'clears the cached allowed user IDs' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = ''
end
end
context 'with a valid path' do
before do
setting.performance_bar_allowed_group_id = group.full_path
end
it 'clears the cached allowed user IDs' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_allowed_group_id = group.full_path
end
end
end
end
end
describe 'performance_bar_allowed_group' do
context 'with no performance_bar_allowed_group_id saved' do
it 'returns nil' do
expect(setting.performance_bar_allowed_group).to be_nil
end
end
context 'with a performance_bar_allowed_group_id saved' do
let(:group) { create(:group) }
before do
setting.performance_bar_allowed_group_id = group.full_path
end
it 'returns the group' do
expect(setting.performance_bar_allowed_group).to eq(group)
end
end
end
describe 'performance_bar_enabled' do
context 'with the Performance Bar is enabled' do
let(:group) { create(:group) }
before do
setting.performance_bar_allowed_group_id = group.full_path
end
it 'returns true' do
expect(setting.performance_bar_enabled).to be_truthy
end
end
end
describe 'performance_bar_enabled=' do
context 'when the performance bar is enabled' do
let(:group) { create(:group) }
before do
setting.performance_bar_allowed_group_id = group.full_path
end
context 'when passing true' do
it 'does not clear allowed user IDs cache' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = true
expect(setting.performance_bar_allowed_group_id).to eq(group.id)
expect(setting.performance_bar_enabled).to be_truthy
end
end
context 'when passing false' do
it 'disables the performance bar and clears allowed user IDs cache' do
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = false
expect(setting.performance_bar_allowed_group_id).to be_nil
expect(setting.performance_bar_enabled).to be_falsey
end
end
end
context 'when the performance bar is disabled' do
context 'when passing true' do
it 'does nothing and does not clear allowed user IDs cache' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = true
expect(setting.performance_bar_allowed_group_id).to be_nil
expect(setting.performance_bar_enabled).to be_falsey
end
end
context 'when passing false' do
it 'does nothing and does not clear allowed user IDs cache' do
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
setting.performance_bar_enabled = false
expect(setting.performance_bar_allowed_group_id).to be_nil
expect(setting.performance_bar_enabled).to be_falsey
end
end
end
end
end
describe 'usage ping settings' do describe 'usage ping settings' do
context 'when the usage ping is disabled in gitlab.yml' do context 'when the usage ping is disabled in gitlab.yml' do
before do before do
......
...@@ -113,6 +113,20 @@ describe API::Features do ...@@ -113,6 +113,20 @@ describe API::Features do
{ 'key' => 'actors', 'value' => ["User:#{user.id}"] } { 'key' => 'actors', 'value' => ["User:#{user.id}"] }
]) ])
end end
it 'creates an enabled feature for the given user and feature group when passed user=username and feature_group=perf_team' do
post api("/features/#{feature_name}", admin), value: 'true', user: user.username, feature_group: 'perf_team'
expect(response).to have_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
{ 'key' => 'groups', 'value' => ['perf_team'] },
{ 'key' => 'actors', 'value' => ["User:#{user.id}"] }
])
end
end end
it 'creates a feature with the given percentage if passed an integer' do it 'creates a feature with the given percentage if passed an integer' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment