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