summaryrefslogtreecommitdiff
path: root/app/models/project_services/prometheus_service.rb
blob: 3f90d841d58410fd39231175523e74f6a6d43b38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
class PrometheusService < MonitoringService
  include ReactiveService

  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
  boolean_accessor :manual_configuration

  with_options presence: true, if: :manual_configuration? do
    validates :api_url, url: true
  end

  before_save :synchronize_service_state!

  after_save :clear_reactive_cache!

  def initialize_properties
    if properties.nil?
      self.properties = {}
    end
  end

  def show_active_box?
    false
  end

  def title
    'Prometheus'
  end

  def description
    s_('PrometheusService|Time-series monitoring service')
  end

  def self.to_param
    'prometheus'
  end

  def fields
    [
      {
        type: 'fieldset',
        name: 'manual_configuration',
        legend: 'Manual Configuration',
        fields: [
          {
            type: 'checkbox',
            name: 'manual_configuration',
            title: s_('PrometheusService|Active'),
            required: true
          }
        ]
      },
      {
        type: 'text',
        name: 'api_url',
        title: 'API URL',
        placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
        help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
        required: true
      }
    ]
  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 environment_metrics(environment)
    with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics))
  end

  def deployment_metrics(deployment)
    metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &method(:rename_data_to_metrics))
    metrics&.merge(deployment_time: deployment.created_at.to_i) || {}
  end

  def additional_environment_metrics(environment)
    with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery.name, environment.id, &:itself)
  end

  def additional_deployment_metrics(deployment)
    with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.environment.id, deployment.id, &:itself)
  end

  def matched_metrics
    with_reactive_cache(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, nil, &:itself)
  end

  # Cache metrics for specific environment
  def calculate_reactive_cache(query_class_name, environment_id, *args)
    return unless active? && project && !project.pending_delete?

    client = client(environment_id)

    data = Kernel.const_get(query_class_name).new(client).query(environment_id, *args)
    {
      success: true,
      data: data,
      last_update: Time.now.utc
    }
  rescue Gitlab::PrometheusError => err
    { success: false, result: err.message }
  end

  def client(environment_id = nil)
    if manual_configuration?
      Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url))
    else
      cluster = cluster_with_prometheus(environment_id)
      raise Gitlab::PrometheusError, "couldn't find cluster with Prometheus installed" unless cluster

      rest_client = client_from_cluster(cluster)
      raise Gitlab::PrometheusError, "couldn't create proxy Prometheus client" unless rest_client

      Gitlab::PrometheusClient.new(rest_client)
    end
  end

  def prometheus_installed?
    return false if template?

    project.clusters.enabled.any? { |cluster| cluster.application_prometheus&.installed? }
  end

  private

  def cluster_with_prometheus(environment_id = nil)
    clusters = if environment_id
                 ::Environment.find_by(id: environment_id).try do |env|
                   # sort results by descending order based on environment_scope being longer
                   # thus more closely matching environment slug
                   project.clusters.enabled.for_environment(env).sort_by { |c| c.environment_scope&.length }.reverse!
                 end
               else
                 project.clusters.enabled.for_all_environments
               end

    clusters&.detect { |cluster| cluster.application_prometheus&.installed? }
  end

  def client_from_cluster(cluster)
    cluster.application_prometheus.proxy_client
  end

  def rename_data_to_metrics(metrics)
    metrics[:metrics] = metrics.delete :data
    metrics
  end

  def synchronize_service_state!
    self.active = prometheus_installed? || self.manual_configuration?

    true
  end
end