Commit 20614140 authored by Pawel Chojnacki's avatar Pawel Chojnacki

Additional metrics initial work, with working metrics listing, but without...

Additional metrics initial work, with working metrics listing, but without actoual metrics mesurements
parent 78de1c05
...@@ -129,6 +129,12 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -129,6 +129,12 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def additional_metrics
additional_metrics = environment.additional_metrics || {}
render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content
end
private private
def verify_api_request! def verify_api_request!
......
class Projects::PrometheusController < Projects::ApplicationController
before_action :authorize_read_project!
def active_metrics
return render_404 unless has_prometheus_metrics?
matched_metrics = prometheus_service.reactive_query(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, &:itself)
if matched_metrics
render json: matched_metrics, status: :ok
else
head :no_content
end
end
def prometheus_service
project.monitoring_service
end
def has_prometheus_metrics?
prometheus_service&.respond_to?(:reactive_query)
end
end
...@@ -149,10 +149,18 @@ class Environment < ActiveRecord::Base ...@@ -149,10 +149,18 @@ class Environment < ActiveRecord::Base
project.monitoring_service.present? && available? && last_deployment.present? project.monitoring_service.present? && available? && last_deployment.present?
end end
def has_additional_metrics?
has_metrics? && project.monitoring_service&.respond_to?(:reactive_query)
end
def metrics def metrics
project.monitoring_service.environment_metrics(self) if has_metrics? project.monitoring_service.environment_metrics(self) if has_metrics?
end end
def additional_metrics
project.monitoring_service.reactive_query(Gitlab::Prometheus::Queries::AdditionalMetricsQuery, self.id) if has_additional_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:
......
...@@ -64,23 +64,26 @@ class PrometheusService < MonitoringService ...@@ -64,23 +64,26 @@ class PrometheusService < MonitoringService
end end
def environment_metrics(environment) def environment_metrics(environment)
with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &:itself) with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics))
end end
def deployment_metrics(deployment) def deployment_metrics(deployment)
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &:itself) metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics))
metrics&.merge(deployment_time: created_at.to_i) || {} metrics&.merge(deployment_time: created_at.to_i) || {}
end end
def reactive_query(query_class, *args, &block)
calculate_reactive_cache(query_class, *args, &block)
end
# Cache metrics for specific environment # Cache metrics for specific environment
def calculate_reactive_cache(query_class_name, *args) def calculate_reactive_cache(query_class_name, *args)
return unless active? && project && !project.pending_delete? return unless active? && project && !project.pending_delete?
metrics = Kernel.const_get(query_class_name).new(client).query(*args) data = Kernel.const_get(query_class_name).new(client).query(*args)
{ {
success: true, success: true,
metrics: metrics, data: data,
last_update: Time.now.utc last_update: Time.now.utc
} }
rescue Gitlab::PrometheusError => err rescue Gitlab::PrometheusError => err
...@@ -90,4 +93,11 @@ class PrometheusService < MonitoringService ...@@ -90,4 +93,11 @@ class PrometheusService < MonitoringService
def client def client
@prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url) @prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url)
end end
private
def rename_data_to_metrics(metrics)
metrics[:metrics] = metrics.delete :data
metrics
end
end end
- group: Kubernetes
priority: 1
metrics:
- title: "Memory usage"
detect: container_memory_usage_bytes
weight: 1
queries:
- query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20'
label: Container memory
unit: MiB
- title: "Current memory usage"
detect: container_memory_usage_bytes
weight: 1
queries:
- query: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20'
unit: MiB
- title: "CPU usage"
detect: container_cpu_usage_seconds_total
weight: 1
queries:
- query_range: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100'
- title: "Current CPU usage"
detect: container_cpu_usage_seconds_total
weight: 1
queries:
- query: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100'
...@@ -72,6 +72,10 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -72,6 +72,10 @@ constraints(ProjectUrlConstrainer.new) do
resource :mattermost, only: [:new, :create] resource :mattermost, only: [:new, :create]
namespace :prometheus do
get :active_metrics
end
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do member do
put :enable put :enable
...@@ -152,6 +156,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -152,6 +156,7 @@ constraints(ProjectUrlConstrainer.new) do
post :stop post :stop
get :terminal get :terminal
get :metrics get :metrics
get :additional_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
......
module Gitlab::Prometheus
class Metric
attr_reader :group, :title, :detect, :weight, :queries
def initialize(group, title, detect, weight, queries = [])
@group = group
@title = title
@detect = detect
@weight = weight
@queries = queries
end
def self.metric_from_entry(group, entry)
missing_fields = [:title, :detect, :weight, :queries].select { |key| !entry.has_key?(key) }
raise ParsingError.new("entry missing required fields #{missing_fields}") unless missing_fields.empty?
Metric.new(group, entry[:title], entry[:detect], entry[:weight], entry[:queries])
end
def self.metrics_from_list(group, list)
list.map { |entry| metric_from_entry(group, entry) }
end
def self.additional_metrics_raw
@additional_metrics_raw ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml')).map(&:deep_symbolize_keys)
end
end
end
module Gitlab::Prometheus
class MetricGroup
attr_reader :priority, :name
attr_accessor :metrics
def initialize(name, priority, metrics = [])
@name = name
@priority = priority
@metrics = metrics
end
def self.all
load_groups_from_yaml
end
def self.group_from_entry(entry)
missing_fields = [:group, :priority, :metrics].select { |key| !entry.has_key?(key) }
raise ParsingError.new("entry missing required fields #{missing_fields}") unless missing_fields.empty?
group = MetricGroup.new(entry[:group], entry[:priority])
group.metrics = Metric.metrics_from_list(group, entry[:metrics])
group
end
def self.load_groups_from_yaml
additional_metrics_raw.map(&method(:group_from_entry))
end
def self.additional_metrics_raw
@additional_metrics_raw ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml'))&.map(&:deep_symbolize_keys).freeze
end
end
end
module Gitlab::Prometheus
module MetricsSources
def self.additional_metrics
@additional_metrics ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml')).deep_symbolize_keys.freeze
end
end
end
module Gitlab::Prometheus
ParsingError = Class.new(StandardError)
end
module Gitlab::Prometheus::Queries
class AdditionalMetricsQuery < BaseQuery
def self.metrics
@metrics ||= YAML.load_file(Rails.root.join('config/custom_metrics.yml')).freeze
end
def query(environment_id)
environment = Environment.find_by(id: environment_id)
context = {
environment_slug: environment.slug,
environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
}
timeframe_start = 8.hours.ago.to_f
timeframe_end = Time.now.to_f
matched_metrics.map do |group|
group[:metrics].map! do |metric|
metric[:queries].map! do |query|
query = query.symbolize_keys
query[:result] =
if query.has_key?(:query_range)
client_query_range(query[:query_range] % context, start: timeframe_start, stop: timeframe_end)
else
client_query(query[:query] % context, time: timeframe_end)
end
query
end
metric
end
group
end
end
def process_query(group, query)
result = if query.has_key?(:query_range)
client_query_range(query[:query_range] % context, start: timeframe_start, stop: timeframe_end)
else
client_query(query[:query] % context, time: timeframe_end)
end
contains_metrics = result.all? do |item|
item&.[](:values)&.any? || item&.[](:value)&.any?
end
end
def process_result(query_result)
contains_metrics = query_result.all? do |item|
item&.[](:values)&.any? || item&.[](:value)&.any?
end
contains_metrics
end
def matched_metrics
label_values = client_label_values || []
result = Gitlab::Prometheus::MetricsSources.additional_metrics.map do |group|
group[:metrics].map!(&:symbolize_keys)
group[:metrics].select! do |metric|
matcher = Regexp.compile(metric[:detect])
label_values.any? &matcher.method(:match)
end
group
end
result.select {|group| !group[:metrics].empty?}
end
end
end
...@@ -3,7 +3,7 @@ module Gitlab ...@@ -3,7 +3,7 @@ module Gitlab
module Queries module Queries
class BaseQuery class BaseQuery
attr_accessor :client attr_accessor :client
delegate :query_range, :query, to: :client, prefix: true delegate :query_range, :query, :label_values, :series, to: :client, prefix: true
def raw_memory_usage_query(environment_slug) def raw_memory_usage_query(environment_slug)
%{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20} %{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20}
......
module Gitlab::Prometheus::Queries
class MatchedMetricsQuery < BaseQuery
MAX_QUERY_ITEMS = 40.freeze
def self.metrics
@metrics ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml')).map(&:deep_symbolize_keys)
end
def query
groups_data.map do |group, data|
{
group: group.name,
priority: group.priority,
active_metrics: data[:active_metrics],
metrics_missing_requirements: data[:metrics_missing_requirements]
}
end
end
def groups_data
metrics_series = metrics_with_series(Gitlab::Prometheus::MetricGroup.all)
lookup = active_series_lookup(metrics_series)
groups = {}
metrics_series.each do |metrics, series|
groups[metrics.group] ||= { active_metrics: 0, metrics_missing_requirements: 0 }
group = groups[metrics.group]
if series.all?(&lookup.method(:has_key?))
group[:active_metrics] += 1
else
group[:metrics_missing_requirements] += 1
end
group
end
groups
end
def active_series_lookup(metrics)
timeframe_start = 8.hours.ago
timeframe_end = Time.now
series = metrics.flat_map { |metrics, series| series }.uniq
lookup = series.each_slice(MAX_QUERY_ITEMS).flat_map do |batched_series|
client_series(*batched_series, start: timeframe_start, stop: timeframe_end)
.select(&method(:has_matching_label))
.map { |series_info| [series_info['__name__'], true] }
end
lookup.to_h
end
def has_matching_label(series_info)
series_info.has_key?('environment')
end
def metrics_with_series(metric_groups)
label_values = client_label_values || []
metrics = metric_groups.flat_map do |group|
group.metrics.map do |metric|
matcher = Regexp.compile(metric.detect)
[metric, label_values.select(&matcher.method(:match))]
end
end
metrics.select { |metric, labels| labels&.any? }
end
end
end
...@@ -29,6 +29,14 @@ module Gitlab ...@@ -29,6 +29,14 @@ module Gitlab
end end
end end
def label_values(name='__name__')
json_api_get("label/#{name}/values")
end
def series(*matches, start: 8.hours.ago, stop: Time.now)
json_api_get('series', 'match': matches, start: start.to_f, end: stop.to_f)
end
private private
def json_api_get(type, args = {}) def json_api_get(type, args = {})
......
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