Skip to content

Commit 2061414

Browse files
committed
Additional metrics initial work, with working metrics listing, but without actoual metrics mesurements
1 parent 78de1c0 commit 2061414

14 files changed

+304
-6
lines changed

app/controllers/projects/environments_controller.rb

+6
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ def metrics
129129
end
130130
end
131131

132+
def additional_metrics
133+
additional_metrics = environment.additional_metrics || {}
134+
135+
render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content
136+
end
137+
132138
private
133139

134140
def verify_api_request!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class Projects::PrometheusController < Projects::ApplicationController
2+
before_action :authorize_read_project!
3+
4+
def active_metrics
5+
return render_404 unless has_prometheus_metrics?
6+
matched_metrics = prometheus_service.reactive_query(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, &:itself)
7+
8+
if matched_metrics
9+
render json: matched_metrics, status: :ok
10+
else
11+
head :no_content
12+
end
13+
end
14+
15+
def prometheus_service
16+
project.monitoring_service
17+
end
18+
19+
def has_prometheus_metrics?
20+
prometheus_service&.respond_to?(:reactive_query)
21+
end
22+
end

app/models/environment.rb

+8
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,18 @@ def has_metrics?
149149
project.monitoring_service.present? && available? && last_deployment.present?
150150
end
151151

152+
def has_additional_metrics?
153+
has_metrics? && project.monitoring_service&.respond_to?(:reactive_query)
154+
end
155+
152156
def metrics
153157
project.monitoring_service.environment_metrics(self) if has_metrics?
154158
end
155159

160+
def additional_metrics
161+
project.monitoring_service.reactive_query(Gitlab::Prometheus::Queries::AdditionalMetricsQuery, self.id) if has_additional_metrics?
162+
end
163+
156164
# An environment name is not necessarily suitable for use in URLs, DNS
157165
# or other third-party contexts, so provide a slugified version. A slug has
158166
# the following properties:

app/models/project_services/prometheus_service.rb

+15-5
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,26 @@ def test(*args)
6464
end
6565

6666
def environment_metrics(environment)
67-
with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &:itself)
67+
with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics))
6868
end
6969

7070
def deployment_metrics(deployment)
71-
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &:itself)
71+
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics))
7272
metrics&.merge(deployment_time: created_at.to_i) || {}
7373
end
7474

75+
def reactive_query(query_class, *args, &block)
76+
calculate_reactive_cache(query_class, *args, &block)
77+
end
78+
7579
# Cache metrics for specific environment
7680
def calculate_reactive_cache(query_class_name, *args)
7781
return unless active? && project && !project.pending_delete?
7882

79-
metrics = Kernel.const_get(query_class_name).new(client).query(*args)
80-
83+
data = Kernel.const_get(query_class_name).new(client).query(*args)
8184
{
8285
success: true,
83-
metrics: metrics,
86+
data: data,
8487
last_update: Time.now.utc
8588
}
8689
rescue Gitlab::PrometheusError => err
@@ -90,4 +93,11 @@ def calculate_reactive_cache(query_class_name, *args)
9093
def client
9194
@prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url)
9295
end
96+
97+
private
98+
99+
def rename_data_to_metrics(metrics)
100+
metrics[:metrics] = metrics.delete :data
101+
metrics
102+
end
93103
end

config/additional_metrics.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
- group: Kubernetes
2+
priority: 1
3+
metrics:
4+
- title: "Memory usage"
5+
detect: container_memory_usage_bytes
6+
weight: 1
7+
queries:
8+
- query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20'
9+
label: Container memory
10+
unit: MiB
11+
- title: "Current memory usage"
12+
detect: container_memory_usage_bytes
13+
weight: 1
14+
queries:
15+
- query: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20'
16+
unit: MiB
17+
- title: "CPU usage"
18+
detect: container_cpu_usage_seconds_total
19+
weight: 1
20+
queries:
21+
- query_range: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100'
22+
- title: "Current CPU usage"
23+
detect: container_cpu_usage_seconds_total
24+
weight: 1
25+
queries:
26+
- query: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100'

config/routes/project.rb

+5
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272

7373
resource :mattermost, only: [:new, :create]
7474

75+
namespace :prometheus do
76+
get :active_metrics
77+
end
78+
7579
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
7680
member do
7781
put :enable
@@ -152,6 +156,7 @@
152156
post :stop
153157
get :terminal
154158
get :metrics
159+
get :additional_metrics
155160
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
156161
end
157162

lib/gitlab/prometheus/metric.rb

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module Gitlab::Prometheus
2+
class Metric
3+
attr_reader :group, :title, :detect, :weight, :queries
4+
5+
def initialize(group, title, detect, weight, queries = [])
6+
@group = group
7+
@title = title
8+
@detect = detect
9+
@weight = weight
10+
@queries = queries
11+
end
12+
13+
def self.metric_from_entry(group, entry)
14+
missing_fields = [:title, :detect, :weight, :queries].select { |key| !entry.has_key?(key) }
15+
raise ParsingError.new("entry missing required fields #{missing_fields}") unless missing_fields.empty?
16+
17+
Metric.new(group, entry[:title], entry[:detect], entry[:weight], entry[:queries])
18+
end
19+
20+
def self.metrics_from_list(group, list)
21+
list.map { |entry| metric_from_entry(group, entry) }
22+
end
23+
24+
def self.additional_metrics_raw
25+
@additional_metrics_raw ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml')).map(&:deep_symbolize_keys)
26+
end
27+
end
28+
end

lib/gitlab/prometheus/metric_group.rb

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module Gitlab::Prometheus
2+
class MetricGroup
3+
attr_reader :priority, :name
4+
attr_accessor :metrics
5+
6+
def initialize(name, priority, metrics = [])
7+
@name = name
8+
@priority = priority
9+
@metrics = metrics
10+
end
11+
12+
def self.all
13+
load_groups_from_yaml
14+
end
15+
16+
def self.group_from_entry(entry)
17+
missing_fields = [:group, :priority, :metrics].select { |key| !entry.has_key?(key) }
18+
raise ParsingError.new("entry missing required fields #{missing_fields}") unless missing_fields.empty?
19+
20+
group = MetricGroup.new(entry[:group], entry[:priority])
21+
group.metrics = Metric.metrics_from_list(group, entry[:metrics])
22+
group
23+
end
24+
25+
def self.load_groups_from_yaml
26+
additional_metrics_raw.map(&method(:group_from_entry))
27+
end
28+
29+
def self.additional_metrics_raw
30+
@additional_metrics_raw ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml'))&.map(&:deep_symbolize_keys).freeze
31+
end
32+
end
33+
end
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Gitlab::Prometheus
2+
module MetricsSources
3+
def self.additional_metrics
4+
@additional_metrics ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml')).deep_symbolize_keys.freeze
5+
end
6+
end
7+
end
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Gitlab::Prometheus
2+
ParsingError = Class.new(StandardError)
3+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module Gitlab::Prometheus::Queries
2+
class AdditionalMetricsQuery < BaseQuery
3+
def self.metrics
4+
@metrics ||= YAML.load_file(Rails.root.join('config/custom_metrics.yml')).freeze
5+
end
6+
7+
def query(environment_id)
8+
environment = Environment.find_by(id: environment_id)
9+
10+
context = {
11+
environment_slug: environment.slug,
12+
environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
13+
}
14+
15+
timeframe_start = 8.hours.ago.to_f
16+
timeframe_end = Time.now.to_f
17+
18+
matched_metrics.map do |group|
19+
group[:metrics].map! do |metric|
20+
metric[:queries].map! do |query|
21+
query = query.symbolize_keys
22+
query[:result] =
23+
if query.has_key?(:query_range)
24+
client_query_range(query[:query_range] % context, start: timeframe_start, stop: timeframe_end)
25+
else
26+
client_query(query[:query] % context, time: timeframe_end)
27+
end
28+
query
29+
end
30+
metric
31+
end
32+
group
33+
end
34+
end
35+
36+
def process_query(group, query)
37+
result = if query.has_key?(:query_range)
38+
client_query_range(query[:query_range] % context, start: timeframe_start, stop: timeframe_end)
39+
else
40+
client_query(query[:query] % context, time: timeframe_end)
41+
end
42+
contains_metrics = result.all? do |item|
43+
item&.[](:values)&.any? || item&.[](:value)&.any?
44+
end
45+
end
46+
47+
def process_result(query_result)
48+
contains_metrics = query_result.all? do |item|
49+
item&.[](:values)&.any? || item&.[](:value)&.any?
50+
end
51+
52+
contains_metrics
53+
end
54+
55+
def matched_metrics
56+
label_values = client_label_values || []
57+
58+
result = Gitlab::Prometheus::MetricsSources.additional_metrics.map do |group|
59+
group[:metrics].map!(&:symbolize_keys)
60+
group[:metrics].select! do |metric|
61+
matcher = Regexp.compile(metric[:detect])
62+
label_values.any? &matcher.method(:match)
63+
end
64+
group
65+
end
66+
67+
result.select {|group| !group[:metrics].empty?}
68+
end
69+
end
70+
end

lib/gitlab/prometheus/queries/base_query.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Prometheus
33
module Queries
44
class BaseQuery
55
attr_accessor :client
6-
delegate :query_range, :query, to: :client, prefix: true
6+
delegate :query_range, :query, :label_values, :series, to: :client, prefix: true
77

88
def raw_memory_usage_query(environment_slug)
99
%{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
module Gitlab::Prometheus::Queries
2+
class MatchedMetricsQuery < BaseQuery
3+
MAX_QUERY_ITEMS = 40.freeze
4+
5+
def self.metrics
6+
@metrics ||= YAML.load_file(Rails.root.join('config/additional_metrics.yml')).map(&:deep_symbolize_keys)
7+
end
8+
9+
def query
10+
groups_data.map do |group, data|
11+
{
12+
group: group.name,
13+
priority: group.priority,
14+
active_metrics: data[:active_metrics],
15+
metrics_missing_requirements: data[:metrics_missing_requirements]
16+
}
17+
end
18+
end
19+
20+
def groups_data
21+
metrics_series = metrics_with_series(Gitlab::Prometheus::MetricGroup.all)
22+
lookup = active_series_lookup(metrics_series)
23+
24+
groups = {}
25+
26+
metrics_series.each do |metrics, series|
27+
groups[metrics.group] ||= { active_metrics: 0, metrics_missing_requirements: 0 }
28+
group = groups[metrics.group]
29+
30+
if series.all?(&lookup.method(:has_key?))
31+
group[:active_metrics] += 1
32+
else
33+
group[:metrics_missing_requirements] += 1
34+
end
35+
group
36+
end
37+
38+
groups
39+
end
40+
41+
def active_series_lookup(metrics)
42+
timeframe_start = 8.hours.ago
43+
timeframe_end = Time.now
44+
45+
series = metrics.flat_map { |metrics, series| series }.uniq
46+
47+
lookup = series.each_slice(MAX_QUERY_ITEMS).flat_map do |batched_series|
48+
client_series(*batched_series, start: timeframe_start, stop: timeframe_end)
49+
.select(&method(:has_matching_label))
50+
.map { |series_info| [series_info['__name__'], true] }
51+
end
52+
lookup.to_h
53+
end
54+
55+
def has_matching_label(series_info)
56+
series_info.has_key?('environment')
57+
end
58+
59+
def metrics_with_series(metric_groups)
60+
label_values = client_label_values || []
61+
62+
metrics = metric_groups.flat_map do |group|
63+
group.metrics.map do |metric|
64+
matcher = Regexp.compile(metric.detect)
65+
[metric, label_values.select(&matcher.method(:match))]
66+
end
67+
end
68+
69+
metrics.select { |metric, labels| labels&.any? }
70+
end
71+
end
72+
end

lib/gitlab/prometheus_client.rb

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ def query_range(query, start: 8.hours.ago, stop: Time.now)
2929
end
3030
end
3131

32+
def label_values(name='__name__')
33+
json_api_get("label/#{name}/values")
34+
end
35+
36+
def series(*matches, start: 8.hours.ago, stop: Time.now)
37+
json_api_get('series', 'match': matches, start: start.to_f, end: stop.to_f)
38+
end
39+
3240
private
3341

3442
def json_api_get(type, args = {})

0 commit comments

Comments
 (0)