Commit 55f2a5de authored by Kamil Trzciński's avatar Kamil Trzciński Committed by Rémy Coutable

Added Prometheus Service and Prometheus graphs

parent a5db7f54
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 */ /* 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 UsernameValidator */
/* global ActiveTabMemoizer */ /* global ActiveTabMemoizer */
...@@ -297,6 +298,8 @@ const UserCallout = require('./user_callout'); ...@@ -297,6 +298,8 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show': case 'ci:lints:show':
new gl.CILintEditor(); new gl.CILintEditor();
break; break;
case 'projects:environments:metrics':
new PrometheusGraph();
case 'users:show': case 'users:show':
new UserCallout(); new UserCallout();
break; break;
......
/* eslint-disable no-new*/
import d3 from 'd3';
import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status';
import '~/lib/utils/common_utils';
import Flash from '~/flash';
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 = $(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();
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 === {}) {
new Flash('Empty metrics', 'alert');
} else {
self.transformData(metricsResponse);
self.createGraph();
}
});
}
plotValues(valuesToPlot, key) {
const x = d3.time.scale()
.range([0, this.width]);
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)
.append('g')
.attr('transform', `translate(${this.margin.left},${this.margin.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(this.commonGraphProperties.axis_no_ticks)
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width)
.orient('left');
this.createAxisLabelContainers(axisLabelContainer, key);
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', 'metric-area')
.attr('fill', graphSpecifics.area_fill_color);
chart.append('path')
.datum(valuesToPlot)
.attr('class', 'metric-line')
.attr('stroke', graphSpecifics.line_color)
.attr('fill', 'none')
.attr('stroke-width', this.commonGraphProperties.area_stroke_width)
.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, prometheusGraphContainer, key));
}
// The legends from the metric
createAxisLabelContainers(axisLabelContainer, key) {
const graphSpecifics = this.graphSpecificProperties[key];
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 - this.margin.right,
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(graphSpecifics.graph_legend_title);
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.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) - this.margin.right)
.attr('y', this.originalHeight - this.marginLabelContainer.top)
.attr('dy', '.35em')
.text('Time');
// Legends
// Metric Usage
axisLabelContainer.append('rect')
.attr('x', this.originalWidth - 170)
.attr('y', (this.originalHeight / 2) - 80)
.style('fill', graphSpecifics.area_fill_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) - 65)
.text(graphSpecifics.graph_legend_title);
axisLabelContainer.append('text')
.attr('class', 'text-metric-usage')
.attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 50);
}
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);
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(`${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({
x1: currentTimeCoordinate,
y1: y(0),
x2: currentTimeCoordinate,
y2: maxValueMetric,
});
chart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', graphSpecifics.line_color)
.attr('cx', currentTimeCoordinate)
.attr('cy', y(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
// The little box with text
const rectTextMetric = chart.append('g')
.attr('class', 'rect-text-metric')
.attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
rectTextMetric.append('rect')
.attr('class', 'rect-metric')
.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', currentTimeCoordinate + 35)
.attr('y', maxValueMetric + 35)
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr('class', 'text-metric-date')
.attr('x', currentTimeCoordinate + 15)
.attr('y', maxValueMetric + 15)
.text(dayFormat(currentData.time));
// Update the text
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 (Cores)',
},
memory_values: {
area_fill_color: '#fca326',
line_color: '#fc6d26',
graph_legend_title: 'Memory Usage (MB)',
},
};
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,
};
}
getData() {
const maxNumberOfRequests = 3;
return gl.utils.backOff((next, stop) => {
$.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({
status: resp.status,
metrics: data,
});
}
} else {
stop({
status: resp.status,
metrics: data,
});
}
}).fail(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) {
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],
}));
});
this.data = metricTypes;
}
}
export default PrometheusGraph;
...@@ -143,3 +143,71 @@ ...@@ -143,3 +143,71 @@
} }
} }
} }
.prometheus-graph {
text {
fill: $stat-graph-axis-fill;
}
}
.x-axis path,
.y-axis path,
.label-x-axis-line,
.label-y-axis-line {
fill: none;
stroke-width: 1;
shape-rendering: crispEdges;
}
.x-axis path,
.y-axis path {
stroke: $stat-graph-axis-fill;
}
.label-x-axis-line,
.label-y-axis-line {
stroke: $border-color;
}
.y-axis {
line {
stroke: $stat-graph-axis-fill;
stroke-width: 1;
}
}
.metric-area {
opacity: 0.8;
}
.prometheus-graph-overlay {
fill: none;
opacity: 0.0;
pointer-events: all;
}
.rect-text-metric {
fill: $white-light;
stroke-width: 1;
stroke: $black;
}
.rect-axis-text {
fill: $white-light;
}
.text-metric,
.text-median-metric,
.text-metric-usage,
.text-metric-date {
fill: $black;
}
.text-metric-date {
font-weight: 200;
}
.selected-metric-line {
stroke: $black;
stroke-width: 1;
}
...@@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] 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 before_action :verify_api_request!, only: :terminal_websocket_authorize
def index def index
...@@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
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, status: @metrics.any? ? :ok : :no_content
end
end
end
private private
def verify_api_request! def verify_api_request!
......
...@@ -74,6 +74,10 @@ module GitlabRoutingHelper ...@@ -74,6 +74,10 @@ module GitlabRoutingHelper
namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
end 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) def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end end
......
...@@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base ...@@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base
project.deployment_service.terminals(self) if has_terminals? project.deployment_service.terminals(self) if has_terminals?
end end
def has_metrics?
project.monitoring_service.present? && available? && last_deployment.present?
end
def metrics
project.monitoring_service.metrics(self) if has_metrics?
end
# An environment name is not necessarily suitable for use in URLs, DNS # 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 # or other third-party contexts, so provide a slugified version. A slug has
# the following properties: # the following properties:
......
...@@ -113,6 +113,7 @@ class Project < ActiveRecord::Base ...@@ -113,6 +113,7 @@ class Project < ActiveRecord::Base
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy has_one :external_wiki_service, dependent: :destroy
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :prometheus_service, dependent: :destroy, inverse_of: :project
has_one :mock_ci_service, dependent: :destroy has_one :mock_ci_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
...@@ -771,6 +772,14 @@ class Project < ActiveRecord::Base ...@@ -771,6 +772,14 @@ class Project < ActiveRecord::Base
@deployment_service ||= deployment_services.reorder(nil).find_by(active: true) @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
end 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? def jira_tracker?
issues_tracker.to_param == 'jira' issues_tracker.to_param == 'jira'
end end
......
# Base class for monitoring services
#
# These services integrate with a deployment solution like Prometheus
# to provide additional features for environments.
class MonitoringService < Service
default_value_for :category, 'monitoring'
def self.supported_events
%w()
end
# Environments have a number of metrics
def metrics(environment)
raise NotImplementedError
end
end
class PrometheusService < MonitoringService
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
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 monitoring'
end
def help
'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
'prometheus'
end
def fields
[
{
type: 'text',
name: 'api_url',
title: 'API URL',
placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
}
]
end
# Check we can connect to the Prometheus API
def test(*args)
client.ping
{ success: true, result: 'Checked API endpoint' }
rescue Gitlab::PrometheusError => err
{ success: false, result: err }
end
def metrics(environment)
with_reactive_cache(environment.slug) do |data|
data
end
end
# Cache metrics for specific 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]))}
{
success: true,
metrics: {
# Memory used in MB
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(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.message }
end
def client
@prometheus ||= Gitlab::Prometheus.new(api_url: api_url)
end
end
...@@ -232,6 +232,7 @@ class Service < ActiveRecord::Base ...@@ -232,6 +232,7 @@ class Service < ActiveRecord::Base
mattermost mattermost
pipelines_email pipelines_email
pivotaltracker pivotaltracker
prometheus
pushover pushover
redmine redmine
slack_slash_commands slack_slash_commands
......
- environment = local_assigns.fetch(:environment)
- return unless environment.has_metrics? && can?(current_user, :read_environment, environment)
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart')
- @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
Environment:
= @environment.name
.col-sm-6
.nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
%h3.page-title= @environment.name %h3.page-title= @environment.name
.col-md-3 .col-md-3
.nav-controls .nav-controls
= render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment = render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment) - if can?(current_user, :update_environment, @environment)
......
...@@ -159,6 +159,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -159,6 +159,7 @@ constraints(ProjectUrlConstrainer.new) do
member do member do
post :stop post :stop
get :terminal get :terminal
get :metrics
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end end
......
...@@ -422,6 +422,14 @@ module API ...@@ -422,6 +422,14 @@ module API
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' 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' => [ 'pushover' => [
{ {
required: true, required: true,
...@@ -558,6 +566,7 @@ module API ...@@ -558,6 +566,7 @@ module API
SlackSlashCommandsService, SlackSlashCommandsService,
PipelinesEmailService, PipelinesEmailService,
PivotaltrackerService, PivotaltrackerService,
PrometheusService,
PushoverService, PushoverService,
RedmineService, RedmineService,
SlackService, SlackService,
......
module Gitlab
PrometheusError = Class.new(StandardError)
# Helper methods to interact with Prometheus network services & resources
class Prometheus
attr_reader :api_url
def initialize(api_url:)
@api_url = api_url
end
def ping
json_api_get('query', query: '1')
end
def query(query)
get_result('vector') do
json_api_get('query', query: query)
end
end
def query_range(query, start: 8.hours.ago)
get_result('matrix') do
json_api_get('query_range',
query: query,
start: start.to_f,
end: Time.now.utc.to_f,
step: 1.minute.to_i)
end
end
private
def json_api_get(type, args = {})
get(join_api_url(type, args))
rescue Errno::ECONNREFUSED
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 handle_response(response)
if response.code == 200 && response['status'] == 'success'
response['data'] || {}
elsif response.code == 400
raise PrometheusError, response['error'] || 'Bad data received'
else
raise PrometheusError, "#{response.code} - #{response.body}"
end
end
def get_result(expected_type)
data = yield
data['result'] if data['resultType'] == expected_type
end
end
end
...@@ -187,6 +187,52 @@ describe Projects::EnvironmentsController do ...@@ -187,6 +187,52 @@ describe Projects::EnvironmentsController do
end end
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 = {}) def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, opts.reverse_merge(namespace_id: project.namespace,
project_id: project, project_id: project,
......
...@@ -195,4 +195,15 @@ FactoryGirl.define do ...@@ -195,4 +195,15 @@ FactoryGirl.define do
factory :kubernetes_project, parent: :empty_project do factory :kubernetes_project, parent: :empty_project do
kubernetes_service kubernetes_service
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 end
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
...@@ -37,13 +37,7 @@ feature 'Environment', :feature do ...@@ -37,13 +37,7 @@ feature 'Environment', :feature do
scenario 'does show deployment SHA' do scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha) 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') 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_terminal_button
end end
end end
...@@ -58,13 +52,7 @@ feature 'Environment', :feature do ...@@ -58,13 +52,7 @@ feature 'Environment', :feature do
scenario 'does show build name' do scenario 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})") expect(page).to have_link("#{build.name} (##{build.id})")
end
scenario 'does show re-deploy button' do
expect(page).to have_link('Re-deploy') expect(page).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_terminal_button
end end
...@@ -117,9 +105,6 @@ feature 'Environment', :feature do ...@@ -117,9 +105,6 @@ feature 'Environment', :feature do
it 'displays a web terminal' do it 'displays a web terminal' do
expect(page).to have_selector('#terminal') 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) expect(page).to have_link(nil, href: environment.external_url)
end end
end end
...@@ -147,10 +132,6 @@ feature 'Environment', :feature do ...@@ -147,10 +132,6 @@ feature 'Environment', :feature do
on_stop: 'close_app') on_stop: 'close_app')
end end
scenario 'does show stop button' do
expect(page).to have_link('Stop')
end
scenario 'does allow to stop environment' do scenario 'does allow to stop environment' do
click_link('Stop') click_link('Stop')
......
%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
import 'jquery';
import es6Promise from 'es6-promise';
import '~/lib/utils/common_utils';
import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data';
es6Promise.polyfill();
describe('PrometheusGraph', () => {
const fixtureName = 'static/environments/metrics.html.raw';
const prometheusGraphContainer = '.prometheus-graph';
const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`;
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
this.prometheusGraph = new PrometheusGraph();
const self = this;
const fakeInit = (metricsResponse) => {
self.prometheusGraph.transformData(metricsResponse);
self.prometheusGraph.createGraph();
};
spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit);
});
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('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);
});
it('has axis, an area, a line and a overlay', () => {
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('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(2);
expect($axisLabelContainer.find('text').length).toBe(4);
});
});
});
/* 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',
},
},
};
...@@ -136,6 +136,7 @@ project: ...@@ -136,6 +136,7 @@ project:
- slack_slash_commands_service - slack_slash_commands_service
- irker_service - irker_service
- pivotaltracker_service - pivotaltracker_service
- prometheus_service
- hipchat_service - hipchat_service
- flowdock_service - flowdock_service
- assembla_service - assembla_service
......
require 'spec_helper'
describe Gitlab::Prometheus, lib: true do
include PrometheusHelpers
subject { described_class.new(api_url: 'https://prometheus.example.com') }
describe '#ping' do
it 'issues a "query" request to the API endpoint' do
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
end
end
# This shared examples expect:
# - 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
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
end
end
context 'when request returns 400 without an error message' do
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
end
end
context 'when request returns 500' do
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
end
end
end
describe '#query' do
let(:prometheus_query) { prometheus_cpu_query('env-slug') }
let(:query_url) { prometheus_query_url(prometheus_query) }
context 'when request returns vector results' do
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
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
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
end
it_behaves_like 'failure response' do
let(:execute_query) { subject.query(prometheus_query) }
end
end
describe '#query_range' do
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
it 'returns nil' do
req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
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
it 'returns data from the API call' do
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
it 'returns []' do
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) { subject.query_range(prometheus_query) }
end
end
end
...@@ -271,7 +271,11 @@ describe Environment, models: true do ...@@ -271,7 +271,11 @@ describe Environment, models: true do
context 'when the environment is unavailable' do context 'when the environment is unavailable' do
let(:project) { create(:kubernetes_project) } let(:project) { create(:kubernetes_project) }
before { environment.stop }
before do
environment.stop
end
it { is_expected.to be_falsy } it { is_expected.to be_falsy }
end end
end end
...@@ -281,20 +285,85 @@ describe Environment, models: true do ...@@ -281,20 +285,85 @@ describe Environment, models: true do
subject { environment.terminals } subject { environment.terminals }
context 'when the environment has terminals' do 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 it 'returns the terminals from the deployment service' do
expect(project.deployment_service). expect(project.deployment_service)
to receive(:terminals).with(environment). .to receive(:terminals).with(environment)
and_return(:fake_terminals) .and_return(:fake_terminals)
is_expected.to eq(:fake_terminals) is_expected.to eq(:fake_terminals)
end end
end end
context 'when the environment does not have terminals' do context 'when the environment does not have terminals' do
before { allow(environment).to receive(:has_terminals?).and_return(false) } before do
it { is_expected.to eq(nil) } 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
end end
......
require 'spec_helper'
describe PrometheusService, models: true, caching: true do
include PrometheusHelpers
include ReactiveCachingHelpers
let(:project) { create(:prometheus_project) }
let(:service) { project.prometheus_service }
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(prometheus_query_url('1'), body: prometheus_value_body('vector')) }
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(prometheus_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) }
around do |example|
Timecop.freeze { example.run }
end
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') }
around do |example|
Timecop.freeze { example.run }
end
subject do
service.calculate_reactive_cache(environment.slug)
end
context 'when service is inactive' do
before do
service.active = false
end
it { is_expected.to be_nil }
end
context 'when Prometheus responds with valid data' do
before do
stub_all_prometheus_requests(environment.slug)
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_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!')
end
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
end
end
end
end
...@@ -6,7 +6,9 @@ describe API::V3::Services, api: true do ...@@ -6,7 +6,9 @@ describe API::V3::Services, api: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
Service.available_services_names.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 describe "DELETE /projects/:id/services/#{service.dasherize}" do
include_context service include_context service
......
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",
"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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment