Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
executable file 202 lines (172 sloc) 6.94 KB
#!/usr/bin/env ruby
#
# check-jenkins-build-time
#
# DESCRIPTION:
# Alert if the last successful build timestamp of a jenkins job is older than
# a specified time duration
# OR not within a specific daily time window
# OR if the total build duration exceeds a specified duration.
#
# OUTPUT:
# plain text
#
# PLATFORMS:
# Linux
#
# USAGE:
# check-jenkins-build-time -u JENKINS_URL -j job_1_name=30min,job_2_name=1:30am-2:30am
#
# -j parameter should be a comma-separated list of JOB_NAME=TIME_EXPRESSION
# where TIME_EXPRESSION is either a relative time duration from now (30m, 1h) or
# a daily time window (1am-2am, 1:01am-2:01am), without spaces.
#
# --check-build-duration to check the last build duration for a job.
#
# DEPENDENCIES:
# gem: sensu-plugin
# jenkins_api_client
# chronic_duration
#
#
# LICENSE:
# Copyright Matt Greensmith mgreensmith@cozy.co matt@mattgreensmith.net
# Released under the same terms as Sensu (the MIT license); see LICENSE
# for details.
#
require 'sensu-plugin/check/cli'
require 'jenkins_api_client'
require 'chronic_duration'
class JenkinsBuildTime < Sensu::Plugin::Check::CLI
ONE_DAY = 86_400
option :url,
description: 'URL to Jenkins API',
short: '-u JENKINS_URL',
long: '--url JENKINS_URL',
required: true
option :jobs,
description: 'Jobs to check. Comma-separated list of JOB_NAME=TIME_EXPRESSION',
short: '-j JOB_NAME=TIME_EXPRESSION,[JOB_NAME=TIME_EXPRESSION]',
long: '--jobs JOB_NAME=TIME_EXPRESSION,[JOB_NAME=TIME_EXPRESSION]',
required: true
option :check_build_duration,
description: 'Mode to check the build duration instead of the last occurence of a build',
short: '-d',
long: '--[no-]check-build-duration'
option :username,
description: 'Username for Jenkins instance',
short: '-U USERNAME',
long: '--username USERNAME',
required: false
option :password,
description: "Password for Jenkins instance. Either set ENV['JENKINS_PASS'] or provide it as an option",
short: '-p PASSWORD',
long: '--password PASSWORD',
required: false,
default: ENV['JENKINS_PASS']
def run
@now = Time.now
critical_jobs = []
jobs = parse_jobs_param
check_build_duration = config[:check_build_duration] || false
jobs.each do |job_name, time_expression|
begin
build_number = last_successful_build_number(job_name)
last_build_time = build_time(job_name, build_number)
last_build_duration = build_duration(job_name, build_number)
rescue
critical "Error looking up Jenkins job: #{job_name}"
end
if check_build_duration
unless time_within_allowed_build_duration?(last_build_duration,
parse_duration_seconds(time_expression))
critical_jobs << critical_message_build_duration(job_name, last_build_time, last_build_duration, time_expression)
end
elsif time_expression_is_window?(time_expression)
unless time_within_window?(last_build_time,
parse_window_start(time_expression),
parse_window_end(time_expression))
critical_jobs << critical_message(job_name, last_build_time, last_build_duration, time_expression)
end
else
unless time_within_allowed_duration?(last_build_time,
parse_duration_seconds(time_expression))
critical_jobs << critical_message(job_name, last_build_time, last_build_duration, time_expression)
end
end
end
critical "#{critical_jobs.length} failure(s): #{critical_jobs.join(', ')}" unless critical_jobs.empty?
ok "#{jobs.keys.length} job(s) had successful builds within allowed times"
end
private
def jenkins
@jenkins ||= JenkinsApi::Client.new(server_url: config[:url], username: config[:username], password: config[:password], log_level: 3)
end
def last_successful_build_number(job_name)
jenkins.job.list_details(job_name)['lastSuccessfulBuild']['number']
end
def build_details(job_name, build_number)
# Cache the results
@build_details ||= {}
@build_details[job_name] ||= {}
@build_details[job_name][build_number] ||= jenkins.job.get_build_details(job_name, build_number)
end
def build_time(job_name, build_number)
# Jenkins expresses timestamps in epoch millis
Time.at(build_details(job_name, build_number)['timestamp'] / 1000)
end
def build_duration(job_name, build_number)
# Jenkins expresses timestamps in epoch millis, convert it to seconds
build_details(job_name, build_number)['duration'] / 1000
end
def time_expression_is_window?(time_expression)
time_expression.index('-') ? true : false
end
def parse_window_start(time_expression)
Time.parse(time_expression.split('-')[0])
end
def parse_window_end(time_expression)
Time.parse(time_expression.split('-')[1])
end
def time_within_window?(time, window_start, window_end)
time_after_window_start_today = window_start < time
time_in_window_today = time_after_window_start_today && window_end > time
time_after_window_start_yesterday = (window_start - ONE_DAY) < time
time_in_window_yesterday = time_after_window_start_yesterday && (window_end - ONE_DAY) > time
time_ok = if @now < window_end && @now > window_start # we are in the window, so we will accept today or yesterday
(time_in_window_today || time_in_window_yesterday)
elsif @now < window_end
# we are before the window, so we only accept yesterday
time_in_window_yesterday
else
# we are after the window, so we only accept today
time_in_window_today
end
time_ok
end
def parse_duration_seconds(time_expression)
ChronicDuration.parse(time_expression)
end
def time_within_allowed_duration?(time, duration_seconds)
time > (@now - duration_seconds)
end
def time_within_allowed_build_duration?(build_duration_seconds, duration_seconds)
build_duration_seconds <= duration_seconds
end
def critical_message_build_duration(job_name, last_build_time, duration_seconds, time_expression)
"#{job_name}: last built at #{last_build_time} (#{ChronicDuration.output(duration_seconds)}) exceeded max duration (#{time_expression})"
end
def critical_message(job_name, last_build_time, duration_seconds, time_expression)
"#{job_name}: last built at #{last_build_time} (#{ChronicDuration.output(duration_seconds)}), not within allowed time: #{time_expression}"
end
def parse_jobs_param
jobs = {}
job_param = config[:jobs].split(',')
job_param.each do |j|
name, time_expression = j.split('=')
raise "Jobs mut be expressed as JOB_NAME=TIME_EXPRESSION. Invalid parameter: '#{j}'" if time_expression.nil?
jobs[name] = time_expression
end
jobs
end
end