diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 04:45:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 04:45:44 +0300 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/lib/gitlab/alert_management | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/lib/gitlab/alert_management')
6 files changed, 769 insertions, 1 deletions
diff --git a/spec/lib/gitlab/alert_management/alert_params_spec.rb b/spec/lib/gitlab/alert_management/alert_params_spec.rb index 1fe27365c83..c3171be5e29 100644 --- a/spec/lib/gitlab/alert_management/alert_params_spec.rb +++ b/spec/lib/gitlab/alert_management/alert_params_spec.rb @@ -34,7 +34,9 @@ RSpec.describe Gitlab::AlertManagement::AlertParams do hosts: ['gitlab.com'], payload: payload, started_at: started_at, - fingerprint: nil + ended_at: nil, + fingerprint: nil, + environment: nil ) end diff --git a/spec/lib/gitlab/alert_management/payload/base_spec.rb b/spec/lib/gitlab/alert_management/payload/base_spec.rb new file mode 100644 index 00000000000..e0f63bad05d --- /dev/null +++ b/spec/lib/gitlab/alert_management/payload/base_spec.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::AlertManagement::Payload::Base do + let_it_be(:project) { create(:project) } + let(:raw_payload) { {} } + let(:payload_class) { described_class } + + subject(:parsed_payload) { payload_class.new(project: project, payload: raw_payload) } + + describe '.attribute' do + subject { parsed_payload.test } + + context 'with a single path provided' do + let(:payload_class) do + Class.new(described_class) do + attribute :test, paths: [['test']] + end + end + + it { is_expected.to be_nil } + + context 'and a matching value' do + let(:raw_payload) { { 'test' => 'value' } } + + it { is_expected.to eq 'value' } + end + end + + context 'with multiple paths provided' do + let(:payload_class) do + Class.new(described_class) do + attribute :test, paths: [['test'], %w(alt test)] + end + end + + it { is_expected.to be_nil } + + context 'and a matching value' do + let(:raw_payload) { { 'alt' => { 'test' => 'value' } } } + + it { is_expected.to eq 'value' } + end + end + + context 'with a fallback provided' do + let(:payload_class) do + Class.new(described_class) do + attribute :test, paths: [['test']], fallback: -> { 'fallback' } + end + end + + it { is_expected.to eq('fallback') } + + context 'and a matching value' do + let(:raw_payload) { { 'test' => 'value' } } + + it { is_expected.to eq 'value' } + end + end + + context 'with a time type provided' do + let(:test_time) { Time.current.change(usec: 0) } + + let(:payload_class) do + Class.new(described_class) do + attribute :test, paths: [['test']], type: :time + end + end + + it { is_expected.to be_nil } + + context 'with a compatible matching value' do + let(:raw_payload) { { 'test' => test_time.to_s } } + + it { is_expected.to eq test_time } + end + + context 'with a value in rfc3339 format' do + let(:raw_payload) { { 'test' => test_time.rfc3339 } } + + it { is_expected.to eq test_time } + end + + context 'with an incompatible matching value' do + let(:raw_payload) { { 'test' => 'bad time' } } + + it { is_expected.to be_nil } + end + end + + context 'with an integer type provided' do + let(:payload_class) do + Class.new(described_class) do + attribute :test, paths: [['test']], type: :integer + end + end + + it { is_expected.to be_nil } + + context 'with a compatible matching value' do + let(:raw_payload) { { 'test' => '15' } } + + it { is_expected.to eq 15 } + end + + context 'with an incompatible matching value' do + let(:raw_payload) { { 'test' => String } } + + it { is_expected.to be_nil } + end + + context 'with an incompatible matching value' do + let(:raw_payload) { { 'test' => 'apple' } } + + it { is_expected.to be_nil } + end + end + end + + describe '#alert_params' do + before do + allow(parsed_payload).to receive(:title).and_return('title') + allow(parsed_payload).to receive(:description).and_return('description') + end + + subject { parsed_payload.alert_params } + + it { is_expected.to eq({ description: 'description', project_id: project.id, title: 'title' }) } + end + + describe '#gitlab_fingerprint' do + subject { parsed_payload.gitlab_fingerprint } + + it { is_expected.to be_nil } + + context 'when plain_gitlab_fingerprint is defined' do + before do + allow(parsed_payload) + .to receive(:plain_gitlab_fingerprint) + .and_return('fingerprint') + end + + it 'returns a fingerprint' do + is_expected.to eq(Digest::SHA1.hexdigest('fingerprint')) + end + end + end + + describe '#environment' do + let_it_be(:environment) { create(:environment, project: project, name: 'production') } + + subject { parsed_payload.environment } + + before do + allow(parsed_payload).to receive(:environment_name).and_return(environment_name) + end + + context 'without an environment name' do + let(:environment_name) { nil } + + it { is_expected.to be_nil } + end + + context 'with a non-matching environment name' do + let(:environment_name) { 'other_environment' } + + it { is_expected.to be_nil } + end + + context 'with a matching environment name' do + let(:environment_name) { 'production' } + + it { is_expected.to eq(environment) } + end + end + + describe '#resolved?' do + before do + allow(parsed_payload).to receive(:status).and_return(status) + end + + subject { parsed_payload.resolved? } + + context 'when status is not defined' do + let(:status) { nil } + + it { is_expected.to be_falsey } + end + + context 'when status is not resovled' do + let(:status) { 'firing' } + + it { is_expected.to be_falsey } + end + + context 'when status is resovled' do + let(:status) { 'resolved' } + + it { is_expected.to be_truthy } + end + end + + describe '#has_required_attributes?' do + subject { parsed_payload.has_required_attributes? } + + it { is_expected.to be(true) } + end +end diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb new file mode 100644 index 00000000000..538a822503e --- /dev/null +++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::AlertManagement::Payload::Generic do + let_it_be(:project) { build_stubbed(:project) } + let(:raw_payload) { {} } + + let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) } + + it_behaves_like 'subclass has expected api' + + describe '#title' do + subject { parsed_payload.title } + + it_behaves_like 'parsable alert payload field with fallback', 'New: Incident', 'title' + end + + describe '#severity' do + subject { parsed_payload.severity } + + it_behaves_like 'parsable alert payload field with fallback', 'critical', 'severity' + end + + describe '#monitoring_tool' do + subject { parsed_payload.monitoring_tool } + + it_behaves_like 'parsable alert payload field', 'monitoring_tool' + end + + describe '#service' do + subject { parsed_payload.service } + + it_behaves_like 'parsable alert payload field', 'service' + end + + describe '#hosts' do + subject { parsed_payload.hosts } + + it_behaves_like 'parsable alert payload field', 'hosts' + end + + describe '#starts_at' do + let(:current_time) { Time.current.change(usec: 0).utc } + + subject { parsed_payload.starts_at } + + around do |example| + Timecop.freeze(current_time) { example.run } + end + + context 'without start_time' do + it { is_expected.to eq(current_time) } + end + + context "with start_time" do + let(:value) { 10.minutes.ago.change(usec: 0).utc } + + before do + raw_payload['start_time'] = value.to_s + end + + it { is_expected.to eq(value) } + end + end + + describe '#runbook' do + subject { parsed_payload.runbook } + + it_behaves_like 'parsable alert payload field', 'runbook' + end + + describe '#gitlab_fingerprint' do + let(:plain_fingerprint) { 'fingerprint' } + let(:raw_payload) { { 'fingerprint' => plain_fingerprint } } + + subject { parsed_payload.gitlab_fingerprint } + + it 'returns a fingerprint' do + is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint)) + end + end + + describe '#environment_name' do + subject { parsed_payload.environment_name } + + it_behaves_like 'parsable alert payload field', 'gitlab_environment_name' + end +end diff --git a/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb new file mode 100644 index 00000000000..862b5b2bdc3 --- /dev/null +++ b/spec/lib/gitlab/alert_management/payload/managed_prometheus_spec.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::AlertManagement::Payload::ManagedPrometheus do + let_it_be(:project) { create(:project) } + let(:raw_payload) { {} } + + let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) } + + it_behaves_like 'subclass has expected api' + + shared_context 'with gitlab alert' do + let_it_be(:gitlab_alert) { create(:prometheus_alert, project: project) } + let(:metric_id) { gitlab_alert.prometheus_metric_id.to_s } + let(:alert_id) { gitlab_alert.id.to_s } + end + + describe '#metric_id' do + subject { parsed_payload.metric_id } + + it { is_expected.to be_nil } + + context 'with gitlab_alert_id' do + let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => '12' } } } + + it { is_expected.to eq(12) } + end + end + + describe '#gitlab_prometheus_alert_id' do + subject { parsed_payload.gitlab_prometheus_alert_id } + + it { is_expected.to be_nil } + + context 'with gitlab_alert_id' do + let(:raw_payload) { { 'labels' => { 'gitlab_prometheus_alert_id' => '12' } } } + + it { is_expected.to eq(12) } + end + end + + describe '#gitlab_alert' do + subject { parsed_payload.gitlab_alert } + + context 'without alert info in payload' do + it { is_expected.to be_nil } + end + + context 'with metric id in payload' do + let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } } + let(:metric_id) { '-1' } + + context 'without matching alert' do + it { is_expected.to be_nil } + end + + context 'with matching alert' do + include_context 'with gitlab alert' + + it { is_expected.to eq(gitlab_alert) } + + context 'when unclear which alert applies' do + # With multiple alerts for different environments, + # we can't be sure which prometheus alert the payload + # belongs to + let_it_be(:another_alert) do + create(:prometheus_alert, + prometheus_metric: gitlab_alert.prometheus_metric, + project: project) + end + + it { is_expected.to be_nil } + end + end + end + + context 'with alert id' do + # gitlab_prometheus_alert_id is a stronger identifier, + # but was added after gitlab_alert_id; we won't + # see it without gitlab_alert_id also present + let(:raw_payload) do + { + 'labels' => { + 'gitlab_alert_id' => metric_id, + 'gitlab_prometheus_alert_id' => alert_id + } + } + end + + context 'without matching alert' do + let(:alert_id) { '-1' } + let(:metric_id) { '-1' } + + it { is_expected.to be_nil } + end + + context 'with matching alerts' do + include_context 'with gitlab alert' + + it { is_expected.to eq(gitlab_alert) } + end + end + end + + describe '#full_query' do + subject { parsed_payload.full_query } + + it { is_expected.to be_nil } + + context 'with gitlab alert' do + include_context 'with gitlab alert' + + let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } } + + it { is_expected.to eq(gitlab_alert.full_query) } + end + + context 'with sufficient fallback info' do + let(:raw_payload) { { 'generatorURL' => 'http://localhost:9090/graph?g0.expr=vector%281%29' } } + + it { is_expected.to eq('vector(1)') } + end + end + + describe '#environment' do + subject { parsed_payload.environment } + + context 'with gitlab alert' do + include_context 'with gitlab alert' + + let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } } + + it { is_expected.to eq(gitlab_alert.environment) } + end + + context 'with sufficient fallback info' do + let_it_be(:environment) { create(:environment, project: project, name: 'production') } + let(:raw_payload) do + { + 'labels' => { + 'gitlab_alert_id' => '-1', + 'gitlab_environment_name' => 'production' + } + } + end + + it { is_expected.to eq(environment) } + end + end + + describe '#metrics_dashboard_url' do + subject { parsed_payload.metrics_dashboard_url } + + context 'without alert' do + it { is_expected.to be_nil } + end + + context 'with gitlab alert' do + include_context 'gitlab-managed prometheus alert attributes' do + let(:raw_payload) { payload } + end + + it { is_expected.to eq(dashboard_url_for_alert) } + end + end +end diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb new file mode 100644 index 00000000000..457db58a28b --- /dev/null +++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb @@ -0,0 +1,240 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do + let_it_be(:project) { create(:project) } + let(:raw_payload) { {} } + + let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) } + + it_behaves_like 'subclass has expected api' + + shared_context 'with environment' do + let_it_be(:environment) { create(:environment, project: project, name: 'production') } + end + + describe '#title' do + subject { parsed_payload.title } + + it_behaves_like 'parsable alert payload field', + 'annotations/title', + 'annotations/summary', + 'labels/alertname' + end + + describe '#description' do + subject { parsed_payload.description } + + it_behaves_like 'parsable alert payload field', 'annotations/description' + end + + describe '#annotations' do + subject { parsed_payload.annotations } + + it_behaves_like 'parsable alert payload field', 'annotations' + end + + describe '#status' do + subject { parsed_payload.status } + + it_behaves_like 'parsable alert payload field', 'status' + end + + describe '#starts_at' do + let(:current_time) { Time.current.utc } + + around do |example| + freeze_time { example.run } + end + + subject { parsed_payload.starts_at } + + context 'without payload' do + it { is_expected.to eq(current_time) } + end + + context "with startsAt" do + let(:value) { 10.minutes.ago.change(usec: 0).utc } + let(:raw_payload) { { 'startsAt' => value.rfc3339 } } + + it { is_expected.to eq(value) } + end + end + + describe '#ends_at' do + subject { parsed_payload.ends_at } + + context 'without payload' do + it { is_expected.to be_nil } + end + + context "with endsAt" do + let(:value) { Time.current.change(usec: 0).utc } + let(:raw_payload) { { 'endsAt' => value.rfc3339 } } + + it { is_expected.to eq(value) } + end + end + + describe '#generator_url' do + subject { parsed_payload.generator_url } + + it_behaves_like 'parsable alert payload field', 'generatorURL' + end + + describe '#runbook' do + subject { parsed_payload.runbook } + + it_behaves_like 'parsable alert payload field', 'annotations/runbook' + end + + describe '#alert_markdown' do + subject { parsed_payload.alert_markdown } + + it_behaves_like 'parsable alert payload field', 'annotations/gitlab_incident_markdown' + end + + describe '#environment_name' do + subject { parsed_payload.environment_name } + + it_behaves_like 'parsable alert payload field', 'labels/gitlab_environment_name' + end + + describe '#gitlab_y_label' do + subject { parsed_payload.gitlab_y_label } + + it_behaves_like 'parsable alert payload field', + 'annotations/gitlab_y_label', + 'annotations/title', + 'annotations/summary', + 'labels/alertname' + end + + describe '#monitoring_tool' do + subject { parsed_payload.monitoring_tool } + + it { is_expected.to eq('Prometheus') } + end + + describe '#full_query' do + using RSpec::Parameterized::TableSyntax + + subject { parsed_payload.full_query } + + where(:generator_url, :expected_query) do + nil | nil + 'http://localhost' | nil + 'invalid url' | nil + 'http://localhost:9090/graph?g1.expr=vector%281%29' | nil + 'http://localhost:9090/graph?g0.expr=vector%281%29' | 'vector(1)' + end + + with_them do + let(:raw_payload) { { 'generatorURL' => generator_url } } + + it { is_expected.to eq(expected_query) } + end + end + + describe '#environment' do + subject { parsed_payload.environment } + + it { is_expected.to be_nil } + + context 'with environment_name' do + let(:raw_payload) { { 'labels' => { 'gitlab_environment_name' => 'production' } } } + + it { is_expected.to be_nil } + + context 'with matching environment' do + include_context 'with environment' + + it { is_expected.to eq(environment) } + end + end + end + + describe '#gitlab_fingerprint' do + subject { parsed_payload.gitlab_fingerprint } + + let(:raw_payload) do + { + 'startsAt' => Time.current.to_s, + 'generatorURL' => 'http://localhost:9090/graph?g0.expr=vector%281%29', + 'annotations' => { 'title' => 'title' } + } + end + + it 'returns a fingerprint' do + plain_fingerprint = [ + parsed_payload.send(:starts_at_raw), + parsed_payload.title, + parsed_payload.full_query + ].join('/') + + is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint)) + end + end + + describe '#metrics_dashboard_url' do + include_context 'self-managed prometheus alert attributes' do + let(:raw_payload) { payload } + end + + subject { parsed_payload.metrics_dashboard_url } + + it { is_expected.to eq(dashboard_url_for_alert) } + + context 'without environment' do + let(:raw_payload) { payload.except('labels') } + + it { is_expected.to be_nil } + end + + context 'without full query' do + let(:raw_payload) { payload.except('generatorURL') } + + it { is_expected.to be_nil } + end + + context 'without title' do + let(:raw_payload) { payload.except('annotations') } + + it { is_expected.to be_nil } + end + end + + describe '#has_required_attributes?' do + let(:starts_at) { Time.current.change(usec: 0).utc } + let(:raw_payload) { { 'annotations' => { 'title' => 'title' }, 'startsAt' => starts_at.rfc3339 } } + + subject { parsed_payload.has_required_attributes? } + + it { is_expected.to be_truthy } + + context 'without project' do + let(:parsed_payload) { described_class.new(project: nil, payload: raw_payload) } + + it { is_expected.to be_falsey } + end + + context 'without title' do + let(:raw_payload) { { 'startsAt' => starts_at.rfc3339 } } + + it { is_expected.to be_falsey } + end + + context 'without startsAt' do + let(:raw_payload) { { 'annotations' => { 'title' => 'title' } } } + + it { is_expected.to be_falsey } + end + + context 'without payload' do + let(:parsed_payload) { described_class.new(project: project, payload: nil) } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/gitlab/alert_management/payload_spec.rb b/spec/lib/gitlab/alert_management/payload_spec.rb new file mode 100644 index 00000000000..44b55e228c5 --- /dev/null +++ b/spec/lib/gitlab/alert_management/payload_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::AlertManagement::Payload do + describe '#parse' do + let_it_be(:project) { build_stubbed(:project) } + let(:payload) { {} } + + context 'without a monitoring_tool specified by caller' do + subject { described_class.parse(project, payload) } + + context 'without a monitoring tool in the payload' do + it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic } + end + + context 'with the payload specifying Prometheus' do + let(:payload) { { 'monitoring_tool' => 'Prometheus' } } + + it { is_expected.to be_a Gitlab::AlertManagement::Payload::Prometheus } + + context 'with gitlab-managed attributes' do + let(:payload) { { 'monitoring_tool' => 'Prometheus', 'labels' => { 'gitlab_alert_id' => '12' } } } + + it { is_expected.to be_a Gitlab::AlertManagement::Payload::ManagedPrometheus } + end + end + + context 'with the payload specifying an unknown tool' do + let(:payload) { { 'monitoring_tool' => 'Custom Tool' } } + + it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic } + end + end + + context 'with monitoring_tool specified by caller' do + subject { described_class.parse(project, payload, monitoring_tool: monitoring_tool) } + + context 'as Prometheus' do + let(:monitoring_tool) { 'Prometheus' } + + context 'with an externally managed prometheus payload' do + it { is_expected.to be_a Gitlab::AlertManagement::Payload::Prometheus } + end + + context 'with a self-managed prometheus payload' do + let(:payload) { { 'labels' => { 'gitlab_alert_id' => '14' } } } + + it { is_expected.to be_a Gitlab::AlertManagement::Payload::ManagedPrometheus } + end + end + + context 'as an unknown tool' do + let(:monitoring_tool) { 'Custom Tool' } + + it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic } + end + end + end +end |