diff --git a/CHANGELOG b/CHANGELOG index 432d251dfc6..3bf20b639fe 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.11.0 (unreleased) - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska) - Add the `sprockets-es6` gem - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska) + - Profile requests when a header is passed v 8.10.2 (unreleased) - User can now search branches by name. !5144 diff --git a/Gemfile b/Gemfile index 8e5757eb2db..85e30a0ee6f 100644 --- a/Gemfile +++ b/Gemfile @@ -334,6 +334,8 @@ gem 'mail_room', '~> 0.8' gem 'email_reply_parser', '~> 0.5.8' +gem 'ruby-prof', '~> 0.15.9' + ## CI gem 'activerecord-session_store', '~> 1.0.0' gem 'nested_form', '~> 0.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index b6379b52f34..2039a0bb421 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -620,6 +620,7 @@ GEM rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) + ruby-prof (0.15.9) ruby-progressbar (1.8.1) ruby-saml (1.3.0) nokogiri (>= 1.5.10) @@ -948,6 +949,7 @@ DEPENDENCIES rubocop (~> 0.41.2) rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) + ruby-prof (~> 0.15.9) sanitize (~> 2.0) sass-rails (~> 5.0.0) scss_lint (~> 0.47.0) diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb new file mode 100644 index 00000000000..a478176e138 --- /dev/null +++ b/app/controllers/admin/requests_profiles_controller.rb @@ -0,0 +1,17 @@ +class Admin::RequestsProfilesController < Admin::ApplicationController + def index + @profile_token = Gitlab::RequestProfiler.profile_token + @profiles = Gitlab::RequestProfiler::Profile.all.group_by(&:request_path) + end + + def show + clean_name = Rack::Utils.clean_path_info(params[:name]) + profile = Gitlab::RequestProfiler::Profile.find(clean_name) + + if profile + render text: profile.content + else + redirect_to admin_requests_profiles_path, alert: 'Profile not found' + end + end +end diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml index 9d722bd7382..89d7a40d6b0 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/background_jobs/_head.html.haml @@ -16,3 +16,7 @@ = link_to admin_health_check_path, title: 'Health Check' do %span Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml new file mode 100644 index 00000000000..ae918086a57 --- /dev/null +++ b/app/views/admin/requests_profiles/index.html.haml @@ -0,0 +1,26 @@ +- @no_container = true +- page_title 'Requests Profiles' += render 'admin/background_jobs/head' + +%div{ class: container_class } + %h3.page-title + = page_title + + .bs-callout.clearfix + Pass the header + %code X-Profile-Token: #{@profile_token} + to profile the request + + - if @profiles.present? + .prepend-top-default + - @profiles.each do |path, profiles| + .panel.panel-default.panel-small + .panel-heading + %code= path + %ul.content-list + - profiles.each do |profile| + %li + = link_to profile.time.to_s(:long), admin_requests_profile_path(profile), data: {no_turbolink: true} + - else + %p + No profiles found diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 5ee8772882e..ac04f57e217 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -9,7 +9,7 @@ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span Overview - = nav_link(controller: %w(system_info background_jobs logs health_check)) do + = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do = link_to admin_system_info_path, title: 'Monitoring' do %span Monitoring diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb new file mode 100644 index 00000000000..9dd228a2483 --- /dev/null +++ b/app/workers/requests_profiles_worker.rb @@ -0,0 +1,9 @@ +class RequestsProfilesWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform + Gitlab::RequestProfiler.remove_all_profiles + end +end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 86f55210487..49130f37b31 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -290,6 +290,9 @@ Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'Repository Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker' +Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker' # # GitLab Shell diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb new file mode 100644 index 00000000000..fb5a7b8372e --- /dev/null +++ b/config/initializers/request_profiler.rb @@ -0,0 +1,3 @@ +Rails.application.configure do |config| + config.middleware.use(Gitlab::RequestProfiler::Middleware) +end diff --git a/config/routes.rb b/config/routes.rb index 21f3585bacd..a41a04a0b38 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -281,6 +281,7 @@ Rails.application.routes.draw do resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] + resources :requests_profiles, only: [:index, :show], param: :name resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do root to: 'projects#index', as: :projects diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb new file mode 100644 index 00000000000..8130e55351e --- /dev/null +++ b/lib/gitlab/request_profiler.rb @@ -0,0 +1,19 @@ +require 'fileutils' + +module Gitlab + module RequestProfiler + PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles" + + def profile_token + Rails.cache.fetch('profile-token') do + Devise.friendly_token + end + end + module_function :profile_token + + def remove_all_profiles + FileUtils.rm_rf(PROFILES_DIR) + end + module_function :remove_all_profiles + end +end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb new file mode 100644 index 00000000000..8da8b754975 --- /dev/null +++ b/lib/gitlab/request_profiler/middleware.rb @@ -0,0 +1,47 @@ +require 'ruby-prof' + +module Gitlab + module RequestProfiler + class Middleware + def initialize(app) + @app = app + end + + def call(env) + if profile?(env) + call_with_profiling(env) + else + @app.call(env) + end + end + + def profile?(env) + header_token = env['HTTP_X_PROFILE_TOKEN'] + return unless header_token.present? + + profile_token = RequestProfiler.profile_token + return unless profile_token.present? + + header_token == profile_token + end + + def call_with_profiling(env) + ret = nil + result = RubyProf::Profile.profile do + ret = @app.call(env) + end + + printer = RubyProf::CallStackPrinter.new(result) + file_name = "#{env['PATH_INFO'].tr('/', '|')}_#{Time.current.to_i}.html" + file_path = "#{PROFILES_DIR}/#{file_name}" + + FileUtils.mkdir_p(PROFILES_DIR) + File.open(file_path, 'wb') do |file| + printer.print(file) + end + + ret + end + end + end +end diff --git a/lib/gitlab/request_profiler/profile.rb b/lib/gitlab/request_profiler/profile.rb new file mode 100644 index 00000000000..f89d56903ef --- /dev/null +++ b/lib/gitlab/request_profiler/profile.rb @@ -0,0 +1,43 @@ +module Gitlab + module RequestProfiler + class Profile + attr_reader :name, :time, :request_path + + alias_method :to_param, :name + + def self.all + Dir["#{PROFILES_DIR}/*.html"].map do |path| + new(File.basename(path)) + end + end + + def self.find(name) + name_dup = name.dup + name_dup << '.html' unless name.end_with?('.html') + + file_path = "#{PROFILES_DIR}/#{name_dup}" + return unless File.exist?(file_path) + + new(name_dup) + end + + def initialize(name) + @name = name + + set_attributes + end + + def content + File.read("#{PROFILES_DIR}/#{name}") + end + + private + + def set_attributes + _, path, timestamp = name.split(/(.*)_(\d+)\.html$/) + @request_path = path.tr('|', '/') + @time = Time.at(timestamp.to_i).utc + end + end + end +end