-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathcheck-jenkins-build-time.rb
executable file
·202 lines (172 loc) · 6.94 KB
/
check-jenkins-build-time.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/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