spec/lib/release_tools/pipeline_tracer/metrics_service_spec.rb (308 lines of code) (raw):
# frozen_string_literal: true
require 'spec_helper'
describe ReleaseTools::PipelineTracer::MetricsService do
describe '.from_pipeline_url' do
it 'creates Service instance' do
url = 'https://ops.gitlab.net/gitlab-org/release/tools/-/pipelines/1590941'
instance = described_class.from_pipeline_url(
url,
version: 'pipeline 1',
depth: 2,
upstream_pipeline_name: 'Test pipeline'
)
pipeline = instance.instance_variable_get(:@pipeline)
version = instance.instance_variable_get(:@version)
depth = instance.instance_variable_get(:@depth)
upstream_pipeline_name = instance.instance_variable_get(:@upstream_pipeline_name)
expect(pipeline.gitlab_instance).to eq('ops.gitlab.net')
expect(pipeline.id).to eq('1590941')
expect(pipeline.project).to eq('gitlab-org/release/tools')
expect(version).to eq('pipeline 1')
expect(depth).to eq(2)
expect(upstream_pipeline_name).to eq('Test pipeline')
end
end
describe '#execute' do
subject(:instance) do
described_class.new(
pipeline: pipeline,
version: version,
depth: depth,
upstream_pipeline_name: upstream_pipeline_name
)
end
let(:gitlab_instance) { 'ops.gitlab.net' }
let(:project) { 'gitlab-org/release/tools' }
let(:pipeline_id) { '1590941' }
let(:gitlab_client) do
case gitlab_instance
when 'ops.gitlab.net'
ReleaseTools::GitlabOpsClient
when 'dev.gitlab.org'
ReleaseTools::GitlabDevClient
else
ReleaseTools::GitlabClient
end
end
let(:pipeline) do
ReleaseTools::PipelineTracer::Pipeline.new(gitlab_instance.to_s, project.to_s, pipeline_id.to_s)
end
let(:upstream_pipeline_name) { 'Upstream pipeline' }
let(:depth) { 2 }
let(:version) { '15.10.202303221120-355b04fb8d1.936857331ed' }
let(:metrics_client) { instance_double(ReleaseTools::Metrics::Client) }
let(:current_pipeline_name) { 'Deployment pipeline - 15.10.202303221120' }
let(:job1) { ReleaseTools::PipelineTracer::Job.new(jobs[0], gitlab_client) }
let(:pipeline_duration_labels) do
"#{project},#{version},#{pipeline_details.status},#{current_pipeline_name},#{pipeline.id},#{pipeline_details.web_url},gstg,cny,#{upstream_pipeline_name}"
end
let(:pipeline_duration_histogram_labels) do
pipeline_details.status.to_s
end
let(:pipeline_details) do
build(
:pipeline,
:success,
id: pipeline.id,
name: current_pipeline_name,
web_url: "https://#{gitlab_instance}/#{project}/-/pipelines/#{pipeline.id}"
)
end
let(:job_duration_labels) do
"job1,stage1,success,#{project},#{version},,,job1,https://example.com/foo/bar/-/jobs/#{job.id},#{job.id},#{pipeline.id},#{pipeline.details.name}"
end
let(:jobs) do
[
build(
:job, :success, name: 'job1'
),
build(
:job, :running, name: 'job2'
)
]
end
let(:bridge_jobs) { [build(:bridge_job, :success, name: 'bridge_job1')] }
let(:job) do
ReleaseTools::PipelineTracer::Job.new(jobs[0], gitlab_client)
end
before do
allow(gitlab_client)
.to receive_messages(pipeline: pipeline_details, pipeline_variables: [build(:gitlab_response, key: 'DEPLOY_ENVIRONMENT', value: 'gstg-cny')])
allow(ReleaseTools::PipelineTracer::Job).to receive(:new).and_call_original
allow(ReleaseTools::PipelineTracer::Job).to receive(:new).with(jobs[0], gitlab_client).and_return(job)
allow(job).to receive(:real_time_duration).and_return(300)
allow(ReleaseTools::Metrics::Client).to receive(:new).and_return(metrics_client)
allow(pipeline)
.to receive_messages(real_time_duration: 200, jobs: Gitlab::PaginatedResponse.new(jobs), bridge_jobs: Gitlab::PaginatedResponse.new(bridge_jobs))
allow(described_class)
.to receive(:from_pipeline_url)
.with(
bridge_jobs.first.downstream_pipeline.web_url,
{
version: version,
depth: depth - 1,
upstream_pipeline_name: pipeline.details.name
}
)
.and_return(double(execute: nil))
allow(metrics_client).to receive(:set)
allow(metrics_client).to receive(:observe)
allow(ReleaseTools::PipelineTracer::Job)
.to receive(:new)
.with(jobs[0], gitlab_client)
.and_return(job1)
allow(job1).to receive(:triggered_downstream_pipeline?).and_return(false)
end
it 'creates metrics for pipeline' do
expect(metrics_client)
.to receive(:set)
.with('deployment_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_labels })
expect(metrics_client)
.to receive(:observe)
.with('deployment_coordinator_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_histogram_labels })
without_dry_run { instance.execute }
end
it 'creates metric for jobs' do
expect(metrics_client)
.to receive(:set)
.with('deployment_job_duration_seconds', job.real_time_duration, { labels: job_duration_labels })
expect(metrics_client)
.not_to receive(:set)
.with('deployment_job_duration_seconds', anything, { labels: include("job2") })
without_dry_run { instance.execute }
end
it 'creates metric for bridge jobs' do
expect(described_class)
.to receive(:from_pipeline_url)
.with(
bridge_jobs[0].downstream_pipeline.web_url,
{
version: version,
depth: 1,
upstream_pipeline_name: pipeline.details.name
}
)
.and_return(double(execute: nil))
without_dry_run { instance.execute }
end
it 'does not call triggered_pipeline_url if triggered_downstream_pipeline? is false' do
expect(job1).to receive(:triggered_downstream_pipeline?)
expect(job1).not_to receive(:triggered_pipeline_url)
expect(described_class)
.not_to receive(:from_pipeline_url)
.with('https://url.local', { version: version, depth: depth - 1, upstream_pipeline_name: pipeline.details.name })
without_dry_run { instance.execute }
end
context 'with no end_time' do
before do
allow(pipeline).to receive(:end_time).and_return(nil)
end
it 'does not generate duration metrics' do
expect(metrics_client).not_to receive(:set)
expect(metrics_client).not_to receive(:observe)
without_dry_run { instance.execute }
end
end
context 'with environment in name' do
let(:job_duration_labels) do
"#{job.name},stage1,success,#{project},#{version},gstg,cny,metrics:deployment_completed:,https://example.com/foo/bar/-/jobs/#{job.id},#{job.id},#{pipeline.id},#{pipeline.details.name}"
end
let(:jobs) do
[build(:job, :success, name: 'metrics:deployment_completed:gstg-cny'), build(:job, :running, name: 'job2')]
end
it 'creates metric for jobs' do
expect(metrics_client)
.to receive(:set)
.with('deployment_job_duration_seconds', job.real_time_duration, { labels: job_duration_labels })
without_dry_run { instance.execute }
end
end
context 'when depth is invalid' do
let(:depth) { 4 }
it 'raises error' do
expect { instance.execute }.to raise_error(described_class::InvalidDepthError, 'Depth must be between 0 and 3')
end
end
context 'when depth is 0' do
subject(:instance) do
described_class.new(pipeline: pipeline, version: version, depth: 0, upstream_pipeline_name: upstream_pipeline_name)
end
it 'does not create job duration metric' do
expect(metrics_client)
.to receive(:set)
.with('deployment_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_labels })
expect(metrics_client).not_to receive(:set).with('deployment_job_duration_seconds', any_args)
expect(described_class).not_to receive(:from_pipeline_url)
without_dry_run { instance.execute }
end
end
context 'triggered job' do
before do
allow(job1).to receive_messages(triggered_downstream_pipeline?: true, triggered_pipeline_url: 'https://url.local')
end
it 'executes MetricsService' do
expect(job1).to receive(:triggered_downstream_pipeline?)
expect(job1).to receive(:triggered_pipeline_url)
expect(described_class)
.to receive(:from_pipeline_url)
.with(
'https://url.local',
{
version: version,
depth: depth - 1,
upstream_pipeline_name: pipeline.details.name
}
)
.and_return(instance_spy(described_class, execute: nil))
without_dry_run { instance.execute }
end
context 'with nil pipeline_name' do
let(:current_pipeline_name) { nil }
it 'calls from_pipeline_url with upstream_pipeline_name set to nil' do
expect(job1).to receive(:triggered_downstream_pipeline?)
expect(job1).to receive(:triggered_pipeline_url)
expect(described_class)
.to receive(:from_pipeline_url)
.with(
'https://url.local',
{
version: version,
depth: depth - 1,
upstream_pipeline_name: nil
}
)
.and_return(instance_spy(described_class, execute: nil))
without_dry_run { instance.execute }
end
end
end
context 'with nil upstream_pipeline_name' do
let(:upstream_pipeline_name) { nil }
it 'sets upstream_pipeline_name label to blank string' do
expect(metrics_client)
.to receive(:set)
.with('deployment_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_labels })
expect(metrics_client)
.not_to receive(:observe)
.with('deployer_pipeline_duration_histogram_seconds', pipeline.real_time_duration, { labels: pipeline_duration_histogram_labels })
without_dry_run { instance.execute }
end
end
context 'with dry run' do
it 'does nothing' do
expect(metrics_client).not_to receive(:set)
expect(metrics_client).not_to receive(:observe)
instance.execute
end
end
context 'when quality pipeline is traced' do
let(:project) { 'gitlab-org/quality/canary' }
let(:current_pipeline_name) { 'Deployment QA pipeline - 17.11.202503252206-0b36d04e4a9.6ba20f9b71c' }
it 'does not submit histogram metric' do
expect(metrics_client)
.to receive(:set)
.with('deployment_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_labels })
expect(metrics_client)
.not_to receive(:observe)
without_dry_run { instance.execute }
end
end
context 'when Omnibus pipeline is traced' do
let(:gitlab_instance) { 'dev.gitlab.org' }
let(:project) { 'gitlab/omnibus-gitlab' }
let(:upstream_pipeline_name) { 'Deployment pipeline - 17.11.202503252206' }
let(:current_pipeline_name) { 'AUTO_DEPLOY_BUILD_PIPELINE' }
let(:pipeline_status) { 'success' }
it 'submits histogram metrics' do
expect(metrics_client)
.to receive(:set)
.with('deployment_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_labels })
expect(metrics_client)
.not_to receive(:observe)
.with('deployment_coordinator_pipeline_duration_seconds', anything, anything)
expect(metrics_client)
.to receive(:observe)
.with('deployment_packager_omnibus_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_histogram_labels })
without_dry_run { instance.execute }
end
end
context 'when CNG pipeline is traced' do
let(:gitlab_instance) { 'dev.gitlab.org' }
let(:upstream_pipeline_name) { 'Deployment pipeline - 17.11.202503252206' }
let(:current_pipeline_name) { 'AUTO_DEPLOY_BUILD_PIPELINE' }
let(:project) { 'gitlab/charts/components/images' }
let(:pipeline_status) { 'running' }
it 'submits histogram metrics' do
expect(metrics_client)
.to receive(:set)
.with('deployment_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_labels })
expect(metrics_client)
.not_to receive(:observe)
.with('deployment_coordinator_pipeline_duration_seconds', anything, anything)
expect(metrics_client)
.to receive(:observe)
.with('deployment_packager_cng_pipeline_duration_seconds', pipeline.real_time_duration, { labels: pipeline_duration_histogram_labels })
without_dry_run { instance.execute }
end
end
end
end