From 345cd22f21e4e5a6e340c35e50b43105ee107570 Mon Sep 17 00:00:00 2001
From: Ahmad Sherif <me@ahmadsherif.com>
Date: Fri, 15 Jul 2016 17:46:39 +0200
Subject: [PATCH] Profile requests when a header is passed

---
 CHANGELOG                                     |  1 +
 Gemfile                                       |  2 +
 Gemfile.lock                                  |  2 +
 .../admin/requests_profiles_controller.rb     | 17 +++++++
 .../admin/background_jobs/_head.html.haml     |  4 ++
 .../admin/requests_profiles/index.html.haml   | 26 ++++++++++
 app/views/layouts/nav/_admin.html.haml        |  2 +-
 app/workers/requests_profiles_worker.rb       |  9 ++++
 config/initializers/1_settings.rb             |  3 ++
 config/initializers/request_profiler.rb       |  3 ++
 config/routes.rb                              |  1 +
 lib/gitlab/request_profiler.rb                | 19 ++++++++
 lib/gitlab/request_profiler/middleware.rb     | 47 +++++++++++++++++++
 lib/gitlab/request_profiler/profile.rb        | 43 +++++++++++++++++
 14 files changed, 178 insertions(+), 1 deletion(-)
 create mode 100644 app/controllers/admin/requests_profiles_controller.rb
 create mode 100644 app/views/admin/requests_profiles/index.html.haml
 create mode 100644 app/workers/requests_profiles_worker.rb
 create mode 100644 config/initializers/request_profiler.rb
 create mode 100644 lib/gitlab/request_profiler.rb
 create mode 100644 lib/gitlab/request_profiler/middleware.rb
 create mode 100644 lib/gitlab/request_profiler/profile.rb

diff --git a/CHANGELOG b/CHANGELOG
index 432d251dfc6b9..3bf20b639fe10 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 8e5757eb2db2d..85e30a0ee6f7e 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 b6379b52f34b9..2039a0bb421b7 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 0000000000000..a478176e13886
--- /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 9d722bd7382ae..89d7a40d6b021 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 0000000000000..ae918086a5756
--- /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 5ee8772882ec6..ac04f57e2172c 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 0000000000000..9dd228a248377
--- /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 86f55210487f1..49130f37b3186 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -290,6 +290,9 @@ def host(url)
 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 0000000000000..fb5a7b8372e55
--- /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 21f3585bacdc6..a41a04a0b3833 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -281,6 +281,7 @@
     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 0000000000000..8130e55351e2b
--- /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 0000000000000..8da8b75497540
--- /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 0000000000000..f89d56903efe5
--- /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
-- 
GitLab