Commit 120e3522 authored by syasonik's avatar syasonik

Add support for alert-based metric embeds in GFM

The GFM pipeline supports embeds for various types of metrics.
This adds support for embedding dashboards based on a firing
alert.
parent 5889b69a
---
title: Add support for alert-based metric embeds in GFM
merge_request: 25075
author:
type: added
# frozen_string_literal: true
module Banzai
module Filter
# HTML filter that inserts a placeholder element for each
# reference to an alert dashboard.
class InlineAlertMetricsFilter < ::Banzai::Filter::InlineEmbedsFilter
include ::Gitlab::Routing
# Placeholder element for the frontend to use as an
# injection point for charts.
def create_element(params)
doc.document.create_element(
'div',
class: 'js-render-metrics',
'data-dashboard-url': metrics_dashboard_url(params)
)
end
# Search params for selecting alert metrics links. A few
# simple checks is enough to boost performance without
# the cost of doing a full regex match.
def xpath_search
"descendant-or-self::a[contains(@href,'metrics_dashboard') and \
contains(@href,'prometheus/alerts') and \
starts-with(@href, '#{::Gitlab.config.gitlab.url}')]"
end
# Regular expression matching alert dashboard urls
def link_pattern
::Gitlab::Metrics::Dashboard::Url.alert_regex
end
private
# Endpoint FE should hit to collect the appropriate
# chart information
def metrics_dashboard_url(params)
metrics_dashboard_namespace_project_prometheus_alert_url(
params['namespace'],
params['project'],
params['alert'],
embedded: true,
format: :json,
**query_params(params['url'])
)
end
# Parses query params out from full url string into hash.
#
# Ex) 'https://<root>/<project>/metrics_dashboard?title=Title&group=Group'
# --> { title: 'Title', group: 'Group' }
def query_params(url)
::Gitlab::Metrics::Dashboard::Url.parse_query(url)
end
end
end
end
# frozen_string_literal: true
module EE
module Banzai
module Filter
module InlineMetricsRedactorFilter
extend ::Gitlab::Utils::Override
ROUTE = ::Banzai::Filter::InlineMetricsRedactorFilter::Route
override :permissions_by_route
def permissions_by_route
super.concat([
ROUTE.new(::Gitlab::Metrics::Dashboard::Url.alert_regex, :read_prometheus_alerts)
])
end
end
end
end
end
...@@ -7,6 +7,13 @@ module EE ...@@ -7,6 +7,13 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do class_methods do
def metrics_filters
[
::Banzai::Filter::InlineAlertMetricsFilter,
*super
]
end
def reference_filters def reference_filters
[ [
::Banzai::Filter::EpicReferenceFilter, ::Banzai::Filter::EpicReferenceFilter,
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
include PrometheusHelpers
include MetricsDashboardUrlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:prometheus_project) }
let(:issue) { create(:issue, project: project, description: description) }
before do
clear_host_from_memoized_variables
allow(::Gitlab.config.gitlab)
.to receive(:url)
.and_return(urls.root_url.chomp('/'))
stub_licensed_features(prometheus_alerts: true)
project.add_maintainer(user)
sign_in(user)
import_common_metrics
stub_any_prometheus_request_with_response
end
after do
clear_host_from_memoized_variables
end
# This context block can be moved as-is to inside the
# 'internal metrics embeds' block in spec/features/markdown/metrics_spec.rb
# while migrating alerting to CE. Add `:alert_regex` to
# clear_host_from_memoized_variables
context 'for GitLab-managed alerting rules' do
let(:metric) { PrometheusMetric.last }
let!(:alert) { create(:prometheus_alert, project: project, prometheus_metric: metric) }
let(:description) { "# Summary \n[](#{metrics_url})" }
let(:metrics_url) do
urls.metrics_dashboard_project_prometheus_alert_url(
project,
metric.id,
environment_id: alert.environment_id,
embedded: true
)
end
it 'shows embedded metrics' do
visit project_issue_path(project, issue)
expect(page).to have_css('div.prometheus-graph')
expect(page).to have_text(metric.title)
expect(page).to have_text(metric.y_label)
expect(page).not_to have_text(metrics_url)
end
end
def import_common_metrics
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::InlineAlertMetricsFilter do
include FilterSpecHelper
let(:params) { ['foo', 'bar', 12] }
let(:query_params) { {} }
let(:trigger_url) { urls.metrics_dashboard_namespace_project_prometheus_alert_url(*params, query_params) }
let(:dashboard_url) { urls.metrics_dashboard_namespace_project_prometheus_alert_url(*params, **query_params, embedded: true, format: :json) }
it_behaves_like 'a metrics embed filter'
context 'with query params specified' do
let(:query_params) { { timestamp: 'yesterday' } }
it_behaves_like 'a metrics embed filter'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::InlineMetricsRedactorFilter do
include FilterSpecHelper
let_it_be(:project) { create(:project) }
let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
let(:doc) { filter(input) }
context 'for an alert embed' do
let_it_be(:alert) { create(:prometheus_alert, project: project) }
let(:url) do
urls.metrics_dashboard_project_prometheus_alert_url(
project,
alert.prometheus_metric_id,
environment_id: alert.environment_id,
embedded: true
)
end
before do
stub_licensed_features(prometheus_alerts: true)
end
it_behaves_like 'redacts the embed placeholder'
it_behaves_like 'retains the embed placeholder when applicable'
end
end
...@@ -143,3 +143,5 @@ module Banzai ...@@ -143,3 +143,5 @@ module Banzai
end end
end end
end end
Banzai::Filter::InlineMetricsRedactorFilter.prepend_if_ee('EE::Banzai::Filter::InlineMetricsRedactorFilter')
...@@ -2,25 +2,32 @@ ...@@ -2,25 +2,32 @@
require 'spec_helper' require 'spec_helper'
describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidekiq_might_not_need_inline do describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
include PrometheusHelpers include PrometheusHelpers
include GrafanaApiHelpers include GrafanaApiHelpers
include MetricsDashboardUrlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:prometheus_project) }
let_it_be(:environment) { create(:environment, project: project) }
let(:user) { create(:user) }
let(:project) { create(:prometheus_project) }
let(:environment) { create(:environment, project: project) }
let(:issue) { create(:issue, project: project, description: description) } let(:issue) { create(:issue, project: project, description: description) }
let(:description) { "See [metrics dashboard](#{metrics_url}) for info." } let(:description) { "See [metrics dashboard](#{metrics_url}) for info." }
let(:metrics_url) { metrics_project_environment_url(project, environment) } let(:metrics_url) { urls.metrics_project_environment_url(project, environment) }
before do before do
configure_host clear_host_from_memoized_variables
allow(::Gitlab.config.gitlab)
.to receive(:url)
.and_return(urls.root_url.chomp('/'))
project.add_developer(user) project.add_developer(user)
sign_in(user) sign_in(user)
end end
after do after do
restore_host clear_host_from_memoized_variables
end end
context 'internal metrics embeds' do context 'internal metrics embeds' do
...@@ -38,7 +45,7 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek ...@@ -38,7 +45,7 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek
end end
context 'when dashboard params are in included the url' do context 'when dashboard params are in included the url' do
let(:metrics_url) { metrics_project_environment_url(project, environment, **chart_params) } let(:metrics_url) { urls.metrics_project_environment_url(project, environment, **chart_params) }
let(:chart_params) do let(:chart_params) do
{ {
...@@ -81,32 +88,4 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek ...@@ -81,32 +88,4 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek
def import_common_metrics def import_common_metrics
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end end
def configure_host
@original_default_host = default_url_options[:host]
@original_gitlab_url = Gitlab.config.gitlab[:url]
# Ensure we create a metrics url with the right host.
# Configure host for route helpers in specs (also updates root_url):
default_url_options[:host] = Capybara.server_host
# Ensure we identify urls with the appropriate host.
# Configure host to include port in app:
Gitlab.config.gitlab[:url] = root_url.chomp('/')
clear_host_from_memoized_variables
end
def restore_host
default_url_options[:host] = @original_default_host
Gitlab.config.gitlab[:url] = @original_gitlab_url
clear_host_from_memoized_variables
end
def clear_host_from_memoized_variables
[:metrics_regex, :grafana_regex].each do |method_name|
Gitlab::Metrics::Dashboard::Url.clear_memoization(method_name)
end
end
end end
# frozen_string_literal: true
module MetricsDashboardUrlHelpers
# Using the url_helpers available in the test suite uses
# the sample host, but the urls generated may need to
# point to the configured host in the :js trait
def urls
::Gitlab::Routing.url_helpers
end
def clear_host_from_memoized_variables
[:metrics_regex, :grafana_regex, :cluster_metrics, :alerts_regex].each do |method_name|
Gitlab::Metrics::Dashboard::Url.clear_memoization(method_name)
end
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