From 1703e926a9a63a3e7e2e36657fd71d13908c2d45 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 2 Feb 2017 16:10:38 +0100 Subject: [PATCH 01/39] Add PrometheusService with API URL --- app/models/project.rb | 9 +++ .../project_services/monitoring_service.rb | 16 +++++ .../project_services/prometheus_service.rb | 71 +++++++++++++++++++ app/models/service.rb | 1 + 4 files changed, 97 insertions(+) create mode 100644 app/models/project_services/monitoring_service.rb create mode 100644 app/models/project_services/prometheus_service.rb diff --git a/app/models/project.rb b/app/models/project.rb index e06fc30dc8a1..e448d64d0bb2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -113,6 +113,7 @@ def update_forks_visibility_level has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy has_one :kubernetes_service, dependent: :destroy, inverse_of: :project + has_one :prometheus_service, dependent: :destroy, inverse_of: :project has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -769,6 +770,14 @@ def deployment_service @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) end + def monitoring_services + services.where(category: :monitoring) + end + + def monitoring_service + @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true) + end + def jira_tracker? issues_tracker.to_param == 'jira' end diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb new file mode 100644 index 000000000000..dacb144edc19 --- /dev/null +++ b/app/models/project_services/monitoring_service.rb @@ -0,0 +1,16 @@ +# Base class for monitoring services +# +# These services integrate with a deployment solution like Kubernetes/OpenShift, +# Mesosphere, etc, to provide additional features to environments. +class MonitoringService < Service + default_value_for :category, 'monitoring' + + def self.supported_events + %w() + end + + # Environments have a number of metrics + def metrics(environment, period) + raise NotImplementedError + end +end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb new file mode 100644 index 000000000000..910046dd58e8 --- /dev/null +++ b/app/models/project_services/prometheus_service.rb @@ -0,0 +1,71 @@ +class PrometheusService < MonitoringService + include ReactiveCaching + + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + + # Access to prometheus is directly through the API + prop_accessor :api_url + + with_options presence: true, if: :activated? do + validates :api_url, url: true + end + + after_save :clear_reactive_cache! + + def initialize_properties + if properties.nil? + self.properties = {} + end + end + + def title + 'Prometheus' + end + + def description + 'Prometheus integration' + end + + def help + end + + def self.to_param + 'prometheus' + end + + def fields + [ + { type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Prometheus API URL, like http://prometheus.example.com/', + } + ] + end + + # Check we can connect to the Kubernetes API + def test(*args) + { success: true, result: "Checked API discovery endpoint" } + rescue => err + { success: false, result: err } + end + + # Caches all pods in the namespace so other calls don't need to block on + # network access. + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? + + { } + end + + private + + def join_api_url(*parts) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [ prefix, *parts ].join("/") + + url.to_s + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 3ef4cbead104..2f75a2e4e7fa 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -232,6 +232,7 @@ def self.available_services_names mattermost pipelines_email pivotaltracker + prometheus pushover redmine slack_slash_commands -- GitLab From f6846dbcc63f185cba25031ae7ed9f2c9994fc6c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 2 Feb 2017 16:10:57 +0100 Subject: [PATCH 02/39] Added metrics endpoint to EnvironmentsController --- .../projects/environments_controller.rb | 15 ++++++++++++++- app/models/environment.rb | 8 ++++++++ config/routes/project.rb | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index fed75396d6e0..1b09de7bd035 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] - before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :verify_api_request!, only: :terminal_websocket_authorize def index @@ -109,6 +109,19 @@ def terminal_websocket_authorize end end + def metrics + # Currently, this acts as a hint to load the metrics details into the cache + # if they aren't there already + @metrics = environment.metrics + + respond_to do |format| + format.html + format.json do + render json: @metrics + end + end + end + private def verify_api_request! diff --git a/app/models/environment.rb b/app/models/environment.rb index 1a21b5e52b54..f28be6e2c91f 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -145,6 +145,14 @@ def terminals project.deployment_service.terminals(self) if has_terminals? end + def has_metrics? + project.monitoring_service.present? && available? && last_deployment.present? + end + + def metrics + project.deployment_service.metrics if has_metrics? + end + # An environment name is not necessarily suitable for use in URLs, DNS # or other third-party contexts, so provide a slugified version. A slug has # the following properties: diff --git a/config/routes/project.rb b/config/routes/project.rb index 84f123ff7172..75de49fbf7ad 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -154,6 +154,7 @@ member do post :stop get :terminal + get :metrics get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } end -- GitLab From 46b59386dfdd3c45a47c5a492965c5a73298fa23 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 2 Feb 2017 16:13:45 +0100 Subject: [PATCH 03/39] Added metrics views --- .../environments/_metrics_button.html.haml | 3 +++ app/views/projects/environments/metrics.html.haml | 15 +++++++++++++++ app/views/projects/environments/show.html.haml | 1 + 3 files changed, 19 insertions(+) create mode 100644 app/views/projects/environments/_metrics_button.html.haml create mode 100644 app/views/projects/environments/metrics.html.haml diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml new file mode 100644 index 000000000000..f6c3fc60029c --- /dev/null +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -0,0 +1,3 @@ +- if environment.has_metrics? && can?(current_user, :read_environment, @project) + = link_to metrics_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn metrics-button' do + = icon('metrics') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml new file mode 100644 index 000000000000..e240826cdc43 --- /dev/null +++ b/app/views/projects/environments/metrics.html.haml @@ -0,0 +1,15 @@ +- @no_container = true +- page_title "Metrics for environment", @environment.name += render "projects/pipelines/head" + +%div{ class: container_class } + .top-area + .row + .col-sm-6 + %h3.page-title + Metrics for environment + = @environment.name + + .col-sm-6 + .nav-controls + = render 'projects/deployments/actions', deployment: @environment.last_deployment diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 7036325fff80..29a98f23b888 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -8,6 +8,7 @@ %h3.page-title= @environment.name .col-md-3 .nav-controls + = render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) -- GitLab From 5ef7bdc6166d407104d6e9930f65e2dfdc9b7105 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Feb 2017 18:54:20 +0100 Subject: [PATCH 04/39] Fetch monitoring data from Prometheus TBD: - Support for testing Prometheus, - Queries to be executed, - Encode environment slug in endpoint --- .../projects/environments_controller.rb | 2 +- app/models/environment.rb | 2 +- .../project_services/monitoring_service.rb | 2 +- .../project_services/prometheus_service.rb | 48 ++++++++----- lib/gitlab/prometheus.rb | 69 +++++++++++++++++++ 5 files changed, 102 insertions(+), 21 deletions(-) create mode 100644 lib/gitlab/prometheus.rb diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 1b09de7bd035..224c8faf0e56 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -117,7 +117,7 @@ def metrics respond_to do |format| format.html format.json do - render json: @metrics + render json: @metrics, status: @metrics ? 200 : 204 end end end diff --git a/app/models/environment.rb b/app/models/environment.rb index f28be6e2c91f..bf33010fd21f 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -150,7 +150,7 @@ def has_metrics? end def metrics - project.deployment_service.metrics if has_metrics? + project.monitoring_service.metrics(self) if has_metrics? end # An environment name is not necessarily suitable for use in URLs, DNS diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb index dacb144edc19..71e456d6ea2f 100644 --- a/app/models/project_services/monitoring_service.rb +++ b/app/models/project_services/monitoring_service.rb @@ -10,7 +10,7 @@ def self.supported_events end # Environments have a number of metrics - def metrics(environment, period) + def metrics(environment) raise NotImplementedError end end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 910046dd58e8..73d35097b369 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -1,7 +1,11 @@ class PrometheusService < MonitoringService + include Gitlab::Prometheus include ReactiveCaching self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + self.reactive_cache_lease_timeout = 30.seconds + self.reactive_cache_refresh_interval = 30.seconds + self.reactive_cache_lifetime = 1.minute # Access to prometheus is directly through the API prop_accessor :api_url @@ -23,7 +27,7 @@ def title end def description - 'Prometheus integration' + 'Prometheus monitoring' end def help @@ -43,29 +47,37 @@ def fields ] end - # Check we can connect to the Kubernetes API + # Check we can connect to the Prometheus API def test(*args) - { success: true, result: "Checked API discovery endpoint" } - rescue => err + self.ping + + { success: true, result: "Checked API endpoint" } + rescue ::Gitlab::PrometheusError => err { success: false, result: err } end - # Caches all pods in the namespace so other calls don't need to block on - # network access. - def calculate_reactive_cache - return unless active? && project && !project.pending_delete? - - { } + def metrics(environment) + with_reactive_cache(environment.slug) do |data| + data + end end - private - - def join_api_url(*parts) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [ prefix, *parts ].join("/") + # Cache metrics for specific environment + def calculate_reactive_cache(environment) + return unless active? && project && !project.pending_delete? - url.to_s + # TODO: encode environment + { + success: true, + metrics: { + memory_values: query_range("go_goroutines{app=\"#{environment}\"}", 8.hours.ago), + memory_current: query("go_goroutines{app=\"#{environment}\"}"), + cpu_values: query_range("go_goroutines{app=\"#{environment}\"}", 8.hours.ago), + cpu_current: query("go_goroutines{app=\"#{environment}\"}"), + } + } + + rescue ::Gitlab::PrometheusError => err + { success: false, result: err } end end diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb new file mode 100644 index 000000000000..ea515e420e4a --- /dev/null +++ b/lib/gitlab/prometheus.rb @@ -0,0 +1,69 @@ +module Gitlab + class PrometheusError < StandardError; end + + # Helper methods to interact with Prometheus network services & resources + module Prometheus + def ping + json_api_get("ping") + end + + def query(query, time = Time.now) + response = json_api_get("query", + query: query, + time: time.utc.to_f) + + data = response.fetch('data', {}) + + if data['resultType'].to_s == 'vector' + data['result'] + end + end + + def query_range(query, start_time, end_time = Time.now, step = 1.minute) + response = json_api_get("query_range", + query: query, + start: start_time.utc.to_f, + end: end_time.utc.to_f, + step: step.to_i) + + data = response.fetch('data', {}) + + if data['resultType'].to_s == 'matrix' + data['result'] + end + end + + private + + def json_api_get(type, args = {}) + url = join_api_url(type, args) + return PrometheusError.new("invalid URL") unless url + + api_parse_response HTTParty.get(url) + rescue Errno::ECONNREFUSED + raise PrometheusError.new("connection refused") + end + + def api_parse_response(response) + if response.code == 200 and response['status'] == 'success' + response + elsif response.code == 400 + raise PrometheusError.new(response['error'] || 'bad data received') + else + raise PrometheusError.new("#{response.code} #{response.message}") + end + end + + def join_api_url(type, args = {}) + url = URI.parse(api_url) + url.path = [ + url.path.sub(%r{/+\z}, ''), + 'api', 'v1', + ERB::Util.url_encode(type) + ].join('/') + + url.query = args.to_query + url.to_s + end + end +end -- GitLab From 96b5a9bb813e3fff63e0561b953efe0149e03f0f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 3 Feb 2017 19:06:16 +0100 Subject: [PATCH 05/39] Include last_update in response --- app/models/project_services/prometheus_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 73d35097b369..038942eb5564 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -74,7 +74,8 @@ def calculate_reactive_cache(environment) memory_current: query("go_goroutines{app=\"#{environment}\"}"), cpu_values: query_range("go_goroutines{app=\"#{environment}\"}", 8.hours.ago), cpu_current: query("go_goroutines{app=\"#{environment}\"}"), - } + }, + last_update: Time.now.utc, } rescue ::Gitlab::PrometheusError => err -- GitLab From 5e1989b2698049565ab963f8fb14da603ad1db75 Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Mon, 20 Feb 2017 21:09:27 -0500 Subject: [PATCH 06/39] Change Prometheus test ping to a functional query so it returns success. --- lib/gitlab/prometheus.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb index ea515e420e4a..787c013cfb50 100644 --- a/lib/gitlab/prometheus.rb +++ b/lib/gitlab/prometheus.rb @@ -4,7 +4,7 @@ class PrometheusError < StandardError; end # Helper methods to interact with Prometheus network services & resources module Prometheus def ping - json_api_get("ping") + json_api_get("query", query: "count({__name__=~\".+\"})") end def query(query, time = Time.now) -- GitLab From 8f6d8b7458e326e6d1089599cc176208f43e311b Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Tue, 21 Feb 2017 17:05:15 -0500 Subject: [PATCH 07/39] Change query to be a simple scalar to reduce load and any chance for failure. --- lib/gitlab/prometheus.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb index 787c013cfb50..cd2ae48c7ed3 100644 --- a/lib/gitlab/prometheus.rb +++ b/lib/gitlab/prometheus.rb @@ -4,7 +4,7 @@ class PrometheusError < StandardError; end # Helper methods to interact with Prometheus network services & resources module Prometheus def ping - json_api_get("query", query: "count({__name__=~\".+\"})") + json_api_get("query", query: "1") end def query(query, time = Time.now) -- GitLab From b412efe69d8e2473c0c9f64602fac22547ab242c Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Thu, 23 Feb 2017 01:09:04 -0500 Subject: [PATCH 08/39] Update Prometheus queries for CPU and Memory. --- app/models/project_services/prometheus_service.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 038942eb5564..601bb758d989 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -70,10 +70,12 @@ def calculate_reactive_cache(environment) { success: true, metrics: { - memory_values: query_range("go_goroutines{app=\"#{environment}\"}", 8.hours.ago), - memory_current: query("go_goroutines{app=\"#{environment}\"}"), - cpu_values: query_range("go_goroutines{app=\"#{environment}\"}", 8.hours.ago), - cpu_current: query("go_goroutines{app=\"#{environment}\"}"), + #Memory used in MB + memory_values: query_range("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024", 8.hours.ago), + memory_current: query("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024"), + #CPU Usage in Seconds. + cpu_values: query_range("sum(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"})", 8.hours.ago), + cpu_current: query("sum(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"})"), }, last_update: Time.now.utc, } -- GitLab From f4d7fa16fd5d74001ad065b09a945e947a8cec14 Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Thu, 23 Feb 2017 08:16:57 -0500 Subject: [PATCH 09/39] Adjust CPU metrics for 2min rate. --- app/models/project_services/prometheus_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 601bb758d989..9b1d82588a1b 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -74,8 +74,8 @@ def calculate_reactive_cache(environment) memory_values: query_range("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024", 8.hours.ago), memory_current: query("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024"), #CPU Usage in Seconds. - cpu_values: query_range("sum(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"})", 8.hours.ago), - cpu_current: query("sum(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"})"), + cpu_values: query_range("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))", 8.hours.ago), + cpu_current: query("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))"), }, last_update: Time.now.utc, } -- GitLab From 8cac0bb39b6f59b32e3a8b9f9e712b3baf0512ee Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Fri, 24 Feb 2017 15:02:53 -0500 Subject: [PATCH 10/39] Fix wording of API URL default text --- app/models/project_services/prometheus_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 9b1d82588a1b..97010aee9b51 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -42,7 +42,7 @@ def fields { type: 'text', name: 'api_url', title: 'API URL', - placeholder: 'Prometheus API URL, like http://prometheus.example.com/', + placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/', } ] end @@ -73,7 +73,7 @@ def calculate_reactive_cache(environment) #Memory used in MB memory_values: query_range("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024", 8.hours.ago), memory_current: query("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024"), - #CPU Usage in Seconds. + #CPU Usage rate in cores. cpu_values: query_range("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))", 8.hours.ago), cpu_current: query("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))"), }, -- GitLab From ff9180584f64795de537dbd9c926fb61050e16aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 2 Mar 2017 19:47:13 +0100 Subject: [PATCH 11/39] Add a controller test and tweak the view a bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../projects/environments_controller.rb | 4 +- app/helpers/gitlab_routing_helper.rb | 4 ++ .../project_services/prometheus_service.rb | 4 +- .../environments/_metrics_button.html.haml | 9 ++-- .../projects/environments_controller_spec.rb | 46 +++++++++++++++++++ 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 224c8faf0e56..fa37963dfd4f 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -112,12 +112,12 @@ def terminal_websocket_authorize def metrics # Currently, this acts as a hint to load the metrics details into the cache # if they aren't there already - @metrics = environment.metrics + @metrics = environment.metrics || {} respond_to do |format| format.html format.json do - render json: @metrics, status: @metrics ? 200 : 204 + render json: @metrics, status: @metrics.any? ? :ok : :no_content end end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f16a63e21789..e9b7cbbad6a5 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -74,6 +74,10 @@ def environment_path(environment, *args) namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) end + def environment_metrics_path(environment, *args) + metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 97010aee9b51..be99f3ec1be3 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -70,10 +70,10 @@ def calculate_reactive_cache(environment) { success: true, metrics: { - #Memory used in MB + # Memory used in MB memory_values: query_range("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024", 8.hours.ago), memory_current: query("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024"), - #CPU Usage rate in cores. + # CPU Usage rate in cores. cpu_values: query_range("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))", 8.hours.ago), cpu_current: query("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))"), }, diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index f6c3fc60029c..bc5e55e179f7 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -1,3 +1,6 @@ -- if environment.has_metrics? && can?(current_user, :read_environment, @project) - = link_to metrics_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn metrics-button' do - = icon('metrics') +- environment = local_assigns.fetch(:environment) + +- return unless environment.has_metrics? && can?(current_user, :read_environment, environment) + += link_to environment_metrics_path(environment), class: 'btn metrics-button' do + = icon('tachometer') diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 84d119f1867d..83d80b376fba 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -187,6 +187,52 @@ end end + describe 'GET #metrics' do + before do + allow(controller).to receive(:environment).and_return(environment) + end + + context 'when environment has no metrics' do + before do + expect(environment).to receive(:metrics).and_return(nil) + end + + it 'returns a metrics page' do + get :metrics, environment_params + + expect(response).to be_ok + end + + context 'when requesting metrics as JSON' do + it 'returns a metrics JSON document' do + get :metrics, environment_params(format: :json) + + expect(response).to have_http_status(204) + expect(json_response).to eq({}) + end + end + end + + context 'when environment has some metrics' do + before do + expect(environment).to receive(:metrics).and_return({ + success: true, + metrics: {}, + last_update: 42 + }) + end + + it 'returns a metrics JSON document' do + get :metrics, environment_params(format: :json) + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['metrics']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end + end + def environment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, -- GitLab From c3e41613a72e1bd0f52e9e5ceb7bb58c000a9861 Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Fri, 3 Mar 2017 00:18:44 -0500 Subject: [PATCH 12/39] Resolve rubocop failures. --- app/models/project_services/prometheus_service.rb | 5 +++-- lib/gitlab/prometheus.rb | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index be99f3ec1be3..16539797ff4a 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -2,7 +2,7 @@ class PrometheusService < MonitoringService include Gitlab::Prometheus include ReactiveCaching - self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } self.reactive_cache_lease_timeout = 30.seconds self.reactive_cache_refresh_interval = 30.seconds self.reactive_cache_lifetime = 1.minute @@ -39,7 +39,8 @@ def self.to_param def fields [ - { type: 'text', + { + type: 'text', name: 'api_url', title: 'API URL', placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/', diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb index cd2ae48c7ed3..7f7cc400a5b8 100644 --- a/lib/gitlab/prometheus.rb +++ b/lib/gitlab/prometheus.rb @@ -45,7 +45,7 @@ def json_api_get(type, args = {}) end def api_parse_response(response) - if response.code == 200 and response['status'] == 'success' + if response.code == 200 && response['status'] == 'success' response elsif response.code == 400 raise PrometheusError.new(response['error'] || 'bad data received') -- GitLab From b198dc330081854469286a915e94e503c9eb85af Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Fri, 3 Mar 2017 08:46:43 -0500 Subject: [PATCH 13/39] Address failing API tests. --- lib/api/services.rb | 9 +++++++++ lib/api/v3/services.rb | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/api/services.rb b/lib/api/services.rb index 79a5f27dc4df..6233654b0c18 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -422,6 +422,14 @@ class Services < Grape::API desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' } ], + 'prometheus' => [ + { + required: true, + name: :api_url, + type: String, + desc: 'Prometheus API Base URL, like http://prometheus.example.com/' + } + ], 'pushover' => [ { required: true, @@ -558,6 +566,7 @@ class Services < Grape::API SlackSlashCommandsService, PipelinesEmailService, PivotaltrackerService, + PrometheusService, PushoverService, RedmineService, SlackService, diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index af0a058f69bd..59dfa10d5a02 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -423,6 +423,14 @@ class Services < Grape::API desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' } ], + 'prometheus' => [ + { + required: true, + name: :api_url, + type: String, + desc: 'Prometheus API Base URL, like http://prometheus.example.com/' + } + ], 'pushover' => [ { required: true, -- GitLab From 50ba999fa1b730dc6352d32239fdedd8a381864b Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Mon, 27 Feb 2017 17:21:03 -0600 Subject: [PATCH 14/39] Created initial version of the graph Shows the median values as well as the current cpu metrics --- app/assets/javascripts/dispatcher.js.es6 | 3 + .../monitoring/prometheus_graph.js | 284 ++++++++++++++++++ .../stylesheets/pages/environments.scss | 32 ++ .../projects/environments/metrics.html.haml | 3 + 4 files changed, 322 insertions(+) create mode 100644 app/assets/javascripts/monitoring/prometheus_graph.js diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 0f678492d4cd..a179583f054a 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -36,6 +36,7 @@ /* global Shortcuts */ const ShortcutsBlob = require('./shortcuts_blob'); +const PrometheusGraph = require('./monitoring/prometheus_graph'); //TODO: Make this a bundle const UserCallout = require('./user_callout'); (function() { @@ -278,6 +279,8 @@ const UserCallout = require('./user_callout'); case 'ci:lints:show': new gl.CILintEditor(); break; + case 'projects:environments:metrics': + new PrometheusGraph(); case 'users:show': new UserCallout(); break; diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js new file mode 100644 index 000000000000..f066543c73a4 --- /dev/null +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -0,0 +1,284 @@ +/* global d3 */ +const prometheusGraphContainer = '.prometheus-graph'; + +window.d3 = require('d3'); +window._ = require('underscore'); + +const metricsEndpoint = 'metrics.json'; +const timeFormat = d3.time.format('%H:%M'); +const dayFormat = d3.time.format('%b %e, %a'); + +class PrometheusGraph { + + constructor() { + this.margin = { top: 80, right: 0, bottom: 80, left: 100 }; + this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; + const parentContainerWidth = $(prometheusGraphContainer).parent().width(); + this.bisectDate = d3.bisector(d => d.time).left; + this.width = parentContainerWidth - this.margin.left - this.margin.right; + this.height = 400 - this.margin.top - this.margin.bottom; + this.originalWidth = parentContainerWidth; + this.originalHeight = 400; + + const self = this; + this.getData().then((metricsResponse) => { + self.data = self.transformData(metricsResponse); + self.createGraph(); + }); + } + + createGraph() { + const self = this; + _.each(self.data, (value, key) => { + // Don't create a graph if there's no data + if (value.length > 0 && (key === 'cpu_values')) { + self.plotValues(value); + } + }); + } + + plotValues(valuesToPlot) { + const self = this; + + // Mean value of the current graph + const median = d3.mean(valuesToPlot, data => data.value); + + const x = d3.time.scale() + .range([0, this.width]); + + const y = d3.scale.linear() + .range([this.height, 0]); + + const chart = d3.select(prometheusGraphContainer) + .attr('width', this.width + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.bottom + this.margin.top) + .append('g') + .attr('transform', `translate(${this.margin.left},${this.margin.top})`); + + // Chart container for the axis labels + const axisLabelContainer = d3.select(prometheusGraphContainer) + .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) + .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) + .append('g') + .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); + + x.domain(d3.extent(valuesToPlot, d => d.time)); + y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]); + + const xAxis = d3.svg.axis() + .scale(x) + .orient('bottom'); + + const yAxis = d3.svg.axis() + .scale(y) + .orient('left'); + + // Axis label container + axisLabelContainer.append('line') + .attr('class', 'label-x-axis-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: 0, + y1: this.originalHeight - this.marginLabelContainer.top, + x2: this.originalWidth, + y2: this.originalHeight - this.marginLabelContainer.top, + }); + + axisLabelContainer.append('line') + .attr('class', 'label-y-axis-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: 0, + y1: 0, + x2: 0, + y2: this.originalHeight - this.marginLabelContainer.top, + }); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('text-anchor', 'middle') + .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`) + .text('CPU Usage'); + + axisLabelContainer.append('rect') + .attr('class', 'rect-axis-text') + .attr('x', this.originalWidth / 2) + .attr('y', this.originalHeight - this.marginLabelContainer.top - 20) + .attr('width', 30) + .attr('height', 80); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('x', this.originalWidth / 2) + .attr('y', this.originalHeight - this.marginLabelContainer.top) + .attr('dy', '.35em') + .text('Time'); + + // Legends + + // Metric Usage + axisLabelContainer.append('rect') + .attr('x', this.originalWidth - 120) + .attr('y', 0) + .style('fill', '#EDF3FC') + .attr('width', 20) + .attr('height', 35); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('x', this.originalWidth - 80) + .attr('y', 10) + .text('CPU Usage'); + + axisLabelContainer.append('text') + .attr('class', 'text-metric-usage') + .attr('x', this.originalWidth - 80) + .attr('y', 30); + + // Mean value of the usage + + axisLabelContainer.append('rect') + .attr('x', this.originalWidth - 240) + .attr('y', 0) + .style('fill', '#5b99f7') + .attr('width', 20) + .attr('height', 35); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('x', this.originalWidth - 200) + .attr('y', 10) + .text('Median'); + + axisLabelContainer.append('text') + .attr('class', 'text-median-metric') + .attr('x', this.originalWidth - 200) + .attr('y', 30) + .text(median.toString().substring(0, 8)); + + + chart.append('g') + .attr('class', 'x-axis') + .attr('transform', `translate(0,${this.height})`) + .call(xAxis); + + chart.append('g') + .attr('class', 'y-axis') + .call(yAxis); + + const area = d3.svg.area() + .x(d => x(d.time)) + .y0(this.height) + .y1(d => y(d.value)) + .interpolate('linear'); + + chart.append('path') + .datum(valuesToPlot) + .attr('d', area) + .attr('class', 'cpu-values') + .style('fill', '#EDF3FC'); + + chart.append('line') + .attr('class', 'median-cpu-line') + .attr('stroke', '#5b99f7') + .attr('stroke-width', '2') + .attr({ + x1: x(d3.extent(valuesToPlot, d => d.time)[0]), + y1: y(median), + x2: x(d3.extent(valuesToPlot, d => d.time)[1]), + y2: y(median), + }); + // Overlay area for the mouseover events + chart.append('rect') + .attr('class', 'prometheus-graph-overlay') + .attr('width', this.width) + .attr('height', this.height) + .on('mousemove', function handleMouseOver() { + const x0 = x.invert(d3.mouse(this)[0]); + const i = self.bisectDate(valuesToPlot, x0, 1); + const d0 = valuesToPlot[i - 1]; + const d1 = valuesToPlot[i]; + const d = x0 - d0.time > d1.time - x0 ? d1 : d0; + // Remove the current selectors + d3.selectAll('.selected-metric-line').remove(); + d3.selectAll('.upper-circle-metric').remove(); + d3.selectAll('.lower-circle-metric').remove(); + d3.selectAll('.rect-text-metric').remove(); + d3.selectAll('.text-metric').remove(); + + chart.append('line') + .attr('class', 'selected-metric-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: x(d.time), + y1: y(0), + x2: x(d.time), + y2: y(d3.max(valuesToPlot.map(metricValue => metricValue.value))), + }); + + chart.append('circle') + .attr('class', 'upper-circle-metric') + .attr('cx', x(d.time)) + .attr('cy', y(d.value)) + .attr('r', 3); + + chart.append('circle') + .attr('class', 'lower-circle-metric') + .attr('cx', x(d.time)) + .attr('cy', y(0)) + .attr('r', 3); + + // The little box with text + const rectTextMetric = chart.append('g') + .attr('class', 'rect-text-metric') + .attr('translate', `(${x(d.time)}, ${y(d.value)})`); + + rectTextMetric.append('rect') + .attr('class', 'rect-metric') + .attr('x', x(d.time) + 10) + .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value)))) + .attr('width', 90) + .attr('height', 40); + + rectTextMetric.append('text') + .attr('class', 'text-metric') + .attr('x', x(d.time) + 35) + .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 35) + .text(timeFormat(d.time)); + + rectTextMetric.append('text') + .attr('class', 'text-metric-date') + .attr('x', x(d.time) + 15) + .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 15) + .text(dayFormat(d.time)); + + // Update the text + d3.select('.text-metric-usage') + .text(d.value.substring(0, 8)); + }); + } + + getData() { + return $.ajax({ + url: metricsEndpoint, + dataType: 'json', + }).done(metricsResponse => metricsResponse); + } + + transformData(metricsResponse) { + const metricTypes = {}; + _.each(metricsResponse.metrics, (value, key) => { + const metricValues = value[0].values; + metricTypes[key] = _.map(metricValues, metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); + }); + return metricTypes; + } +} + +module.exports = PrometheusGraph; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index f789ae1ccd3b..9fb50b064c0d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -160,3 +160,35 @@ } } } + +.prometheus-graph-overlay { + fill: none; + pointer-events: all; +} + +.rect-text-metric { + fill: white; + stroke-width: 1; + stroke: black; +} + +.rect-axis-text { + fill: white; +} + +.text-metric { + fill: black; +} + +.text-metric-date { + fill: black; + font-weight: 200; +} + +.text-metric-usage { + fill: black; +} + +.text-median-metric { + fill: black; +} diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index e240826cdc43..10986351c35b 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -13,3 +13,6 @@ .col-sm-6 .nav-controls = render 'projects/deployments/actions', deployment: @environment.last_deployment + .row + .col-sm-12 + %svg.prometheus-graph{ 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } -- GitLab From 804e527af3bc33c9d05161e23401971366afbd20 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Tue, 28 Feb 2017 18:29:57 -0600 Subject: [PATCH 15/39] Improvements to the design and code cleanup Also added backoff code --- app/assets/javascripts/dispatcher.js.es6 | 2 +- .../monitoring/prometheus_graph.js | 331 ++++++++++-------- .../stylesheets/pages/environments.scss | 40 +++ .../projects/environments/metrics.html.haml | 2 +- 4 files changed, 235 insertions(+), 140 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index a179583f054a..41dcbd2685ea 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -1,3 +1,4 @@ +import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* global UsernameValidator */ /* global ActiveTabMemoizer */ @@ -36,7 +37,6 @@ /* global Shortcuts */ const ShortcutsBlob = require('./shortcuts_blob'); -const PrometheusGraph = require('./monitoring/prometheus_graph'); //TODO: Make this a bundle const UserCallout = require('./user_callout'); (function() { diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index f066543c73a4..edc4aaf27a53 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -1,9 +1,10 @@ -/* global d3 */ -const prometheusGraphContainer = '.prometheus-graph'; - -window.d3 = require('d3'); -window._ = require('underscore'); +import d3 from 'd3'; +import _ from 'underscore'; +import statusCodes from '~/lib/utils/http_status'; +import '~/lib/utils/common_utils.js.es6'; +import Flash from '~/flash'; +const prometheusGraphContainer = '.prometheus-graph'; const metricsEndpoint = 'metrics.json'; const timeFormat = d3.time.format('%H:%M'); const dayFormat = d3.time.format('%b %e, %a'); @@ -11,19 +12,24 @@ const dayFormat = d3.time.format('%b %e, %a'); class PrometheusGraph { constructor() { - this.margin = { top: 80, right: 0, bottom: 80, left: 100 }; + this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; - const parentContainerWidth = $(prometheusGraphContainer).parent().width(); + const parentContainerWidth = $(prometheusGraphContainer).parent().width() + 100; this.bisectDate = d3.bisector(d => d.time).left; this.width = parentContainerWidth - this.margin.left - this.margin.right; this.height = 400 - this.margin.top - this.margin.bottom; this.originalWidth = parentContainerWidth; this.originalHeight = 400; + this.backOffRequestCounter = 0; const self = this; this.getData().then((metricsResponse) => { - self.data = self.transformData(metricsResponse); - self.createGraph(); + if (metricsResponse === {}) { + new Flash('Empty metrics', 'alert'); + } else { + self.data = self.transformData(metricsResponse); + self.createGraph(); + } }); } @@ -38,10 +44,8 @@ class PrometheusGraph { } plotValues(valuesToPlot) { - const self = this; - // Mean value of the current graph - const median = d3.mean(valuesToPlot, data => data.value); + this.median = d3.mean(valuesToPlot, data => data.value); const x = d3.time.scale() .range([0, this.width]); @@ -55,25 +59,70 @@ class PrometheusGraph { .append('g') .attr('transform', `translate(${this.margin.left},${this.margin.top})`); - // Chart container for the axis labels - const axisLabelContainer = d3.select(prometheusGraphContainer) - .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) - .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) - .append('g') - .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); + const axisLabelContainer = d3.select(prometheusGraphContainer) + .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) + .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) + .append('g') + .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); x.domain(d3.extent(valuesToPlot, d => d.time)); y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]); const xAxis = d3.svg.axis() .scale(x) + .ticks(3) .orient('bottom'); const yAxis = d3.svg.axis() .scale(y) + .ticks(3) + .tickSize(-this.width) .orient('left'); - // Axis label container + this.createAxisLabelContainers(axisLabelContainer); + + chart.append('g') + .attr('class', 'x-axis') + .attr('transform', `translate(0,${this.height})`) + .call(xAxis); + + chart.append('g') + .attr('class', 'y-axis') + .call(yAxis); + + const area = d3.svg.area() + .x(d => x(d.time)) + .y0(this.height) + .y1(d => y(d.value)) + .interpolate('linear'); + + const line = d3.svg.line() + .x(d => x(d.time)) + .y(d => y(d.value)); + + chart.append('path') + .datum(valuesToPlot) + .attr('d', area) + .attr('class', 'cpu-area'); + + chart.append('path') + .datum(valuesToPlot) + .attr('class', 'cpu-line') // TODO: metric line + .attr('stroke', '#5b99f7') + .attr('fill', 'none') + .attr('stroke-width', 2) + .attr('d', line); + + // Overlay area for the mouseover events + chart.append('rect') + .attr('class', 'prometheus-graph-overlay') + .attr('width', this.width) + .attr('height', this.height) + .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart)); + } + + // The legends from the metric + createAxisLabelContainers(axisLabelContainer) { axisLabelContainer.append('line') .attr('class', 'label-x-axis-line') .attr('stroke', '#000000') @@ -81,7 +130,7 @@ class PrometheusGraph { .attr({ x1: 0, y1: this.originalHeight - this.marginLabelContainer.top, - x2: this.originalWidth, + x2: this.originalWidth - this.margin.right, y2: this.originalHeight - this.marginLabelContainer.top, }); @@ -104,14 +153,14 @@ class PrometheusGraph { axisLabelContainer.append('rect') .attr('class', 'rect-axis-text') - .attr('x', this.originalWidth / 2) + .attr('x', (this.originalWidth / 2) - this.margin.right) .attr('y', this.originalHeight - this.marginLabelContainer.top - 20) .attr('width', 30) .attr('height', 80); axisLabelContainer.append('text') .attr('class', 'label-axis-text') - .attr('x', this.originalWidth / 2) + .attr('x', (this.originalWidth / 2) - this.margin.right) .attr('y', this.originalHeight - this.marginLabelContainer.top) .attr('dy', '.35em') .text('Time'); @@ -120,152 +169,158 @@ class PrometheusGraph { // Metric Usage axisLabelContainer.append('rect') - .attr('x', this.originalWidth - 120) - .attr('y', 0) + .attr('x', this.originalWidth - 170) + .attr('y', (this.originalHeight / 2) - 80) .style('fill', '#EDF3FC') .attr('width', 20) .attr('height', 35); axisLabelContainer.append('text') .attr('class', 'label-axis-text') - .attr('x', this.originalWidth - 80) - .attr('y', 10) + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 65) .text('CPU Usage'); axisLabelContainer.append('text') .attr('class', 'text-metric-usage') - .attr('x', this.originalWidth - 80) - .attr('y', 30); + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 50); // Mean value of the usage axisLabelContainer.append('rect') - .attr('x', this.originalWidth - 240) - .attr('y', 0) + .attr('x', this.originalWidth - 170) + .attr('y', (this.originalHeight / 2) - 25) .style('fill', '#5b99f7') .attr('width', 20) .attr('height', 35); axisLabelContainer.append('text') .attr('class', 'label-axis-text') - .attr('x', this.originalWidth - 200) - .attr('y', 10) + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 15) .text('Median'); axisLabelContainer.append('text') .attr('class', 'text-median-metric') - .attr('x', this.originalWidth - 200) - .attr('y', 30) - .text(median.toString().substring(0, 8)); - - - chart.append('g') - .attr('class', 'x-axis') - .attr('transform', `translate(0,${this.height})`) - .call(xAxis); - - chart.append('g') - .attr('class', 'y-axis') - .call(yAxis); - - const area = d3.svg.area() - .x(d => x(d.time)) - .y0(this.height) - .y1(d => y(d.value)) - .interpolate('linear'); + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) + 5) + .text(this.median.toString().substring(0, 8)); + } - chart.append('path') - .datum(valuesToPlot) - .attr('d', area) - .attr('class', 'cpu-values') - .style('fill', '#EDF3FC'); + handleMouseOverGraph(x, y, valuesToPlot, chart) { + const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); + const self = this; + const x0 = x.invert(d3.mouse(rectOverlay)[0]); + const i = self.bisectDate(valuesToPlot, x0, 1); + const d0 = valuesToPlot[i - 1]; + const d1 = valuesToPlot[i]; + const d = x0 - d0.time > d1.time - x0 ? d1 : d0; + // Remove the current selectors + d3.selectAll('.selected-metric-line').remove(); + d3.selectAll('.upper-circle-metric').remove(); + d3.selectAll('.lower-circle-metric').remove(); + d3.selectAll('.rect-text-metric').remove(); + d3.selectAll('.text-metric').remove(); chart.append('line') - .attr('class', 'median-cpu-line') - .attr('stroke', '#5b99f7') - .attr('stroke-width', '2') - .attr({ - x1: x(d3.extent(valuesToPlot, d => d.time)[0]), - y1: y(median), - x2: x(d3.extent(valuesToPlot, d => d.time)[1]), - y2: y(median), - }); - // Overlay area for the mouseover events - chart.append('rect') - .attr('class', 'prometheus-graph-overlay') - .attr('width', this.width) - .attr('height', this.height) - .on('mousemove', function handleMouseOver() { - const x0 = x.invert(d3.mouse(this)[0]); - const i = self.bisectDate(valuesToPlot, x0, 1); - const d0 = valuesToPlot[i - 1]; - const d1 = valuesToPlot[i]; - const d = x0 - d0.time > d1.time - x0 ? d1 : d0; - // Remove the current selectors - d3.selectAll('.selected-metric-line').remove(); - d3.selectAll('.upper-circle-metric').remove(); - d3.selectAll('.lower-circle-metric').remove(); - d3.selectAll('.rect-text-metric').remove(); - d3.selectAll('.text-metric').remove(); - - chart.append('line') - .attr('class', 'selected-metric-line') - .attr('stroke', '#000000') - .attr('stroke-width', '1') - .attr({ - x1: x(d.time), - y1: y(0), - x2: x(d.time), - y2: y(d3.max(valuesToPlot.map(metricValue => metricValue.value))), - }); + .attr('class', 'selected-metric-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: x(d.time), + y1: y(0), + x2: x(d.time), + y2: y(d3.max(valuesToPlot.map(metricValue => metricValue.value))), + }); - chart.append('circle') - .attr('class', 'upper-circle-metric') - .attr('cx', x(d.time)) - .attr('cy', y(d.value)) - .attr('r', 3); - - chart.append('circle') - .attr('class', 'lower-circle-metric') - .attr('cx', x(d.time)) - .attr('cy', y(0)) - .attr('r', 3); - - // The little box with text - const rectTextMetric = chart.append('g') - .attr('class', 'rect-text-metric') - .attr('translate', `(${x(d.time)}, ${y(d.value)})`); - - rectTextMetric.append('rect') - .attr('class', 'rect-metric') - .attr('x', x(d.time) + 10) - .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value)))) - .attr('width', 90) - .attr('height', 40); - - rectTextMetric.append('text') - .attr('class', 'text-metric') - .attr('x', x(d.time) + 35) - .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 35) - .text(timeFormat(d.time)); - - rectTextMetric.append('text') - .attr('class', 'text-metric-date') - .attr('x', x(d.time) + 15) - .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 15) - .text(dayFormat(d.time)); - - // Update the text - d3.select('.text-metric-usage') - .text(d.value.substring(0, 8)); - }); + chart.append('circle') + .attr('class', 'upper-circle-metric') + .attr('cx', x(d.time)) + .attr('cy', y(d.value)) + .attr('r', 5); + + chart.append('circle') + .attr('class', 'lower-circle-metric') + .attr('cx', x(d.time)) + .attr('cy', y(this.median)) + .attr('r', 5); + + // The little box with text + const rectTextMetric = chart.append('g') + .attr('class', 'rect-text-metric') + .attr('translate', `(${x(d.time)}, ${y(d.value)})`); + + rectTextMetric.append('rect') + .attr('class', 'rect-metric') + .attr('x', x(d.time) + 10) + .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value)))) + .attr('width', 90) + .attr('height', 40); + + rectTextMetric.append('text') + .attr('class', 'text-metric') + .attr('x', x(d.time) + 35) + .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 35) + .text(timeFormat(d.time)); + + rectTextMetric.append('text') + .attr('class', 'text-metric-date') + .attr('x', x(d.time) + 15) + .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 15) + .text(dayFormat(d.time)); + + // Update the text + d3.select('.text-metric-usage') + .text(d.value.substring(0, 8)); + } + + metricsService() { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', metricsEndpoint, true); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200 || xhr.status === 204) { + const data = JSON.parse(xhr.responseText); + return resolve({ + status: xhr.status, + metrics: data, + }); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); } getData() { - return $.ajax({ - url: metricsEndpoint, - dataType: 'json', - }).done(metricsResponse => metricsResponse); + const maxNumberOfRequests = 3; + const self = this; + return gl.utils.backOff((next, stop) => { + self.metricsService() + .then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + if (this.backOffRequestCounter < maxNumberOfRequests) { + next(); + } else { + stop(resp); + } + } else { + stop(resp); + } + }).catch(stop); + }) + .then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + return {}; + } + return resp.metrics; + }) + .catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); } transformData(metricsResponse) { @@ -281,4 +336,4 @@ class PrometheusGraph { } } -module.exports = PrometheusGraph; +export default PrometheusGraph; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 9fb50b064c0d..00f76a833b57 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -161,8 +161,48 @@ } } +.prometheus-graph { + text { + fill: #AAA; + } +} + +.x-axis path, +.y-axis path{ + fill: none; + stroke: #AAA; + stroke-width: 1; + shape-rendering: crispEdges; +} + +.y-axis { + line { + stroke: #AAA; + stroke-width: 1; + } +} + +.label-x-axis-line, +.label-y-axis-line{ + fill: none; + stroke: #E6E6E6; + stroke-width: 1; + shape-rendering: crispEdges; +} + +.cpu-area { + opacity: 0.8; + fill: #EDF3FC; +} + +.upper-circle-metric, +.lower-circle-metric { + fill: #5b99f7; +} + .prometheus-graph-overlay { fill: none; + opacity: 0.0; pointer-events: all; } diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 10986351c35b..46bfb6a641e0 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -15,4 +15,4 @@ = render 'projects/deployments/actions', deployment: @environment.last_deployment .row .col-sm-12 - %svg.prometheus-graph{ 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } + %svg.prometheus-graph{ 'data-base-endpoint' => metrics_namespace_project_environment_path(@project.namespace, @project, @environment) } -- GitLab From dc800eedd1f88a5e4ec734db57b0b21346f94d8a Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Wed, 1 Mar 2017 18:01:46 -0600 Subject: [PATCH 16/39] Code cleanup Also both graphs now show on the view --- .../monitoring/prometheus_graph.js | 166 +++++++++++------- .../stylesheets/pages/environments.scss | 51 +++--- .../projects/environments/metrics.html.haml | 5 +- 3 files changed, 128 insertions(+), 94 deletions(-) diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index edc4aaf27a53..25b223366751 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -4,30 +4,33 @@ import statusCodes from '~/lib/utils/http_status'; import '~/lib/utils/common_utils.js.es6'; import Flash from '~/flash'; -const prometheusGraphContainer = '.prometheus-graph'; +const prometheusGraphsContainer = '.prometheus-graph'; const metricsEndpoint = 'metrics.json'; const timeFormat = d3.time.format('%H:%M'); const dayFormat = d3.time.format('%b %e, %a'); +const bisectDate = d3.bisector(d => d.time).left; +const extraAddedWidthParent = 100; class PrometheusGraph { constructor() { this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; - const parentContainerWidth = $(prometheusGraphContainer).parent().width() + 100; - this.bisectDate = d3.bisector(d => d.time).left; - this.width = parentContainerWidth - this.margin.left - this.margin.right; - this.height = 400 - this.margin.top - this.margin.bottom; + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + + extraAddedWidthParent; this.originalWidth = parentContainerWidth; this.originalHeight = 400; + this.width = parentContainerWidth - this.margin.left - this.margin.right; + this.height = 400 - this.margin.top - this.margin.bottom; this.backOffRequestCounter = 0; + this.configureGraph(); const self = this; this.getData().then((metricsResponse) => { if (metricsResponse === {}) { new Flash('Empty metrics', 'alert'); } else { - self.data = self.transformData(metricsResponse); + self.transformData(metricsResponse); self.createGraph(); } }); @@ -35,17 +38,16 @@ class PrometheusGraph { createGraph() { const self = this; - _.each(self.data, (value, key) => { - // Don't create a graph if there's no data - if (value.length > 0 && (key === 'cpu_values')) { - self.plotValues(value); + _.each(this.data, (value, key) => { + if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) { + self.plotValues(value, key); } }); } - plotValues(valuesToPlot) { + plotValues(valuesToPlot, key) { // Mean value of the current graph - this.median = d3.mean(valuesToPlot, data => data.value); + const mean = d3.mean(valuesToPlot, data => data.value); const x = d3.time.scale() .range([0, this.width]); @@ -53,6 +55,10 @@ class PrometheusGraph { const y = d3.scale.linear() .range([this.height, 0]); + const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; + + const graphSpecifics = this.graphSpecificProperties[key]; + const chart = d3.select(prometheusGraphContainer) .attr('width', this.width + this.margin.left + this.margin.right) .attr('height', this.height + this.margin.bottom + this.margin.top) @@ -70,16 +76,16 @@ class PrometheusGraph { const xAxis = d3.svg.axis() .scale(x) - .ticks(3) + .ticks(this.commonGraphProperties.axis_no_ticks) .orient('bottom'); const yAxis = d3.svg.axis() .scale(y) - .ticks(3) + .ticks(this.commonGraphProperties.axis_no_ticks) .tickSize(-this.width) .orient('left'); - this.createAxisLabelContainers(axisLabelContainer); + this.createAxisLabelContainers(axisLabelContainer, mean, key); chart.append('g') .attr('class', 'x-axis') @@ -103,14 +109,15 @@ class PrometheusGraph { chart.append('path') .datum(valuesToPlot) .attr('d', area) - .attr('class', 'cpu-area'); + .attr('class', 'metric-area') + .attr('fill', graphSpecifics.area_fill_color); chart.append('path') .datum(valuesToPlot) - .attr('class', 'cpu-line') // TODO: metric line - .attr('stroke', '#5b99f7') + .attr('class', 'metric-line') + .attr('stroke', graphSpecifics.line_color) .attr('fill', 'none') - .attr('stroke-width', 2) + .attr('stroke-width', this.commonGraphProperties.area_stroke_width) .attr('d', line); // Overlay area for the mouseover events @@ -118,11 +125,13 @@ class PrometheusGraph { .attr('class', 'prometheus-graph-overlay') .attr('width', this.width) .attr('height', this.height) - .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart)); + .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, mean, key)); } // The legends from the metric - createAxisLabelContainers(axisLabelContainer) { + createAxisLabelContainers(axisLabelContainer, mean, key) { + const graphSpecifics = this.graphSpecificProperties[key]; + axisLabelContainer.append('line') .attr('class', 'label-x-axis-line') .attr('stroke', '#000000') @@ -149,7 +158,7 @@ class PrometheusGraph { .attr('class', 'label-axis-text') .attr('text-anchor', 'middle') .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`) - .text('CPU Usage'); + .text(graphSpecifics.graph_legend_title); axisLabelContainer.append('rect') .attr('class', 'rect-axis-text') @@ -171,7 +180,7 @@ class PrometheusGraph { axisLabelContainer.append('rect') .attr('x', this.originalWidth - 170) .attr('y', (this.originalHeight / 2) - 80) - .style('fill', '#EDF3FC') + .style('fill', graphSpecifics.area_fill_color) .attr('width', 20) .attr('height', 35); @@ -179,7 +188,7 @@ class PrometheusGraph { .attr('class', 'label-axis-text') .attr('x', this.originalWidth - 140) .attr('y', (this.originalHeight / 2) - 65) - .text('CPU Usage'); + .text(graphSpecifics.graph_legend_title); axisLabelContainer.append('text') .attr('class', 'text-metric-usage') @@ -191,7 +200,7 @@ class PrometheusGraph { axisLabelContainer.append('rect') .attr('x', this.originalWidth - 170) .attr('y', (this.originalHeight / 2) - 25) - .style('fill', '#5b99f7') + .style('fill', graphSpecifics.line_color) .attr('width', 20) .attr('height', 35); @@ -205,74 +214,99 @@ class PrometheusGraph { .attr('class', 'text-median-metric') .attr('x', this.originalWidth - 140) .attr('y', (this.originalHeight / 2) + 5) - .text(this.median.toString().substring(0, 8)); + .text(mean.toString().substring(0, 8)); } - handleMouseOverGraph(x, y, valuesToPlot, chart) { + handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, mean, key) { const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); - const self = this; - const x0 = x.invert(d3.mouse(rectOverlay)[0]); - const i = self.bisectDate(valuesToPlot, x0, 1); - const d0 = valuesToPlot[i - 1]; - const d1 = valuesToPlot[i]; - const d = x0 - d0.time > d1.time - x0 ? d1 : d0; + const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]); + const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1); + const d0 = valuesToPlot[timeValueIndex - 1]; + const d1 = valuesToPlot[timeValueIndex]; + const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0; + const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value))); + const currentTimeCoordinate = x(currentData.time); + const graphSpecifics = this.graphSpecificProperties[key]; // Remove the current selectors - d3.selectAll('.selected-metric-line').remove(); - d3.selectAll('.upper-circle-metric').remove(); - d3.selectAll('.lower-circle-metric').remove(); - d3.selectAll('.rect-text-metric').remove(); - d3.selectAll('.text-metric').remove(); + d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove(); + d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove(); + d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove(); + d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove(); chart.append('line') .attr('class', 'selected-metric-line') - .attr('stroke', '#000000') - .attr('stroke-width', '1') .attr({ - x1: x(d.time), + x1: currentTimeCoordinate, y1: y(0), - x2: x(d.time), - y2: y(d3.max(valuesToPlot.map(metricValue => metricValue.value))), + x2: currentTimeCoordinate, + y2: maxValueMetric, }); chart.append('circle') - .attr('class', 'upper-circle-metric') - .attr('cx', x(d.time)) - .attr('cy', y(d.value)) - .attr('r', 5); + .attr('class', 'circle-metric') + .attr('fill', graphSpecifics.line_color) + .attr('cx', currentTimeCoordinate) + .attr('cy', y(currentData.value)) + .attr('r', this.commonGraphProperties.circle_radius_metric); chart.append('circle') - .attr('class', 'lower-circle-metric') - .attr('cx', x(d.time)) - .attr('cy', y(this.median)) - .attr('r', 5); + .attr('class', 'circle-metric') + .attr('fill', graphSpecifics.line_color) + .attr('cx', currentTimeCoordinate) + .attr('cy', y(mean)) + .attr('r', this.commonGraphProperties.circle_radius_metric); // The little box with text const rectTextMetric = chart.append('g') .attr('class', 'rect-text-metric') - .attr('translate', `(${x(d.time)}, ${y(d.value)})`); + .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`); rectTextMetric.append('rect') .attr('class', 'rect-metric') - .attr('x', x(d.time) + 10) - .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value)))) - .attr('width', 90) - .attr('height', 40); + .attr('x', currentTimeCoordinate + 10) + .attr('y', maxValueMetric) + .attr('width', this.commonGraphProperties.rect_text_width) + .attr('height', this.commonGraphProperties.rect_text_height); rectTextMetric.append('text') .attr('class', 'text-metric') - .attr('x', x(d.time) + 35) - .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 35) - .text(timeFormat(d.time)); + .attr('x', currentTimeCoordinate + 35) + .attr('y', maxValueMetric + 35) + .text(timeFormat(currentData.time)); rectTextMetric.append('text') .attr('class', 'text-metric-date') - .attr('x', x(d.time) + 15) - .attr('y', y(d3.max(valuesToPlot.map(metricValue => metricValue.value))) + 15) - .text(dayFormat(d.time)); + .attr('x', currentTimeCoordinate + 15) + .attr('y', maxValueMetric + 15) + .text(dayFormat(currentData.time)); // Update the text - d3.select('.text-metric-usage') - .text(d.value.substring(0, 8)); + d3.select(`${prometheusGraphContainer} .text-metric-usage`) + .text(currentData.value.substring(0, 8)); + } + + configureGraph() { + this.graphSpecificProperties = { + cpu_values: { + area_fill_color: '#edf3fc', + line_color: '#5b99f7', + graph_legend_title: 'CPU Usage', + }, + memory_values: { + area_fill_color: '#fca326', + line_color: '#fc6d26', + graph_legend_title: 'Memory Usage', + }, + }; + + this.commonGraphProperties = { + area_stroke_width: 2, + median_total_characters: 8, + circle_radius_metric: 5, + rect_text_width: 90, + rect_text_height: 40, + axis_no_ticks: 3, + }; } metricsService() { @@ -281,7 +315,7 @@ class PrometheusGraph { xhr.open('GET', metricsEndpoint, true); xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200 || xhr.status === 204) { + if (xhr.status === statusCodes.OK || xhr.status === statusCodes.NO_CONTENT) { const data = JSON.parse(xhr.responseText); return resolve({ status: xhr.status, @@ -332,7 +366,7 @@ class PrometheusGraph { value: metric[1], })); }); - return metricTypes; + this.data = metricTypes; } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 00f76a833b57..69262a8b7798 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -168,36 +168,34 @@ } .x-axis path, -.y-axis path{ +.y-axis path, +.label-x-axis-line, +.label-y-axis-line +{ fill: none; - stroke: #AAA; stroke-width: 1; shape-rendering: crispEdges; } -.y-axis { - line { - stroke: #AAA; - stroke-width: 1; - } +.x-axis path, +.y-axis path { + stroke: $stat-graph-axis-fill; } .label-x-axis-line, .label-y-axis-line{ - fill: none; stroke: #E6E6E6; - stroke-width: 1; - shape-rendering: crispEdges; } -.cpu-area { - opacity: 0.8; - fill: #EDF3FC; +.y-axis { + line { + stroke: $stat-graph-axis-fill; + stroke-width: 1; + } } -.upper-circle-metric, -.lower-circle-metric { - fill: #5b99f7; +.metric-area { + opacity: 0.8; } .prometheus-graph-overlay { @@ -207,28 +205,27 @@ } .rect-text-metric { - fill: white; + fill: $white-light; stroke-width: 1; stroke: black; } .rect-axis-text { - fill: white; + fill: $white-light; } -.text-metric { - fill: black; +.text-metric, +.text-median-metric, +.text-metric-usage, +.text-metric-date { + fill: $black; } .text-metric-date { - fill: black; font-weight: 200; } -.text-metric-usage { - fill: black; -} - -.text-median-metric { - fill: black; +.selected-metric-line { + stroke: $black; + stroke-width: 1; } diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 46bfb6a641e0..9bb5cd7bb230 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -13,6 +13,9 @@ .col-sm-6 .nav-controls = render 'projects/deployments/actions', deployment: @environment.last_deployment + .row{ 'data-base-endpoint' => metrics_namespace_project_environment_path(@project.namespace, @project, @environment) } + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } .row .col-sm-12 - %svg.prometheus-graph{ 'data-base-endpoint' => metrics_namespace_project_environment_path(@project.namespace, @project, @environment) } + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } -- GitLab From 4983a21c0faccfe4a6ee5432d0ac38a36a78a156 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Wed, 1 Mar 2017 18:37:46 -0600 Subject: [PATCH 17/39] Changed the getData method a $.ajax call Also fixed some CSS inconsistencies --- .../monitoring/prometheus_graph.js | 41 +++++++------------ .../stylesheets/pages/environments.scss | 13 +++--- .../projects/environments/metrics.html.haml | 2 +- 3 files changed, 21 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 25b223366751..4943b18c9160 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -309,44 +309,31 @@ class PrometheusGraph { }; } - metricsService() { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', metricsEndpoint, true); - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === statusCodes.OK || xhr.status === statusCodes.NO_CONTENT) { - const data = JSON.parse(xhr.responseText); - return resolve({ - status: xhr.status, - metrics: data, - }); - } else { - return reject([xhr.responseText, xhr.status]); - } - } - }; - xhr.send(); - }); - } - getData() { const maxNumberOfRequests = 3; - const self = this; return gl.utils.backOff((next, stop) => { - self.metricsService() - .then((resp) => { + $.ajax({ + url: metricsEndpoint, + dataType: 'json', + }) + .done((data, statusText, resp) => { if (resp.status === statusCodes.NO_CONTENT) { this.backOffRequestCounter = this.backOffRequestCounter += 1; if (this.backOffRequestCounter < maxNumberOfRequests) { next(); } else { - stop(resp); + stop({ + status: resp.status, + metrics: data, + }); } } else { - stop(resp); + stop({ + status: resp.status, + metrics: data, + }); } - }).catch(stop); + }).fail(stop); }) .then((resp) => { if (resp.status === statusCodes.NO_CONTENT) { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 69262a8b7798..f087c51f905c 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -163,15 +163,14 @@ .prometheus-graph { text { - fill: #AAA; - } + fill: $stat-graph-axis-fill; + } } .x-axis path, .y-axis path, .label-x-axis-line, -.label-y-axis-line -{ +.label-y-axis-line { fill: none; stroke-width: 1; shape-rendering: crispEdges; @@ -183,8 +182,8 @@ } .label-x-axis-line, -.label-y-axis-line{ - stroke: #E6E6E6; +.label-y-axis-line { + stroke: $border-color; } .y-axis { @@ -207,7 +206,7 @@ .rect-text-metric { fill: $white-light; stroke-width: 1; - stroke: black; + stroke: $black; } .rect-axis-text { diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 9bb5cd7bb230..edd597c1c877 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -13,7 +13,7 @@ .col-sm-6 .nav-controls = render 'projects/deployments/actions', deployment: @environment.last_deployment - .row{ 'data-base-endpoint' => metrics_namespace_project_environment_path(@project.namespace, @project, @environment) } + .row .col-sm-12 %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } .row -- GitLab From 2e8807e5f752421ceee0586045ac5cc762ce1f9e Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 3 Mar 2017 10:37:21 -0600 Subject: [PATCH 18/39] Added initial version of specs --- .../fixtures/environments/metrics.html.haml | 12 + .../monitoring/prometheus_graph_spec.js | 81 ++ .../monitoring/prometheus_mock_data.js | 1014 +++++++++++++++++ 3 files changed, 1107 insertions(+) create mode 100644 spec/javascripts/fixtures/environments/metrics.html.haml create mode 100644 spec/javascripts/monitoring/prometheus_graph_spec.js create mode 100644 spec/javascripts/monitoring/prometheus_mock_data.js diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml new file mode 100644 index 000000000000..483063fb889f --- /dev/null +++ b/spec/javascripts/fixtures/environments/metrics.html.haml @@ -0,0 +1,12 @@ +%div + .top-area + .row + .col-sm-6 + %h3.page-title + Metrics for environment + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } \ No newline at end of file diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js new file mode 100644 index 000000000000..0e6906cfb4dd --- /dev/null +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -0,0 +1,81 @@ +import '~/lib/utils/common_utils.js.es6'; +import PrometheusGraph from '~/monitoring/prometheus_graph'; +import { prometheusMockData } from './prometheus_mock_data'; + +require('es6-promise').polyfill(); + +fdescribe('PrometheusGraph', () => { + const fixtureName = 'static/environments/metrics.html.raw'; + // let promiseHelper = {}; + // let getDataPromise = {}; + + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + this.prometheusGraph = new PrometheusGraph(); + + this.ajaxSpy = spyOn($, 'ajax').and.callFake((req) => { + req.success(prometheusMockData); + }); + + // const fetchPromise = new Promise((res, rej) => { + // promiseHelper = { + // resolve: res, + // reject: rej, + // }; + // }); + + // spyOn(this.prometheusGraph, 'getData').and.returnValue(fetchPromise); + // getDataPromise = this.prometheusGraph.getData(); + spyOn(gl.utils, 'backOff').and.callFake(() => { + const promise = new Promise(); + promise.resolve(prometheusMockData.metrics); + return promise; + }); + }); + + it('initializes graph properties', () => { + // Test for the measurements + expect(this.prometheusGraph.margin).toBeDefined(); + expect(this.prometheusGraph.marginLabelContainer).toBeDefined(); + expect(this.prometheusGraph.originalWidth).toBeDefined(); + expect(this.prometheusGraph.originalHeight).toBeDefined(); + expect(this.prometheusGraph.height).toBeDefined(); + expect(this.prometheusGraph.width).toBeDefined(); + expect(this.prometheusGraph.backOffRequestCounter).toBeDefined(); + // Test for the graph properties (colors, radius, etc.) + expect(this.prometheusGraph.graphSpecificProperties).toBeDefined(); + expect(this.prometheusGraph.commonGraphProperties).toBeDefined(); + }); + + it('getData should be called', () => { + }); + + + + // describe('fetches the data from the prometheus integration', () => { + // beforeEach((done) => { + // promiseHelper.resolve(prometheusMockData.metrics); + // done(); + // }); + + // it('calls the getData method', () => { + // expect(this.prometheusGraph.getData).toHaveBeenCalled(); + // }); + + // it('resolves the getData promise', (done) => { + // getDataPromise.then((metricsResponse) => { + // expect(metricsResponse).toBeDefined(); + // done(); + // }); + // }); + + // it('transforms the data after fetch', () => { + // getDataPromise.then((metricsResponse) => { + // this.transformData(metricsResponse.metrics); + // expect(this.data).toBeDefined(); + // }); + // }); + // }); +}); diff --git a/spec/javascripts/monitoring/prometheus_mock_data.js b/spec/javascripts/monitoring/prometheus_mock_data.js new file mode 100644 index 000000000000..1cdc14faaa89 --- /dev/null +++ b/spec/javascripts/monitoring/prometheus_mock_data.js @@ -0,0 +1,1014 @@ +/* eslint-disable import/prefer-default-export*/ +export const prometheusMockData = { + status: 200, + metrics: { + success: true, + metrics: { + memory_values: [ + { + metric: { + }, + values: [ + [ + 1488462917.256, + '10.12890625', + ], + [ + 1488462977.256, + '10.140625', + ], + [ + 1488463037.256, + '10.140625', + ], + [ + 1488463097.256, + '10.14453125', + ], + [ + 1488463157.256, + '10.1484375', + ], + [ + 1488463217.256, + '10.15625', + ], + [ + 1488463277.256, + '10.15625', + ], + [ + 1488463337.256, + '10.15625', + ], + [ + 1488463397.256, + '10.1640625', + ], + [ + 1488463457.256, + '10.171875', + ], + [ + 1488463517.256, + '10.171875', + ], + [ + 1488463577.256, + '10.171875', + ], + [ + 1488463637.256, + '10.18359375', + ], + [ + 1488463697.256, + '10.1953125', + ], + [ + 1488463757.256, + '10.203125', + ], + [ + 1488463817.256, + '10.20703125', + ], + [ + 1488463877.256, + '10.20703125', + ], + [ + 1488463937.256, + '10.20703125', + ], + [ + 1488463997.256, + '10.20703125', + ], + [ + 1488464057.256, + '10.2109375', + ], + [ + 1488464117.256, + '10.2109375', + ], + [ + 1488464177.256, + '10.2109375', + ], + [ + 1488464237.256, + '10.2109375', + ], + [ + 1488464297.256, + '10.21484375', + ], + [ + 1488464357.256, + '10.22265625', + ], + [ + 1488464417.256, + '10.22265625', + ], + [ + 1488464477.256, + '10.2265625', + ], + [ + 1488464537.256, + '10.23046875', + ], + [ + 1488464597.256, + '10.23046875', + ], + [ + 1488464657.256, + '10.234375', + ], + [ + 1488464717.256, + '10.234375', + ], + [ + 1488464777.256, + '10.234375', + ], + [ + 1488464837.256, + '10.234375', + ], + [ + 1488464897.256, + '10.234375', + ], + [ + 1488464957.256, + '10.234375', + ], + [ + 1488465017.256, + '10.23828125', + ], + [ + 1488465077.256, + '10.23828125', + ], + [ + 1488465137.256, + '10.2421875', + ], + [ + 1488465197.256, + '10.2421875', + ], + [ + 1488465257.256, + '10.2421875', + ], + [ + 1488465317.256, + '10.2421875', + ], + [ + 1488465377.256, + '10.2421875', + ], + [ + 1488465437.256, + '10.2421875', + ], + [ + 1488465497.256, + '10.2421875', + ], + [ + 1488465557.256, + '10.2421875', + ], + [ + 1488465617.256, + '10.2421875', + ], + [ + 1488465677.256, + '10.2421875', + ], + [ + 1488465737.256, + '10.2421875', + ], + [ + 1488465797.256, + '10.24609375', + ], + [ + 1488465857.256, + '10.25', + ], + [ + 1488465917.256, + '10.25390625', + ], + [ + 1488465977.256, + '9.98828125', + ], + [ + 1488466037.256, + '9.9921875', + ], + [ + 1488466097.256, + '9.9921875', + ], + [ + 1488466157.256, + '9.99609375', + ], + [ + 1488466217.256, + '10', + ], + [ + 1488466277.256, + '10.00390625', + ], + [ + 1488466337.256, + '10.0078125', + ], + [ + 1488466397.256, + '10.01171875', + ], + [ + 1488466457.256, + '10.0234375', + ], + [ + 1488466517.256, + '10.02734375', + ], + [ + 1488466577.256, + '10.02734375', + ], + [ + 1488466637.256, + '10.03125', + ], + [ + 1488466697.256, + '10.03125', + ], + [ + 1488466757.256, + '10.03125', + ], + [ + 1488466817.256, + '10.03125', + ], + [ + 1488466877.256, + '10.03125', + ], + [ + 1488466937.256, + '10.03125', + ], + [ + 1488466997.256, + '10.03125', + ], + [ + 1488467057.256, + '10.0390625', + ], + [ + 1488467117.256, + '10.0390625', + ], + [ + 1488467177.256, + '10.04296875', + ], + [ + 1488467237.256, + '10.05078125', + ], + [ + 1488467297.256, + '10.05859375', + ], + [ + 1488467357.256, + '10.06640625', + ], + [ + 1488467417.256, + '10.06640625', + ], + [ + 1488467477.256, + '10.0703125', + ], + [ + 1488467537.256, + '10.07421875', + ], + [ + 1488467597.256, + '10.0859375', + ], + [ + 1488467657.256, + '10.0859375', + ], + [ + 1488467717.256, + '10.09765625', + ], + [ + 1488467777.256, + '10.1015625', + ], + [ + 1488467837.256, + '10.10546875', + ], + [ + 1488467897.256, + '10.10546875', + ], + [ + 1488467957.256, + '10.125', + ], + [ + 1488468017.256, + '10.13671875', + ], + [ + 1488468077.256, + '10.1484375', + ], + [ + 1488468137.256, + '10.15625', + ], + [ + 1488468197.256, + '10.16796875', + ], + [ + 1488468257.256, + '10.171875', + ], + [ + 1488468317.256, + '10.171875', + ], + [ + 1488468377.256, + '10.171875', + ], + [ + 1488468437.256, + '10.171875', + ], + [ + 1488468497.256, + '10.171875', + ], + [ + 1488468557.256, + '10.171875', + ], + [ + 1488468617.256, + '10.171875', + ], + [ + 1488468677.256, + '10.17578125', + ], + [ + 1488468737.256, + '10.17578125', + ], + [ + 1488468797.256, + '10.265625', + ], + [ + 1488468857.256, + '10.19921875', + ], + [ + 1488468917.256, + '10.19921875', + ], + [ + 1488468977.256, + '10.19921875', + ], + [ + 1488469037.256, + '10.19921875', + ], + [ + 1488469097.256, + '10.19921875', + ], + [ + 1488469157.256, + '10.203125', + ], + [ + 1488469217.256, + '10.43359375', + ], + [ + 1488469277.256, + '10.20703125', + ], + [ + 1488469337.256, + '10.2109375', + ], + [ + 1488469397.256, + '10.22265625', + ], + [ + 1488469457.256, + '10.21484375', + ], + [ + 1488469517.256, + '10.21484375', + ], + [ + 1488469577.256, + '10.21484375', + ], + [ + 1488469637.256, + '10.22265625', + ], + [ + 1488469697.256, + '10.234375', + ], + [ + 1488469757.256, + '10.234375', + ], + [ + 1488469817.256, + '10.234375', + ], + [ + 1488469877.256, + '10.2421875', + ], + [ + 1488469937.256, + '10.25', + ], + [ + 1488469997.256, + '10.25390625', + ], + [ + 1488470057.256, + '10.26171875', + ], + [ + 1488470117.256, + '10.2734375', + ], + ], + }, + ], + memory_current: [ + { + metric: { + }, + value: [ + 1488470117.737, + '10.2734375', + ], + }, + ], + cpu_values: [ + { + metric: { + }, + values: [ + [ + 1488462918.15, + '0.0002996458625058103', + ], + [ + 1488462978.15, + '0.0002652382333333314', + ], + [ + 1488463038.15, + '0.0003485461333333421', + ], + [ + 1488463098.15, + '0.0003420421999999886', + ], + [ + 1488463158.15, + '0.00023107150000001297', + ], + [ + 1488463218.15, + '0.00030463981666664826', + ], + [ + 1488463278.15, + '0.0002477177833333677', + ], + [ + 1488463338.15, + '0.00026936656666665115', + ], + [ + 1488463398.15, + '0.000406264750000022', + ], + [ + 1488463458.15, + '0.00029592802026561453', + ], + [ + 1488463518.15, + '0.00023426999683316343', + ], + [ + 1488463578.15, + '0.0003057080666666915', + ], + [ + 1488463638.15, + '0.0003408470500000149', + ], + [ + 1488463698.15, + '0.00025497336666665166', + ], + [ + 1488463758.15, + '0.0003009282833333534', + ], + [ + 1488463818.15, + '0.0003119383499999924', + ], + [ + 1488463878.15, + '0.00028719019999998705', + ], + [ + 1488463938.15, + '0.000327864749999988', + ], + [ + 1488463998.15, + '0.0002514917333333422', + ], + [ + 1488464058.15, + '0.0003614651166666742', + ], + [ + 1488464118.15, + '0.0003221668000000122', + ], + [ + 1488464178.15, + '0.00023323083333330884', + ], + [ + 1488464238.15, + '0.00028531499475009274', + ], + [ + 1488464298.15, + '0.0002627695294921391', + ], + [ + 1488464358.15, + '0.00027145463333333453', + ], + [ + 1488464418.15, + '0.00025669488333335266', + ], + [ + 1488464478.15, + '0.00022307761666665965', + ], + [ + 1488464538.15, + '0.0003307265833333517', + ], + [ + 1488464598.15, + '0.0002817050666666709', + ], + [ + 1488464658.15, + '0.00022357458333332285', + ], + [ + 1488464718.15, + '0.00032648590000000275', + ], + [ + 1488464778.15, + '0.00028410750000000816', + ], + [ + 1488464838.15, + '0.0003038076999999954', + ], + [ + 1488464898.15, + '0.00037568226666667335', + ], + [ + 1488464958.15, + '0.00020160354999999202', + ], + [ + 1488465018.15, + '0.0003229403333333399', + ], + [ + 1488465078.15, + '0.00033516069999999236', + ], + [ + 1488465138.15, + '0.0003365978333333371', + ], + [ + 1488465198.15, + '0.00020262178333331585', + ], + [ + 1488465258.15, + '0.00040567498333331876', + ], + [ + 1488465318.15, + '0.00029114155000001436', + ], + [ + 1488465378.15, + '0.0002498841000000122', + ], + [ + 1488465438.15, + '0.00027296763333331715', + ], + [ + 1488465498.15, + '0.0002958794000000135', + ], + [ + 1488465558.15, + '0.0002922354666666867', + ], + [ + 1488465618.15, + '0.00034186624999999653', + ], + [ + 1488465678.15, + '0.0003397984166666627', + ], + [ + 1488465738.15, + '0.0002658284166666469', + ], + [ + 1488465798.15, + '0.00026221139999999346', + ], + [ + 1488465858.15, + '0.00029467960000001034', + ], + [ + 1488465918.15, + '0.0002634141333333358', + ], + [ + 1488465978.15, + '0.0003202958333333209', + ], + [ + 1488466038.15, + '0.00037890760000000394', + ], + [ + 1488466098.15, + '0.00023453356666666518', + ], + [ + 1488466158.15, + '0.0002866827333333433', + ], + [ + 1488466218.15, + '0.0003335935499999998', + ], + [ + 1488466278.15, + '0.00022787131666666125', + ], + [ + 1488466338.15, + '0.00033821938333333064', + ], + [ + 1488466398.15, + '0.00029233375000001043', + ], + [ + 1488466458.15, + '0.00026562758333333514', + ], + [ + 1488466518.15, + '0.0003142600999999819', + ], + [ + 1488466578.15, + '0.00027392178333333444', + ], + [ + 1488466638.15, + '0.00028178598333334173', + ], + [ + 1488466698.15, + '0.0002463400666666911', + ], + [ + 1488466758.15, + '0.00040234373333332125', + ], + [ + 1488466818.15, + '0.00023677453333332822', + ], + [ + 1488466878.15, + '0.00030852703333333523', + ], + [ + 1488466938.15, + '0.0003582272833333455', + ], + [ + 1488466998.15, + '0.0002176380833332973', + ], + [ + 1488467058.15, + '0.00026180203333335447', + ], + [ + 1488467118.15, + '0.00027862966666667436', + ], + [ + 1488467178.15, + '0.0002769731166666567', + ], + [ + 1488467238.15, + '0.0002832899166666477', + ], + [ + 1488467298.15, + '0.0003446533500000311', + ], + [ + 1488467358.15, + '0.0002691345999999761', + ], + [ + 1488467418.15, + '0.000284919933333357', + ], + [ + 1488467478.15, + '0.0002396026166666528', + ], + [ + 1488467538.15, + '0.00035625295000002075', + ], + [ + 1488467598.15, + '0.00036759816666664946', + ], + [ + 1488467658.15, + '0.00030326608333333855', + ], + [ + 1488467718.15, + '0.00023584972418043393', + ], + [ + 1488467778.15, + '0.00025744508892115107', + ], + [ + 1488467838.15, + '0.00036737541666663395', + ], + [ + 1488467898.15, + '0.00034325741666666094', + ], + [ + 1488467958.15, + '0.00026390046666667407', + ], + [ + 1488468018.15, + '0.0003302534500000102', + ], + [ + 1488468078.15, + '0.00035243794999999527', + ], + [ + 1488468138.15, + '0.00020149738333333407', + ], + [ + 1488468198.15, + '0.0003183469666666679', + ], + [ + 1488468258.15, + '0.0003835329166666845', + ], + [ + 1488468318.15, + '0.0002485075333333124', + ], + [ + 1488468378.15, + '0.0003011457166666768', + ], + [ + 1488468438.15, + '0.00032242785497684965', + ], + [ + 1488468498.15, + '0.0002659713747457531', + ], + [ + 1488468558.15, + '0.0003476860333333202', + ], + [ + 1488468618.15, + '0.00028336403333334794', + ], + [ + 1488468678.15, + '0.00017132354999998728', + ], + [ + 1488468738.15, + '0.0003001915833333276', + ], + [ + 1488468798.15, + '0.0003025715666666725', + ], + [ + 1488468858.15, + '0.0003012370166666815', + ], + [ + 1488468918.15, + '0.00030203619999997025', + ], + [ + 1488468978.15, + '0.0002804355000000314', + ], + [ + 1488469038.15, + '0.00033194884999998564', + ], + [ + 1488469098.15, + '0.00025201496666665455', + ], + [ + 1488469158.15, + '0.0002777531500000189', + ], + [ + 1488469218.15, + '0.0003314885833333392', + ], + [ + 1488469278.15, + '0.0002234891422095589', + ], + [ + 1488469338.15, + '0.000349117355867791', + ], + [ + 1488469398.15, + '0.0004036731333333303', + ], + [ + 1488469458.15, + '0.00024553911666667835', + ], + [ + 1488469518.15, + '0.0003056456833333184', + ], + [ + 1488469578.15, + '0.0002618737166666681', + ], + [ + 1488469638.15, + '0.00022972643333331414', + ], + [ + 1488469698.15, + '0.0003713522500000307', + ], + [ + 1488469758.15, + '0.00018322576666666515', + ], + [ + 1488469818.15, + '0.00034534762753952466', + ], + [ + 1488469878.15, + '0.00028200510008501677', + ], + [ + 1488469938.15, + '0.0002773708499999768', + ], + [ + 1488469998.15, + '0.00027547160000001013', + ], + [ + 1488470058.15, + '0.00031713610000000023', + ], + [ + 1488470118.15, + '0.00035276853333332525', + ], + ], + }, + ], + cpu_current: [ + { + metric: { + }, + value: [ + 1488470118.566, + '0.00035276853333332525', + ], + }, + ], + last_update: '2017-03-02T15:55:18.981Z', + }, + }, +}; -- GitLab From 5739c2693f0f3524793b0261a51b6312e6d4bac1 Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Fri, 3 Mar 2017 14:32:14 -0500 Subject: [PATCH 19/39] Add Prometheus to import/export model list. --- spec/lib/gitlab/import_export/all_models.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 06617f3b0074..d075c0711d11 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -135,6 +135,7 @@ project: - slack_slash_commands_service - irker_service - pivotaltracker_service +- prometheus_service - hipchat_service - flowdock_service - assembla_service -- GitLab From af17318b98666fe889aa4fd0a1846e1bb84f0008 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 3 Mar 2017 17:56:10 -0600 Subject: [PATCH 20/39] Improved spec coverage on prometheus_graph.js --- .../monitoring/prometheus_graph.js | 21 +++-- .../monitoring/prometheus_graph_spec.js | 91 +++++++++---------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 4943b18c9160..66e6cd25fbb6 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -24,7 +24,19 @@ class PrometheusGraph { this.height = 400 - this.margin.top - this.margin.bottom; this.backOffRequestCounter = 0; this.configureGraph(); + this.init(); + } + + createGraph() { + const self = this; + _.each(this.data, (value, key) => { + if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) { + self.plotValues(value, key); + } + }); + } + init() { const self = this; this.getData().then((metricsResponse) => { if (metricsResponse === {}) { @@ -36,15 +48,6 @@ class PrometheusGraph { }); } - createGraph() { - const self = this; - _.each(this.data, (value, key) => { - if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) { - self.plotValues(value, key); - } - }); - } - plotValues(valuesToPlot, key) { // Mean value of the current graph const mean = d3.mean(valuesToPlot, data => data.value); diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index 0e6906cfb4dd..db134841d8ed 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,38 +1,28 @@ import '~/lib/utils/common_utils.js.es6'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; +import 'jquery'; +import es6Promise from 'es6-promise'; -require('es6-promise').polyfill(); +es6Promise.polyfill(); fdescribe('PrometheusGraph', () => { const fixtureName = 'static/environments/metrics.html.raw'; - // let promiseHelper = {}; - // let getDataPromise = {}; + const prometheusGraphContainer = '.prometheus-graph'; + const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`; + let originalTimeout = {}; preloadFixtures(fixtureName); beforeEach(() => { loadFixtures(fixtureName); this.prometheusGraph = new PrometheusGraph(); - - this.ajaxSpy = spyOn($, 'ajax').and.callFake((req) => { - req.success(prometheusMockData); - }); - - // const fetchPromise = new Promise((res, rej) => { - // promiseHelper = { - // resolve: res, - // reject: rej, - // }; - // }); - - // spyOn(this.prometheusGraph, 'getData').and.returnValue(fetchPromise); - // getDataPromise = this.prometheusGraph.getData(); - spyOn(gl.utils, 'backOff').and.callFake(() => { - const promise = new Promise(); - promise.resolve(prometheusMockData.metrics); - return promise; - }); + const self = this; + const fakeInit = function(metricsResponse) { + self.prometheusGraph.transformData(metricsResponse); + self.prometheusGraph.createGraph(); + } + spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit); }); it('initializes graph properties', () => { @@ -49,33 +39,42 @@ fdescribe('PrometheusGraph', () => { expect(this.prometheusGraph.commonGraphProperties).toBeDefined(); }); - it('getData should be called', () => { + it('transforms the data', () => { + this.prometheusGraph.init(prometheusMockData.metrics); + expect(this.prometheusGraph.data).toBeDefined(); + expect(this.prometheusGraph.data.cpu_values.length).toBe(121); + expect(this.prometheusGraph.data.memory_values.length).toBe(121); }); + it('creates two graphs', () => { + this.prometheusGraph.init(prometheusMockData.metrics); + expect($(prometheusGraphContainer).length).toBe(2); + }); + describe('Graph contents', () => { + beforeEach(() => { + this.prometheusGraph.init(prometheusMockData.metrics); + }); - // describe('fetches the data from the prometheus integration', () => { - // beforeEach((done) => { - // promiseHelper.resolve(prometheusMockData.metrics); - // done(); - // }); - - // it('calls the getData method', () => { - // expect(this.prometheusGraph.getData).toHaveBeenCalled(); - // }); - - // it('resolves the getData promise', (done) => { - // getDataPromise.then((metricsResponse) => { - // expect(metricsResponse).toBeDefined(); - // done(); - // }); - // }); + it('has axis, an area, a line and a overlay', () => { + const $prometheusGraphContents = $(prometheusGraphContents); + const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent(); + expect($graphContainer.find('.x-axis')).toBeDefined(); + expect($graphContainer.find('.y-axis')).toBeDefined(); + expect($graphContainer.find('.prometheus-graph-overlay')).toBeDefined(); + expect($graphContainer.find('.metric-line')).toBeDefined(); + expect($graphContainer.find('.metric-area')).toBeDefined(); + }); - // it('transforms the data after fetch', () => { - // getDataPromise.then((metricsResponse) => { - // this.transformData(metricsResponse.metrics); - // expect(this.data).toBeDefined(); - // }); - // }); - // }); + it('has legends, labels and an extra axis that labels the metrics', () => { + const $prometheusGraphContents = $(prometheusGraphContents); + const $axisLabelContainer = $(prometheusGraphContents).find('.label-x-axis-line').parent(); + expect($prometheusGraphContents.find('.label-x-axis-line')).toBeDefined(); + expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined(); + expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined(); + expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined(); + expect($axisLabelContainer.find('rect').length).toBe(3); + expect($axisLabelContainer.find('text').length).toBe(6); + }); + }); }); -- GitLab From 69866fc6204a79eb0a87a902d4e96e2a0e9474fc Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 3 Mar 2017 18:07:47 -0600 Subject: [PATCH 21/39] Removed the median from the current graphs, also, fixed tests --- .../monitoring/prometheus_graph.js | 40 +++---------------- .../monitoring/prometheus_graph_spec.js | 16 ++++---- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 66e6cd25fbb6..a344af8139c3 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -1,3 +1,4 @@ +/* eslint-disable no-new*/ import d3 from 'd3'; import _ from 'underscore'; import statusCodes from '~/lib/utils/http_status'; @@ -49,9 +50,6 @@ class PrometheusGraph { } plotValues(valuesToPlot, key) { - // Mean value of the current graph - const mean = d3.mean(valuesToPlot, data => data.value); - const x = d3.time.scale() .range([0, this.width]); @@ -88,7 +86,7 @@ class PrometheusGraph { .tickSize(-this.width) .orient('left'); - this.createAxisLabelContainers(axisLabelContainer, mean, key); + this.createAxisLabelContainers(axisLabelContainer, key); chart.append('g') .attr('class', 'x-axis') @@ -128,11 +126,11 @@ class PrometheusGraph { .attr('class', 'prometheus-graph-overlay') .attr('width', this.width) .attr('height', this.height) - .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, mean, key)); + .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key)); } // The legends from the metric - createAxisLabelContainers(axisLabelContainer, mean, key) { + createAxisLabelContainers(axisLabelContainer, key) { const graphSpecifics = this.graphSpecificProperties[key]; axisLabelContainer.append('line') @@ -197,30 +195,9 @@ class PrometheusGraph { .attr('class', 'text-metric-usage') .attr('x', this.originalWidth - 140) .attr('y', (this.originalHeight / 2) - 50); - - // Mean value of the usage - - axisLabelContainer.append('rect') - .attr('x', this.originalWidth - 170) - .attr('y', (this.originalHeight / 2) - 25) - .style('fill', graphSpecifics.line_color) - .attr('width', 20) - .attr('height', 35); - - axisLabelContainer.append('text') - .attr('class', 'label-axis-text') - .attr('x', this.originalWidth - 140) - .attr('y', (this.originalHeight / 2) - 15) - .text('Median'); - - axisLabelContainer.append('text') - .attr('class', 'text-median-metric') - .attr('x', this.originalWidth - 140) - .attr('y', (this.originalHeight / 2) + 5) - .text(mean.toString().substring(0, 8)); } - handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, mean, key) { + handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) { const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]); const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1); @@ -252,13 +229,6 @@ class PrometheusGraph { .attr('cy', y(currentData.value)) .attr('r', this.commonGraphProperties.circle_radius_metric); - chart.append('circle') - .attr('class', 'circle-metric') - .attr('fill', graphSpecifics.line_color) - .attr('cx', currentTimeCoordinate) - .attr('cy', y(mean)) - .attr('r', this.commonGraphProperties.circle_radius_metric); - // The little box with text const rectTextMetric = chart.append('g') .attr('class', 'rect-text-metric') diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index db134841d8ed..34d6f5f7dd82 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,16 +1,15 @@ +import 'jquery'; +import es6Promise from 'es6-promise'; import '~/lib/utils/common_utils.js.es6'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; -import 'jquery'; -import es6Promise from 'es6-promise'; es6Promise.polyfill(); -fdescribe('PrometheusGraph', () => { +describe('PrometheusGraph', () => { const fixtureName = 'static/environments/metrics.html.raw'; const prometheusGraphContainer = '.prometheus-graph'; const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`; - let originalTimeout = {}; preloadFixtures(fixtureName); @@ -18,10 +17,10 @@ fdescribe('PrometheusGraph', () => { loadFixtures(fixtureName); this.prometheusGraph = new PrometheusGraph(); const self = this; - const fakeInit = function(metricsResponse) { + const fakeInit = (metricsResponse) => { self.prometheusGraph.transformData(metricsResponse); self.prometheusGraph.createGraph(); - } + }; spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit); }); @@ -57,7 +56,6 @@ fdescribe('PrometheusGraph', () => { }); it('has axis, an area, a line and a overlay', () => { - const $prometheusGraphContents = $(prometheusGraphContents); const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent(); expect($graphContainer.find('.x-axis')).toBeDefined(); expect($graphContainer.find('.y-axis')).toBeDefined(); @@ -73,8 +71,8 @@ fdescribe('PrometheusGraph', () => { expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined(); expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined(); expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined(); - expect($axisLabelContainer.find('rect').length).toBe(3); - expect($axisLabelContainer.find('text').length).toBe(6); + expect($axisLabelContainer.find('rect').length).toBe(2); + expect($axisLabelContainer.find('text').length).toBe(4); }); }); }); -- GitLab From 9c893e54370ee356e011f5b00f4c9eac32b15bdf Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Fri, 3 Mar 2017 21:05:07 -0500 Subject: [PATCH 22/39] Update monitoring service comment text, and add Prometheus service help text. --- app/models/project_services/monitoring_service.rb | 4 ++-- app/models/project_services/prometheus_service.rb | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb index 71e456d6ea2f..ea585721e8f1 100644 --- a/app/models/project_services/monitoring_service.rb +++ b/app/models/project_services/monitoring_service.rb @@ -1,7 +1,7 @@ # Base class for monitoring services # -# These services integrate with a deployment solution like Kubernetes/OpenShift, -# Mesosphere, etc, to provide additional features to environments. +# These services integrate with a deployment solution like Prometheus +# to provide additional features for environments. class MonitoringService < Service default_value_for :category, 'monitoring' diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 16539797ff4a..f6dec782b24a 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -31,6 +31,7 @@ def description end def help + 'Retrives `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.' end def self.to_param -- GitLab From c95f1a1508d90656db2d4cfde85b1caeda8f35ef Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Sat, 4 Mar 2017 19:27:40 -0700 Subject: [PATCH 23/39] Update metrics button icon, clarify legend text and page title. --- app/assets/javascripts/monitoring/prometheus_graph.js | 4 ++-- app/views/projects/environments/_metrics_button.html.haml | 2 +- app/views/projects/environments/metrics.html.haml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index a344af8139c3..6f4cebc14b8b 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -263,12 +263,12 @@ class PrometheusGraph { cpu_values: { area_fill_color: '#edf3fc', line_color: '#5b99f7', - graph_legend_title: 'CPU Usage', + graph_legend_title: 'CPU Usage (Cores)', }, memory_values: { area_fill_color: '#fca326', line_color: '#fc6d26', - graph_legend_title: 'Memory Usage', + graph_legend_title: 'Memory Usage (MB)', }, }; diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index bc5e55e179f7..8cf0f84277d1 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -3,4 +3,4 @@ - return unless environment.has_metrics? && can?(current_user, :read_environment, environment) = link_to environment_metrics_path(environment), class: 'btn metrics-button' do - = icon('tachometer') + = icon('area-chart') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index edd597c1c877..dc8dda6229f4 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -7,7 +7,7 @@ .row .col-sm-6 %h3.page-title - Metrics for environment + Environment: = @environment.name .col-sm-6 -- GitLab From 29e23595c92600c161b1c2bf42ba970234c102d9 Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Sun, 5 Mar 2017 09:16:46 -0700 Subject: [PATCH 24/39] Fix trailing space on metrics.html.haml --- app/views/projects/environments/metrics.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index dc8dda6229f4..f8e94ca98aeb 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -7,7 +7,7 @@ .row .col-sm-6 %h3.page-title - Environment: + Environment: = @environment.name .col-sm-6 -- GitLab From 44325b484dadae90a7ca2088300fc80ac10c0338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 00:56:03 +0100 Subject: [PATCH 25/39] Add specs for Gitlab::Prometheus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../project_services/prometheus_service.rb | 23 ++- lib/gitlab/prometheus.rb | 85 +++++---- spec/lib/gitlab/prometheus_spec.rb | 180 ++++++++++++++++++ 3 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 spec/lib/gitlab/prometheus_spec.rb diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index f6dec782b24a..1ada77c269cd 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -1,5 +1,4 @@ class PrometheusService < MonitoringService - include Gitlab::Prometheus include ReactiveCaching self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } @@ -31,7 +30,7 @@ def description end def help - 'Retrives `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.' + 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.' end def self.to_param @@ -51,10 +50,10 @@ def fields # Check we can connect to the Prometheus API def test(*args) - self.ping + client.ping { success: true, result: "Checked API endpoint" } - rescue ::Gitlab::PrometheusError => err + rescue Gitlab::PrometheusError => err { success: false, result: err } end @@ -73,16 +72,20 @@ def calculate_reactive_cache(environment) success: true, metrics: { # Memory used in MB - memory_values: query_range("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024", 8.hours.ago), - memory_current: query("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024"), + memory_values: client.query_range("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024", start: 8.hours.ago), + memory_current: client.query("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024"), # CPU Usage rate in cores. - cpu_values: query_range("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))", 8.hours.ago), - cpu_current: query("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))"), + cpu_values: client.query_range("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))", start: 8.hours.ago), + cpu_current: client.query("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))"), }, - last_update: Time.now.utc, + last_update: Time.now.utc } - rescue ::Gitlab::PrometheusError => err + rescue Gitlab::PrometheusError => err { success: false, result: err } end + + def client + @prometheus ||= Gitlab::Prometheus.new(api_url: api_url) + end end diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb index 7f7cc400a5b8..2ef399cd388a 100644 --- a/lib/gitlab/prometheus.rb +++ b/lib/gitlab/prometheus.rb @@ -1,69 +1,70 @@ module Gitlab - class PrometheusError < StandardError; end + PrometheusError = Class.new(StandardError) # Helper methods to interact with Prometheus network services & resources - module Prometheus - def ping - json_api_get("query", query: "1") - end + class Prometheus + attr_reader :api_url - def query(query, time = Time.now) - response = json_api_get("query", - query: query, - time: time.utc.to_f) + def initialize(api_url:) + @api_url = api_url + end - data = response.fetch('data', {}) + def ping + json_api_get('query', query: '1') + end - if data['resultType'].to_s == 'vector' - data['result'] + def query(query) + get_result('vector') do + json_api_get('query', query: query, time: Time.now.utc.to_f) end end - def query_range(query, start_time, end_time = Time.now, step = 1.minute) - response = json_api_get("query_range", - query: query, - start: start_time.utc.to_f, - end: end_time.utc.to_f, - step: step.to_i) - - data = response.fetch('data', {}) - - if data['resultType'].to_s == 'matrix' - data['result'] + def query_range(query, start:) + get_result('matrix') do + json_api_get('query_range', + query: query, + start: start.utc.to_f, + end: Time.now.utc.to_f, + step: 1.minute.to_i) end end private def json_api_get(type, args = {}) - url = join_api_url(type, args) - return PrometheusError.new("invalid URL") unless url - - api_parse_response HTTParty.get(url) + get(join_api_url(type, args)) rescue Errno::ECONNREFUSED - raise PrometheusError.new("connection refused") + raise PrometheusError, 'Connection refused' + end + + def join_api_url(type, args = {}) + url = URI.parse(api_url) + rescue URI::Error + raise PrometheusError, "Invalid API URL: #{api_url}" + else + url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/') + url.query = args.to_query + + url.to_s + end + + def get(url) + handle_response(HTTParty.get(url)) end - def api_parse_response(response) + def handle_response(response) if response.code == 200 && response['status'] == 'success' - response + response['data'] || {} elsif response.code == 400 - raise PrometheusError.new(response['error'] || 'bad data received') + raise PrometheusError, response['error'] || 'Bad data received' else - raise PrometheusError.new("#{response.code} #{response.message}") + raise PrometheusError, "#{response.code} - #{response.message}" end end - def join_api_url(type, args = {}) - url = URI.parse(api_url) - url.path = [ - url.path.sub(%r{/+\z}, ''), - 'api', 'v1', - ERB::Util.url_encode(type) - ].join('/') - - url.query = args.to_query - url.to_s + def get_result(expected_type) + data = yield + data['result'] if data['resultType'] == expected_type end end end diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb new file mode 100644 index 000000000000..7c88829944b7 --- /dev/null +++ b/spec/lib/gitlab/prometheus_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' + +describe Gitlab::Prometheus, lib: true do + subject { described_class.new(api_url: 'http://example.org') } + + def success_response(data = {}) + parsed_body = { 'status' => 'success', 'data' => data } + request = double(options: {}) + response = double(code: 200, to_hash: {}) + parsed_block = -> { parsed_body } + + HTTParty::Response.new(request, response, parsed_block, body: parsed_body.to_json) + end + + def failure_response(code, parsed_body = {}) + request = double(options: {}) + response = double(code: code, to_hash: {}, message: 'FAIL!') + parsed_block = -> { parsed_body } + + HTTParty::Response.new(request, response, parsed_block, body: parsed_body.to_json) + end + + describe '#ping' do + it 'issues a "query" request to the API endpoint' do + response = success_response + expect(HTTParty) + .to receive(:get) + .with('http://example.org/api/v1/query?query=1') + .and_return(response) + + subject.ping + end + end + + # This shared examples expect: + # - current_time: A time instance to freeze the time to + # - execute_query: A query call + shared_examples 'failure response' do + context 'when request returns 400 with an error message' do + let(:response) { failure_response(400, 'error' => 'bar!') } + + it 'raises a Gitlab::PrometheusError error' do + Timecop.freeze(current_time) do + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, 'bar!') + end + end + end + + context 'when request returns 400 without an error message' do + let(:response) { failure_response(400) } + + it 'raises a Gitlab::PrometheusError error' do + Timecop.freeze(current_time) do + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, 'Bad data received') + end + end + end + + context 'when request returns 500' do + let(:response) { failure_response(500) } + + it 'raises a Gitlab::PrometheusError error' do + Timecop.freeze(current_time) do + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, '500 - FAIL!') + end + end + end + end + + describe '#query' do + let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } + let(:current_time) { Time.now.utc } + + before do + Timecop.freeze(current_time) do + query = { + query: prometheus_query, + time: current_time.to_f + }.to_query + + expect(HTTParty) + .to receive(:get) + .with("http://example.org/api/v1/query?#{query}") + .and_return(response) + end + end + + context 'when request returns vector results' do + let(:response) { success_response('resultType' => 'vector', 'result' => 'foo') } + + it 'returns data from the API call' do + Timecop.freeze(current_time) do + expect(subject.query(prometheus_query)).to eq 'foo' + end + end + end + + context 'when request returns matrix results' do + let(:response) { success_response('resultType' => 'matrix', 'result' => 'foo') } + + it 'returns nil' do + Timecop.freeze(current_time) do + expect(subject.query(prometheus_query)).to be_nil + end + end + end + + context 'when request returns no data' do + let(:response) { success_response(nil) } + + it 'returns nil' do + Timecop.freeze(current_time) do + expect(subject.query(prometheus_query)).to be_nil + end + end + end + + it_behaves_like 'failure response' do + let(:execute_query) { subject.query(prometheus_query) } + end + end + + describe '#query_range' do + let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } + let(:current_time) { Time.now.utc } + + before do + Timecop.freeze(current_time) do + query = { + query: prometheus_query, + start: current_time.to_f, + end: current_time.to_f, + step: 1.minute.to_i + }.to_query + + expect(HTTParty) + .to receive(:get) + .with("http://example.org/api/v1/query_range?#{query}") + .and_return(response) + end + end + + context 'when request returns vector results' do + let(:response) { success_response('resultType' => 'vector', 'result' => 'foo') } + + it 'returns mil' do + Timecop.freeze(current_time) do + expect(subject.query_range(prometheus_query, start: current_time)).to be_nil + end + end + end + + context 'when request returns matrix results' do + let(:response) { success_response('resultType' => 'matrix', 'result' => 'foo') } + + it 'returns data from the API call' do + Timecop.freeze(current_time) do + expect(subject.query_range(prometheus_query, start: current_time)).to eq 'foo' + end + end + end + + context 'when request returns no data' do + let(:response) { success_response(nil) } + + it 'returns nil' do + Timecop.freeze(current_time) do + expect(subject.query_range(prometheus_query, start: current_time)).to be_nil + end + end + end + + it_behaves_like 'failure response' do + let(:execute_query) { subject.query_range(prometheus_query, start: current_time) } + end + end +end -- GitLab From fe9e51032a4eb59821e46d637fa69f4d0a5aa1aa Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Sun, 5 Mar 2017 21:26:23 -0700 Subject: [PATCH 26/39] Resolve javascript es6 extension issues, indenting. --- .../javascripts/monitoring/prometheus_graph.js | 2 +- .../monitoring/prometheus_graph_spec.js | 2 +- spec/lib/gitlab/prometheus_spec.rb | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 1a8bf1d00005..9384fe3f276a 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -2,7 +2,7 @@ import d3 from 'd3'; import _ from 'underscore'; import statusCodes from '~/lib/utils/http_status'; -import '~/lib/utils/common_utils.js'; +import '~/lib/utils/common_utils'; import Flash from '~/flash'; const prometheusGraphsContainer = '.prometheus-graph'; diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index 34d6f5f7dd82..823b4bab7fc5 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,6 +1,6 @@ import 'jquery'; import es6Promise from 'es6-promise'; -import '~/lib/utils/common_utils.js.es6'; +import '~/lib/utils/common_utils'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb index 7c88829944b7..7c176c0f0cab 100644 --- a/spec/lib/gitlab/prometheus_spec.rb +++ b/spec/lib/gitlab/prometheus_spec.rb @@ -81,10 +81,10 @@ def failure_response(code, parsed_body = {}) time: current_time.to_f }.to_query - expect(HTTParty) - .to receive(:get) - .with("http://example.org/api/v1/query?#{query}") - .and_return(response) + expect(HTTParty) + .to receive(:get) + .with("http://example.org/api/v1/query?#{query}") + .and_return(response) end end @@ -136,10 +136,10 @@ def failure_response(code, parsed_body = {}) step: 1.minute.to_i }.to_query - expect(HTTParty) - .to receive(:get) - .with("http://example.org/api/v1/query_range?#{query}") - .and_return(response) + expect(HTTParty) + .to receive(:get) + .with("http://example.org/api/v1/query_range?#{query}") + .and_return(response) end end -- GitLab From fc97a025134151a7341a602fd7ddc51762bd8cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 11:19:19 +0100 Subject: [PATCH 27/39] Don't pass time for `query` Prometheus queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/models/project_services/prometheus_service.rb | 4 ++-- lib/gitlab/prometheus.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 1ada77c269cd..27c164dfbb08 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -72,8 +72,8 @@ def calculate_reactive_cache(environment) success: true, metrics: { # Memory used in MB - memory_values: client.query_range("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024", start: 8.hours.ago), - memory_current: client.query("sum(container_memory_usage_bytes{container_name=\"app\", environment=\"#{environment}\"})/1024/1024"), + memory_values: client.query_range("sum(container_memory_usage_bytes{container_name=\"app\",environment=\"#{environment}\"})/1024/1024", start: 8.hours.ago), + memory_current: client.query("sum(container_memory_usage_bytes{container_name=\"app\",environment=\"#{environment}\"})/1024/1024"), # CPU Usage rate in cores. cpu_values: client.query_range("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))", start: 8.hours.ago), cpu_current: client.query("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))"), diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb index 2ef399cd388a..ebff2e7f5ec8 100644 --- a/lib/gitlab/prometheus.rb +++ b/lib/gitlab/prometheus.rb @@ -15,7 +15,7 @@ def ping def query(query) get_result('vector') do - json_api_get('query', query: query, time: Time.now.utc.to_f) + json_api_get('query', query: query) end end -- GitLab From 0f24dd33de127ffcc08a20581c104c2c64756902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 12:38:59 +0100 Subject: [PATCH 28/39] Improve Gitlab::Prometheus specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/gitlab/prometheus.rb | 2 +- spec/lib/gitlab/prometheus_spec.rb | 152 +++++++++++++---------------- spec/support/prometheus_helpers.rb | 47 +++++++++ 3 files changed, 116 insertions(+), 85 deletions(-) create mode 100644 spec/support/prometheus_helpers.rb diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb index ebff2e7f5ec8..ea06f34bae9a 100644 --- a/lib/gitlab/prometheus.rb +++ b/lib/gitlab/prometheus.rb @@ -58,7 +58,7 @@ def handle_response(response) elsif response.code == 400 raise PrometheusError, response['error'] || 'Bad data received' else - raise PrometheusError, "#{response.code} - #{response.message}" + raise PrometheusError, "#{response.code} - #{response.body}" end end diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb index 7c176c0f0cab..99b7351cb1a0 100644 --- a/spec/lib/gitlab/prometheus_spec.rb +++ b/spec/lib/gitlab/prometheus_spec.rb @@ -1,120 +1,103 @@ require 'spec_helper' describe Gitlab::Prometheus, lib: true do - subject { described_class.new(api_url: 'http://example.org') } + include PrometheusHelpers - def success_response(data = {}) - parsed_body = { 'status' => 'success', 'data' => data } - request = double(options: {}) - response = double(code: 200, to_hash: {}) - parsed_block = -> { parsed_body } + subject { described_class.new(api_url: 'https://prometheus.example.com') } - HTTParty::Response.new(request, response, parsed_block, body: parsed_body.to_json) + def success_response(url, body) + WebMock.stub_request(:get, url) + .to_return({ + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: body.to_json + }) end - def failure_response(code, parsed_body = {}) - request = double(options: {}) - response = double(code: code, to_hash: {}, message: 'FAIL!') - parsed_block = -> { parsed_body } - - HTTParty::Response.new(request, response, parsed_block, body: parsed_body.to_json) + def failure_response(url, code, parsed_body = {}) + WebMock.stub_request(:get, url) + .to_return({ + status: code, + headers: { 'Content-Type' => 'application/json' }, + body: parsed_body.to_json + }) end describe '#ping' do it 'issues a "query" request to the API endpoint' do - response = success_response - expect(HTTParty) - .to receive(:get) - .with('http://example.org/api/v1/query?query=1') - .and_return(response) + req_stub = success_response('https://prometheus.example.com/api/v1/query?query=1', prometheus_value_body('vector')) - subject.ping + expect(subject.ping).to eq({ "resultType"=>"vector", "result"=>[{"metric"=>{}, "value"=>[1488772511.004, "0.000041021495238095323"]}] }) + expect(req_stub).to have_been_requested end end # This shared examples expect: - # - current_time: A time instance to freeze the time to + # - query_url: A query URL # - execute_query: A query call shared_examples 'failure response' do context 'when request returns 400 with an error message' do - let(:response) { failure_response(400, 'error' => 'bar!') } + let!(:req_stub) { failure_response(query_url, 400, error: 'bar!') } it 'raises a Gitlab::PrometheusError error' do - Timecop.freeze(current_time) do - expect { execute_query } - .to raise_error(Gitlab::PrometheusError, 'bar!') - end + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, 'bar!') + expect(req_stub).to have_been_requested end end context 'when request returns 400 without an error message' do - let(:response) { failure_response(400) } + let!(:req_stub) { failure_response(query_url, 400) } it 'raises a Gitlab::PrometheusError error' do - Timecop.freeze(current_time) do - expect { execute_query } - .to raise_error(Gitlab::PrometheusError, 'Bad data received') - end + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, 'Bad data received') + expect(req_stub).to have_been_requested end end context 'when request returns 500' do - let(:response) { failure_response(500) } + let!(:req_stub) { failure_response(query_url, 500, message: 'FAIL!') } it 'raises a Gitlab::PrometheusError error' do - Timecop.freeze(current_time) do - expect { execute_query } - .to raise_error(Gitlab::PrometheusError, '500 - FAIL!') - end + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, '500 - {"message":"FAIL!"}') + expect(req_stub).to have_been_requested end end end describe '#query' do let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } - let(:current_time) { Time.now.utc } - - before do - Timecop.freeze(current_time) do - query = { - query: prometheus_query, - time: current_time.to_f - }.to_query - - expect(HTTParty) - .to receive(:get) - .with("http://example.org/api/v1/query?#{query}") - .and_return(response) - end + let(:query_url) do + query = { query: prometheus_query }.to_query + "https://prometheus.example.com/api/v1/query?#{query}" end context 'when request returns vector results' do - let(:response) { success_response('resultType' => 'vector', 'result' => 'foo') } + let!(:req_stub) { success_response(query_url, prometheus_value_body('vector')) } it 'returns data from the API call' do - Timecop.freeze(current_time) do - expect(subject.query(prometheus_query)).to eq 'foo' - end + expect(subject.query(prometheus_query)).to eq [{"metric"=>{}, "value"=>[1488772511.004, "0.000041021495238095323"]}] + expect(req_stub).to have_been_requested end end context 'when request returns matrix results' do - let(:response) { success_response('resultType' => 'matrix', 'result' => 'foo') } + let!(:req_stub) { success_response(query_url, prometheus_value_body('matrix')) } it 'returns nil' do - Timecop.freeze(current_time) do - expect(subject.query(prometheus_query)).to be_nil - end + expect(subject.query(prometheus_query)).to be_nil + expect(req_stub).to have_been_requested end end context 'when request returns no data' do - let(:response) { success_response(nil) } + let!(:req_stub) { success_response(query_url, prometheus_empty_body('vector')) } - it 'returns nil' do - Timecop.freeze(current_time) do - expect(subject.query(prometheus_query)).to be_nil - end + it 'returns []' do + expect(subject.query(prometheus_query)).to be_empty + expect(req_stub).to have_been_requested end end @@ -126,55 +109,56 @@ def failure_response(code, parsed_body = {}) describe '#query_range' do let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } let(:current_time) { Time.now.utc } - - before do - Timecop.freeze(current_time) do - query = { - query: prometheus_query, - start: current_time.to_f, - end: current_time.to_f, - step: 1.minute.to_i - }.to_query - - expect(HTTParty) - .to receive(:get) - .with("http://example.org/api/v1/query_range?#{query}") - .and_return(response) - end + let(:query_url) do + query = { + query: prometheus_query, + start: current_time.to_f, + end: current_time.to_f, + step: 1.minute.to_i + }.to_query + "https://prometheus.example.com/api/v1/query_range?#{query}" end context 'when request returns vector results' do - let(:response) { success_response('resultType' => 'vector', 'result' => 'foo') } + let!(:req_stub) { success_response(query_url, prometheus_values_body('vector')) } it 'returns mil' do Timecop.freeze(current_time) do expect(subject.query_range(prometheus_query, start: current_time)).to be_nil + expect(req_stub).to have_been_requested end end end context 'when request returns matrix results' do - let(:response) { success_response('resultType' => 'matrix', 'result' => 'foo') } + let!(:req_stub) { success_response(query_url, prometheus_values_body('matrix')) } it 'returns data from the API call' do Timecop.freeze(current_time) do - expect(subject.query_range(prometheus_query, start: current_time)).to eq 'foo' + expect(subject.query_range(prometheus_query, start: current_time)).to eq([ + { + "metric"=>{}, + "values"=> [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]] + } + ]) + expect(req_stub).to have_been_requested end end end context 'when request returns no data' do - let(:response) { success_response(nil) } + let!(:req_stub) { success_response(query_url, prometheus_empty_body('matrix')) } - it 'returns nil' do + it 'returns []' do Timecop.freeze(current_time) do - expect(subject.query_range(prometheus_query, start: current_time)).to be_nil + expect(subject.query_range(prometheus_query, start: current_time)).to be_empty + expect(req_stub).to have_been_requested end end end it_behaves_like 'failure response' do - let(:execute_query) { subject.query_range(prometheus_query, start: current_time) } + let(:execute_query) { Timecop.freeze(current_time) { subject.query_range(prometheus_query, start: current_time) } } end end end diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb new file mode 100644 index 000000000000..994d1e5a086e --- /dev/null +++ b/spec/support/prometheus_helpers.rb @@ -0,0 +1,47 @@ +module PrometheusHelpers + def prometheus_empty_body(type) + { + "status": "success", + "data": { + "resultType": type, + "result": [] + } + } + end + + def prometheus_value_body(type = 'vector') + { + "status": "success", + "data": { + "resultType": type, + "result": [ + { + "metric": {}, + "value": [ + 1488772511.004, + "0.000041021495238095323" + ] + } + ] + } + } + end + + def prometheus_values_body(type = 'matrix') + { + "status": "success", + "data": { + "resultType": type, + "result": [ + { + "metric": {}, + "values": [ + [1488758662.506, "0.00002996364761904785"], + [1488758722.506, "0.00003090239047619091"] + ] + } + ] + } + } + end +end -- GitLab From dc8c63efe85a1cfe48efea8f85c733b3b3e0514e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 14:10:15 +0100 Subject: [PATCH 29/39] Add specs for PrometheusService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../project_services/prometheus_service.rb | 29 +-- spec/factories/projects.rb | 11 ++ .../prometheus_service_spec.rb | 172 ++++++++++++++++++ 3 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 spec/models/project_services/prometheus_service_spec.rb diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 27c164dfbb08..ba479e8b58d7 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -39,12 +39,12 @@ def self.to_param def fields [ - { - type: 'text', - name: 'api_url', - title: 'API URL', - placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/', - } + { + type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/' + } ] end @@ -52,7 +52,7 @@ def fields def test(*args) client.ping - { success: true, result: "Checked API endpoint" } + { success: true, result: 'Checked API endpoint' } rescue Gitlab::PrometheusError => err { success: false, result: err } end @@ -64,25 +64,28 @@ def metrics(environment) end # Cache metrics for specific environment - def calculate_reactive_cache(environment) + def calculate_reactive_cache(environment_slug) return unless active? && project && !project.pending_delete? + memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} + cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} + # TODO: encode environment { success: true, metrics: { # Memory used in MB - memory_values: client.query_range("sum(container_memory_usage_bytes{container_name=\"app\",environment=\"#{environment}\"})/1024/1024", start: 8.hours.ago), - memory_current: client.query("sum(container_memory_usage_bytes{container_name=\"app\",environment=\"#{environment}\"})/1024/1024"), + memory_values: client.query_range(memory_query, start: 8.hours.ago), + memory_current: client.query(memory_query), # CPU Usage rate in cores. - cpu_values: client.query_range("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))", start: 8.hours.ago), - cpu_current: client.query("sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"#{environment}\"}[2m]))"), + cpu_values: client.query_range(cpu_query, start: 8.hours.ago), + cpu_current: client.query(cpu_query) }, last_update: Time.now.utc } rescue Gitlab::PrometheusError => err - { success: false, result: err } + { success: false, result: err.message } end def client diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 04de35121256..590f7ba4163e 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -214,4 +214,15 @@ ) end end + + factory :prometheus_project, parent: :empty_project do + after :create do |project| + project.create_prometheus_service( + active: true, + properties: { + api_url: 'https://prometheus.example.com' + } + ) + end + end end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb new file mode 100644 index 000000000000..56cd0ec95089 --- /dev/null +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +describe PrometheusService, models: true, caching: true do + include PrometheusHelpers + include ReactiveCachingHelpers + + let(:project) { create(:prometheus_project) } + let(:service) { project.prometheus_service } + let(:current_time) { Time.now.utc } + + # let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } + def query_url(prometheus_query) + query = { query: prometheus_query }.to_query + "https://prometheus.example.com/api/v1/query?#{query}" + end + + def query_range_url(prometheus_query, current_time) + query = { + query: prometheus_query, + start: 8.hours.ago(current_time).to_f, + end: current_time.to_f, + step: 1.minute.to_i + }.to_query + "https://prometheus.example.com/api/v1/query_range?#{query}" + end + + def memory_query(environment) + %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment.slug}"})/1024/1024} + end + + def cpu_query(environment) + %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment.slug}"}[2m]))} + end + + let(:prometheus_data) do + { + success: true, + metrics: { + memory_values: prometheus_values_body('matrix').dig(:data, :result), + memory_current: prometheus_value_body('vector').dig(:data, :result), + cpu_values: prometheus_values_body('matrix').dig(:data, :result), + cpu_current: prometheus_value_body('vector').dig(:data, :result) + }, + last_update: current_time + } + end + + def stub_prometheus_request(query_url, status: 200, body: prometheus_value_body('vector')) + WebMock.stub_request(:get, query_url) + .to_return({ + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body.to_json + }) + end + + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:api_url) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:api_url) } + end + end + + describe '#test' do + let!(:req_stub) { stub_prometheus_request(query_url('1')) } + + context 'success' do + it 'reads the discovery endpoint' do + expect(service.test[:success]).to be_truthy + expect(req_stub).to have_been_requested + end + end + + context 'failure' do + let!(:req_stub) { stub_prometheus_request(query_url('1'), status: 404) } + + it 'fails to read the discovery endpoint' do + expect(service.test[:success]).to be_falsy + expect(req_stub).to have_been_requested + end + end + end + + describe '#metrics' do + let(:environment) { build_stubbed(:environment, slug: 'env-slug') } + subject { service.metrics(environment) } + + context 'with valid data' do + before do + stub_reactive_cache(service, prometheus_data, 'env-slug') + end + + it 'returns reactive data' do + is_expected.to eq(prometheus_data) + end + end + end + + describe '#calculate_reactive_cache' do + let(:environment) { build_stubbed(:environment, slug: 'env-slug') } + subject { Timecop.freeze(current_time) { service.calculate_reactive_cache(environment.slug) } } + + context 'when service is inactive' do + before { service.active = false } + + it { is_expected.to be_nil } + end + + context 'when Prometheus responds with valid data' do + before do + stub_prometheus_request( + query_url(memory_query(environment)), + body: prometheus_value_body + ) + stub_prometheus_request( + query_range_url(memory_query(environment), current_time), + body: prometheus_values_body + ) + stub_prometheus_request( + query_url(cpu_query(environment)), + body: prometheus_value_body + ) + stub_prometheus_request( + query_range_url(cpu_query(environment), current_time), + body: prometheus_values_body + ) + end + + it { expect(subject.to_json).to eq(prometheus_data.to_json) } + end + + [404, 500].each do |status| + context "when Prometheus responds with #{status}" do + before do + stub_prometheus_request( + query_url(memory_query(environment)), + status: status, + body: 'MEMORY QUERY FAILED!' + ) + stub_prometheus_request( + query_range_url(memory_query(environment), current_time), + status: status, + body: 'MEMORY QUERY RANGE FAILED!' + ) + stub_prometheus_request( + query_url(cpu_query(environment)), + status: status, + body: 'CPU QUERY RANGE FAILED!' + ) + stub_prometheus_request( + query_range_url(cpu_query(environment), current_time), + status: status, + body: 'CPU QUERY RANGE FAILED!' + ) + end + + it { is_expected.to eq(success: false, result: %(#{status} - "MEMORY QUERY RANGE FAILED!")) } + end + end + end +end -- GitLab From 2ca8793e0de1050d8ea6790a1627161c8d70dd7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 16:18:28 +0100 Subject: [PATCH 30/39] Fix Rubocop offenses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/lib/gitlab/prometheus_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb index 99b7351cb1a0..480ae46d80de 100644 --- a/spec/lib/gitlab/prometheus_spec.rb +++ b/spec/lib/gitlab/prometheus_spec.rb @@ -27,7 +27,7 @@ def failure_response(url, code, parsed_body = {}) it 'issues a "query" request to the API endpoint' do req_stub = success_response('https://prometheus.example.com/api/v1/query?query=1', prometheus_value_body('vector')) - expect(subject.ping).to eq({ "resultType"=>"vector", "result"=>[{"metric"=>{}, "value"=>[1488772511.004, "0.000041021495238095323"]}] }) + expect(subject.ping).to eq({ "resultType" => "vector", "result" => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] }) expect(req_stub).to have_been_requested end end @@ -78,7 +78,7 @@ def failure_response(url, code, parsed_body = {}) let!(:req_stub) { success_response(query_url, prometheus_value_body('vector')) } it 'returns data from the API call' do - expect(subject.query(prometheus_query)).to eq [{"metric"=>{}, "value"=>[1488772511.004, "0.000041021495238095323"]}] + expect(subject.query(prometheus_query)).to eq [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] expect(req_stub).to have_been_requested end end @@ -137,8 +137,8 @@ def failure_response(url, code, parsed_body = {}) Timecop.freeze(current_time) do expect(subject.query_range(prometheus_query, start: current_time)).to eq([ { - "metric"=>{}, - "values"=> [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]] + "metric" => {}, + "values" => [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]] } ]) expect(req_stub).to have_been_requested -- GitLab From 39eaf07dc9479da3abe1b697ed1abad29b22a252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 16:18:44 +0100 Subject: [PATCH 31/39] Add specs for Environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/models/environment_spec.rb | 83 ++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index dce18f008f84..b4305e92812d 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -271,7 +271,11 @@ context 'when the environment is unavailable' do let(:project) { create(:kubernetes_project) } - before { environment.stop } + + before do + environment.stop + end + it { is_expected.to be_falsy } end end @@ -281,20 +285,85 @@ subject { environment.terminals } context 'when the environment has terminals' do - before { allow(environment).to receive(:has_terminals?).and_return(true) } + before do + allow(environment).to receive(:has_terminals?).and_return(true) + end it 'returns the terminals from the deployment service' do - expect(project.deployment_service). - to receive(:terminals).with(environment). - and_return(:fake_terminals) + expect(project.deployment_service) + .to receive(:terminals).with(environment) + .and_return(:fake_terminals) is_expected.to eq(:fake_terminals) end end context 'when the environment does not have terminals' do - before { allow(environment).to receive(:has_terminals?).and_return(false) } - it { is_expected.to eq(nil) } + before do + allow(environment).to receive(:has_terminals?).and_return(false) + end + + it { is_expected.to be_nil } + end + end + + describe '#has_metrics?' do + subject { environment.has_metrics? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:prometheus_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a monitoring service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:prometheus_project) } + + before do + environment.stop + end + + it { is_expected.to be_falsy } + end + end + + describe '#metrics' do + let(:project) { create(:prometheus_project) } + subject { environment.metrics } + + context 'when the environment has metrics' do + before do + allow(environment).to receive(:has_metrics?).and_return(true) + end + + it 'returns the metrics from the deployment service' do + expect(project.monitoring_service) + .to receive(:metrics).with(environment) + .and_return(:fake_metrics) + + is_expected.to eq(:fake_metrics) + end + end + + context 'when the environment does not have metrics' do + before do + allow(environment).to receive(:has_metrics?).and_return(false) + end + + it { is_expected.to be_nil } end end -- GitLab From 2e9983d9147b94b657e30ce39b52c5d7281c16c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 18:12:35 +0100 Subject: [PATCH 32/39] Add a title to the metrics button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/views/projects/environments/_metrics_button.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index 8cf0f84277d1..acbac1869fdb 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -2,5 +2,5 @@ - return unless environment.has_metrics? && can?(current_user, :read_environment, environment) -= link_to environment_metrics_path(environment), class: 'btn metrics-button' do += link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do = icon('area-chart') -- GitLab From c5275f4ec9e9d049a6f628df289dc9f6863af911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 18:40:17 +0100 Subject: [PATCH 33/39] Improve PrometheusHelpers and related specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/gitlab/prometheus.rb | 4 +- spec/lib/gitlab/prometheus_spec.rb | 119 ++++++++---------- .../prometheus_service_spec.rb | 108 +++------------- spec/support/prometheus_helpers.rb | 70 +++++++++++ 4 files changed, 141 insertions(+), 160 deletions(-) diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb index ea06f34bae9a..62239779454f 100644 --- a/lib/gitlab/prometheus.rb +++ b/lib/gitlab/prometheus.rb @@ -19,11 +19,11 @@ def query(query) end end - def query_range(query, start:) + def query_range(query, start: 8.hours.ago) get_result('matrix') do json_api_get('query_range', query: query, - start: start.utc.to_f, + start: start.to_f, end: Time.now.utc.to_f, step: 1.minute.to_i) end diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb index 480ae46d80de..280264188e2e 100644 --- a/spec/lib/gitlab/prometheus_spec.rb +++ b/spec/lib/gitlab/prometheus_spec.rb @@ -5,27 +5,9 @@ subject { described_class.new(api_url: 'https://prometheus.example.com') } - def success_response(url, body) - WebMock.stub_request(:get, url) - .to_return({ - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: body.to_json - }) - end - - def failure_response(url, code, parsed_body = {}) - WebMock.stub_request(:get, url) - .to_return({ - status: code, - headers: { 'Content-Type' => 'application/json' }, - body: parsed_body.to_json - }) - end - describe '#ping' do it 'issues a "query" request to the API endpoint' do - req_stub = success_response('https://prometheus.example.com/api/v1/query?query=1', prometheus_value_body('vector')) + req_stub = stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) expect(subject.ping).to eq({ "resultType" => "vector", "result" => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] }) expect(req_stub).to have_been_requested @@ -37,9 +19,9 @@ def failure_response(url, code, parsed_body = {}) # - execute_query: A query call shared_examples 'failure response' do context 'when request returns 400 with an error message' do - let!(:req_stub) { failure_response(query_url, 400, error: 'bar!') } - it 'raises a Gitlab::PrometheusError error' do + req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'bar!' }) + expect { execute_query } .to raise_error(Gitlab::PrometheusError, 'bar!') expect(req_stub).to have_been_requested @@ -47,9 +29,9 @@ def failure_response(url, code, parsed_body = {}) end context 'when request returns 400 without an error message' do - let!(:req_stub) { failure_response(query_url, 400) } - it 'raises a Gitlab::PrometheusError error' do + req_stub = stub_prometheus_request(query_url, status: 400) + expect { execute_query } .to raise_error(Gitlab::PrometheusError, 'Bad data received') expect(req_stub).to have_been_requested @@ -57,9 +39,9 @@ def failure_response(url, code, parsed_body = {}) end context 'when request returns 500' do - let!(:req_stub) { failure_response(query_url, 500, message: 'FAIL!') } - it 'raises a Gitlab::PrometheusError error' do + req_stub = stub_prometheus_request(query_url, status: 500, body: { message: 'FAIL!' }) + expect { execute_query } .to raise_error(Gitlab::PrometheusError, '500 - {"message":"FAIL!"}') expect(req_stub).to have_been_requested @@ -68,34 +50,31 @@ def failure_response(url, code, parsed_body = {}) end describe '#query' do - let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } - let(:query_url) do - query = { query: prometheus_query }.to_query - "https://prometheus.example.com/api/v1/query?#{query}" - end + let(:prometheus_query) { prometheus_cpu_query('env-slug') } + let(:query_url) { prometheus_query_url(prometheus_query) } context 'when request returns vector results' do - let!(:req_stub) { success_response(query_url, prometheus_value_body('vector')) } - it 'returns data from the API call' do + req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector')) + expect(subject.query(prometheus_query)).to eq [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] expect(req_stub).to have_been_requested end end context 'when request returns matrix results' do - let!(:req_stub) { success_response(query_url, prometheus_value_body('matrix')) } - it 'returns nil' do + req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('matrix')) + expect(subject.query(prometheus_query)).to be_nil expect(req_stub).to have_been_requested end end context 'when request returns no data' do - let!(:req_stub) { success_response(query_url, prometheus_empty_body('vector')) } - it 'returns []' do + req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('vector')) + expect(subject.query(prometheus_query)).to be_empty expect(req_stub).to have_been_requested end @@ -107,58 +86,58 @@ def failure_response(url, code, parsed_body = {}) end describe '#query_range' do - let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } - let(:current_time) { Time.now.utc } - let(:query_url) do - query = { - query: prometheus_query, - start: current_time.to_f, - end: current_time.to_f, - step: 1.minute.to_i - }.to_query - "https://prometheus.example.com/api/v1/query_range?#{query}" + let(:prometheus_query) { prometheus_memory_query('env-slug') } + let(:query_url) { prometheus_query_range_url(prometheus_query) } + + around do |example| + Timecop.freeze { example.run } + end + + context 'when a start time is passed' do + let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) } + + it 'passed it in the requested URL' do + req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector')) + + subject.query_range(prometheus_query, start: 2.hours.ago) + expect(req_stub).to have_been_requested + end end context 'when request returns vector results' do - let!(:req_stub) { success_response(query_url, prometheus_values_body('vector')) } + it 'returns nil' do + req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector')) - it 'returns mil' do - Timecop.freeze(current_time) do - expect(subject.query_range(prometheus_query, start: current_time)).to be_nil - expect(req_stub).to have_been_requested - end + expect(subject.query_range(prometheus_query)).to be_nil + expect(req_stub).to have_been_requested end end context 'when request returns matrix results' do - let!(:req_stub) { success_response(query_url, prometheus_values_body('matrix')) } - it 'returns data from the API call' do - Timecop.freeze(current_time) do - expect(subject.query_range(prometheus_query, start: current_time)).to eq([ - { - "metric" => {}, - "values" => [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]] - } - ]) - expect(req_stub).to have_been_requested - end + req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('matrix')) + + expect(subject.query_range(prometheus_query)).to eq([ + { + "metric" => {}, + "values" => [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]] + } + ]) + expect(req_stub).to have_been_requested end end context 'when request returns no data' do - let!(:req_stub) { success_response(query_url, prometheus_empty_body('matrix')) } - it 'returns []' do - Timecop.freeze(current_time) do - expect(subject.query_range(prometheus_query, start: current_time)).to be_empty - expect(req_stub).to have_been_requested - end + req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('matrix')) + + expect(subject.query_range(prometheus_query)).to be_empty + expect(req_stub).to have_been_requested end end it_behaves_like 'failure response' do - let(:execute_query) { Timecop.freeze(current_time) { subject.query_range(prometheus_query, start: current_time) } } + let(:execute_query) { subject.query_range(prometheus_query) } end end end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index 56cd0ec95089..d15079b686be 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -6,53 +6,6 @@ let(:project) { create(:prometheus_project) } let(:service) { project.prometheus_service } - let(:current_time) { Time.now.utc } - - # let(:prometheus_query) { "sum(rate(container_cpu_usage_seconds_total{container_name=\"app\",environment=\"env-slug\"}[2m]))" } - def query_url(prometheus_query) - query = { query: prometheus_query }.to_query - "https://prometheus.example.com/api/v1/query?#{query}" - end - - def query_range_url(prometheus_query, current_time) - query = { - query: prometheus_query, - start: 8.hours.ago(current_time).to_f, - end: current_time.to_f, - step: 1.minute.to_i - }.to_query - "https://prometheus.example.com/api/v1/query_range?#{query}" - end - - def memory_query(environment) - %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment.slug}"})/1024/1024} - end - - def cpu_query(environment) - %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment.slug}"}[2m]))} - end - - let(:prometheus_data) do - { - success: true, - metrics: { - memory_values: prometheus_values_body('matrix').dig(:data, :result), - memory_current: prometheus_value_body('vector').dig(:data, :result), - cpu_values: prometheus_values_body('matrix').dig(:data, :result), - cpu_current: prometheus_value_body('vector').dig(:data, :result) - }, - last_update: current_time - } - end - - def stub_prometheus_request(query_url, status: 200, body: prometheus_value_body('vector')) - WebMock.stub_request(:get, query_url) - .to_return({ - status: status, - headers: { 'Content-Type' => 'application/json' }, - body: body.to_json - }) - end describe "Associations" do it { is_expected.to belong_to :project } @@ -73,7 +26,7 @@ def stub_prometheus_request(query_url, status: 200, body: prometheus_value_body( end describe '#test' do - let!(:req_stub) { stub_prometheus_request(query_url('1')) } + let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) } context 'success' do it 'reads the discovery endpoint' do @@ -83,7 +36,7 @@ def stub_prometheus_request(query_url, status: 200, body: prometheus_value_body( end context 'failure' do - let!(:req_stub) { stub_prometheus_request(query_url('1'), status: 404) } + let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), status: 404) } it 'fails to read the discovery endpoint' do expect(service.test[:success]).to be_falsy @@ -96,6 +49,10 @@ def stub_prometheus_request(query_url, status: 200, body: prometheus_value_body( let(:environment) { build_stubbed(:environment, slug: 'env-slug') } subject { service.metrics(environment) } + around do |example| + Timecop.freeze { example.run } + end + context 'with valid data' do before do stub_reactive_cache(service, prometheus_data, 'env-slug') @@ -109,32 +66,26 @@ def stub_prometheus_request(query_url, status: 200, body: prometheus_value_body( describe '#calculate_reactive_cache' do let(:environment) { build_stubbed(:environment, slug: 'env-slug') } - subject { Timecop.freeze(current_time) { service.calculate_reactive_cache(environment.slug) } } + + around do |example| + Timecop.freeze { example.run } + end + + subject do + service.calculate_reactive_cache(environment.slug) + end context 'when service is inactive' do - before { service.active = false } + before do + service.active = false + end it { is_expected.to be_nil } end context 'when Prometheus responds with valid data' do before do - stub_prometheus_request( - query_url(memory_query(environment)), - body: prometheus_value_body - ) - stub_prometheus_request( - query_range_url(memory_query(environment), current_time), - body: prometheus_values_body - ) - stub_prometheus_request( - query_url(cpu_query(environment)), - body: prometheus_value_body - ) - stub_prometheus_request( - query_range_url(cpu_query(environment), current_time), - body: prometheus_values_body - ) + stub_all_prometheus_requests(environment.slug) end it { expect(subject.to_json).to eq(prometheus_data.to_json) } @@ -143,29 +94,10 @@ def stub_prometheus_request(query_url, status: 200, body: prometheus_value_body( [404, 500].each do |status| context "when Prometheus responds with #{status}" do before do - stub_prometheus_request( - query_url(memory_query(environment)), - status: status, - body: 'MEMORY QUERY FAILED!' - ) - stub_prometheus_request( - query_range_url(memory_query(environment), current_time), - status: status, - body: 'MEMORY QUERY RANGE FAILED!' - ) - stub_prometheus_request( - query_url(cpu_query(environment)), - status: status, - body: 'CPU QUERY RANGE FAILED!' - ) - stub_prometheus_request( - query_range_url(cpu_query(environment), current_time), - status: status, - body: 'CPU QUERY RANGE FAILED!' - ) + stub_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!') end - it { is_expected.to eq(success: false, result: %(#{status} - "MEMORY QUERY RANGE FAILED!")) } + it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) } end end end diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb index 994d1e5a086e..a52d8f37d144 100644 --- a/spec/support/prometheus_helpers.rb +++ b/spec/support/prometheus_helpers.rb @@ -1,4 +1,74 @@ module PrometheusHelpers + def prometheus_memory_query(environment_slug) + %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} + end + + def prometheus_cpu_query(environment_slug) + %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} + end + + def prometheus_query_url(prometheus_query) + query = { query: prometheus_query }.to_query + + "https://prometheus.example.com/api/v1/query?#{query}" + end + + def prometheus_query_range_url(prometheus_query, start: 8.hours.ago) + query = { + query: prometheus_query, + start: start.to_f, + end: Time.now.utc.to_f, + step: 1.minute.to_i + }.to_query + + "https://prometheus.example.com/api/v1/query_range?#{query}" + end + + def stub_prometheus_request(url, body: {}, status: 200) + WebMock.stub_request(:get, url) + .to_return({ + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body.to_json + }) + end + + def stub_all_prometheus_requests(environment_slug, body: nil, status: 200) + stub_prometheus_request( + prometheus_query_url(prometheus_memory_query(environment_slug)), + status: status, + body: body || prometheus_value_body + ) + stub_prometheus_request( + prometheus_query_range_url(prometheus_memory_query(environment_slug)), + status: status, + body: body || prometheus_values_body + ) + stub_prometheus_request( + prometheus_query_url(prometheus_cpu_query(environment_slug)), + status: status, + body: body || prometheus_value_body + ) + stub_prometheus_request( + prometheus_query_range_url(prometheus_cpu_query(environment_slug)), + status: status, + body: body || prometheus_values_body + ) + end + + def prometheus_data(last_update: Time.now.utc) + { + success: true, + metrics: { + memory_values: prometheus_values_body('matrix').dig(:data, :result), + memory_current: prometheus_value_body('vector').dig(:data, :result), + cpu_values: prometheus_values_body('matrix').dig(:data, :result), + cpu_current: prometheus_value_body('vector').dig(:data, :result) + }, + last_update: last_update + } + end + def prometheus_empty_body(type) { "status": "success", -- GitLab From d9917e5496d7857557c7424fadde1524cff847cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 6 Mar 2017 18:44:51 +0100 Subject: [PATCH 34/39] Add feature spec for Environment metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../environments/environment_metrics_spec.rb | 39 +++++++++++++++++++ .../environments}/environment_spec.rb | 30 +++++--------- .../environments}/environments_spec.rb | 0 3 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 spec/features/projects/environments/environment_metrics_spec.rb rename spec/features/{ => projects/environments}/environment_spec.rb (91%) rename spec/features/{ => projects/environments}/environments_spec.rb (100%) diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb new file mode 100644 index 000000000000..ee925e811e1b --- /dev/null +++ b/spec/features/projects/environments/environment_metrics_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +feature 'Environment > Metrics', :feature do + include PrometheusHelpers + + given(:user) { create(:user) } + given(:project) { create(:prometheus_project) } + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:environment) { create(:environment, project: project) } + given(:current_time) { Time.now.utc } + + background do + project.add_developer(user) + create(:deployment, environment: environment, deployable: build) + stub_all_prometheus_requests(environment.slug) + + login_as(user) + visit_environment(environment) + end + + around do |example| + Timecop.freeze(current_time) { example.run } + end + + context 'with deployments and related deployable present' do + scenario 'shows metrics' do + click_link('See metrics') + + expect(page).to have_css('svg.prometheus-graph') + end + end + + def visit_environment(environment) + visit namespace_project_environment_path(environment.project.namespace, + environment.project, + environment) + end +end diff --git a/spec/features/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb similarity index 91% rename from spec/features/environment_spec.rb rename to spec/features/projects/environments/environment_spec.rb index c203e1f20c16..d52700aa6ae5 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -37,14 +37,9 @@ scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) - end - - scenario 'does not show a re-deploy button for deployment without build' do expect(page).not_to have_link('Re-deploy') - end - - scenario 'does not show terminal button' do expect(page).not_to have_terminal_button + expect(page).not_to have_metrics_button end end @@ -56,16 +51,12 @@ create(:deployment, environment: environment, deployable: build) end - scenario 'does show build name' do + scenario 'does show build name, metrics, re-deploy, and terminal buttons' do expect(page).to have_link("#{build.name} (##{build.id})") - end - - scenario 'does show re-deploy button' do + expect(page).to have_link('See metrics') expect(page).to have_link('Re-deploy') - end - - scenario 'does not show terminal button' do expect(page).not_to have_terminal_button + expect(page).to have_metrics_button end context 'with manual action' do @@ -77,7 +68,7 @@ scenario 'does allow to play manual action' do expect(manual).to be_skipped - expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + expect { click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end @@ -111,9 +102,6 @@ it 'displays a web terminal' do expect(page).to have_selector('#terminal') - end - - it 'displays a link to the environment external url' do expect(page).to have_link(nil, href: environment.external_url) end end @@ -133,10 +121,6 @@ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - scenario 'does show stop button' do - expect(page).to have_link('Stop') - end - scenario 'does allow to stop environment' do click_link('Stop') @@ -222,4 +206,8 @@ def visit_environment(environment) def have_terminal_button have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) end + + def have_metrics_button + have_link(nil, href: metrics_namespace_project_environment_path(project.namespace, project, environment)) + end end diff --git a/spec/features/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb similarity index 100% rename from spec/features/environments_spec.rb rename to spec/features/projects/environments/environments_spec.rb -- GitLab From a2f7a719cb01e3faab681667e6e71c070125fa7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 7 Mar 2017 10:21:54 +0100 Subject: [PATCH 35/39] Remove metrics expectations from the environment feature spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/features/projects/environments/environment_spec.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index d52700aa6ae5..bbc15f1a3ad3 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -39,7 +39,6 @@ expect(page).to have_link(deployment.short_sha) expect(page).not_to have_link('Re-deploy') expect(page).not_to have_terminal_button - expect(page).not_to have_metrics_button end end @@ -51,12 +50,10 @@ create(:deployment, environment: environment, deployable: build) end - scenario 'does show build name, metrics, re-deploy, and terminal buttons' do + scenario 'does show build name' do expect(page).to have_link("#{build.name} (##{build.id})") - expect(page).to have_link('See metrics') expect(page).to have_link('Re-deploy') expect(page).not_to have_terminal_button - expect(page).to have_metrics_button end context 'with manual action' do @@ -206,8 +203,4 @@ def visit_environment(environment) def have_terminal_button have_link(nil, href: terminal_namespace_project_environment_path(project.namespace, project, environment)) end - - def have_metrics_button - have_link(nil, href: metrics_namespace_project_environment_path(project.namespace, project, environment)) - end end -- GitLab From 01a6c1b9059ab0ed55a6a576baaf3718753eac35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 7 Mar 2017 10:43:25 +0100 Subject: [PATCH 36/39] Add PrometheusService and metrics page for environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prometheus Service supports fetching metrics for an environment and displaying that on environments page. Signed-off-by: Rémy Coutable --- app/models/project_services/prometheus_service.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index ba479e8b58d7..375966b9efc6 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -70,7 +70,6 @@ def calculate_reactive_cache(environment_slug) memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} - # TODO: encode environment { success: true, metrics: { -- GitLab From c78e7fe31b3a048aa090a4c20c21380beb26d762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 7 Mar 2017 10:55:19 +0100 Subject: [PATCH 37/39] Add PrometheusService and metrics page for environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prometheus Service supports fetching metrics for an environment and displaying that on environments page. Signed-off-by: Rémy Coutable --- lib/api/v3/services.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index 2e8c59b44631..d77185ffe5ab 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -423,14 +423,6 @@ class Services < Grape::API desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' } ], - 'prometheus' => [ - { - required: true, - name: :api_url, - type: String, - desc: 'Prometheus API Base URL, like http://prometheus.example.com/' - } - ], 'pushover' => [ { required: true, -- GitLab From 9310bc99c93e55dd3469ee643f73834e90632d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 7 Mar 2017 13:39:11 +0100 Subject: [PATCH 38/39] Add PrometheusService and metrics page for environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prometheus Service supports fetching metrics for an environment and displaying that on environments page. Signed-off-by: Rémy Coutable --- spec/requests/api/v3/services_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb index 7e8c8753d028..cb96c540e115 100644 --- a/spec/requests/api/v3/services_spec.rb +++ b/spec/requests/api/v3/services_spec.rb @@ -6,7 +6,7 @@ let(:user) { create(:user) } let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } - Service.available_services_names.each do |service| + Service.available_services_names.except(:prometheus).each do |service| describe "DELETE /projects/:id/services/#{service.dasherize}" do include_context service -- GitLab From 87d3cdcd4443d2d0fa35284a091e5452c04011e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 7 Mar 2017 15:52:56 +0100 Subject: [PATCH 39/39] Add PrometheusService and metrics page for environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prometheus Service supports fetching metrics for an environment and displaying that on environments page. Signed-off-by: Rémy Coutable --- spec/requests/api/v3/services_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb index cb96c540e115..3a760a8f25c6 100644 --- a/spec/requests/api/v3/services_spec.rb +++ b/spec/requests/api/v3/services_spec.rb @@ -6,7 +6,9 @@ let(:user) { create(:user) } let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } - Service.available_services_names.except(:prometheus).each do |service| + available_services = Service.available_services_names + available_services.delete('prometheus') + available_services.each do |service| describe "DELETE /projects/:id/services/#{service.dasherize}" do include_context service -- GitLab