Skip to content

Commit 627c17c

Browse files
Merge pull request #28 from puppetlabs/SLV-653
(SLV-653) Add a script for generating system stats
2 parents 8607626 + c3a948a commit 627c17c

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,3 +694,6 @@ Style/YodaCondition:
694694
Enabled: false
695695
Style/ZeroLengthPredicate:
696696
Enabled: false
697+
GetText/DecorateString:
698+
Exclude:
699+
- 'files/generate_system_metrics'

files/generate_system_metrics

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#!/opt/puppetlabs/puppet/bin/ruby
2+
# frozen_string_literal: true
3+
4+
# rubocop::disable GetText/DecorateFunctionMessage
5+
6+
require "optparse"
7+
require "json"
8+
require "time"
9+
require "fileutils"
10+
11+
# This script is intended to be run on a puppet infrastructure node as part of the
12+
# puppet_metrics_collector module. It will generate system statistics in the file format that
13+
# other puppet_metrics_collector stats follow. Over a time period defined as the file interval,
14+
# it will poll the system for data based on the polling interval, then calculate an average from
15+
# the polling data to put in the file as a single data point at the end of the file interval.
16+
# sar_metric.pp will setup a cron job to run this similar to how pe_metric runs tk_metrics
17+
#
18+
# Example execution
19+
# generate_system_metrics --metric_type cpu --file_interval 300 --polling_interval 1
20+
# --metrics_dir /opt/puppetlabs/puppet-metrics-collector
21+
22+
# General namespace for SystemMetrics module
23+
module SystemMetrics
24+
# Main class for collecting system metrics into a json file
25+
#
26+
# @author Randell Pelak
27+
#
28+
# @attr [integer] polling_interval Time in seconds between calls to poll the system for data.
29+
# @attr [integer] file_interval Time in seconds between the creation of each output file.
30+
# @attr [string] metric_type cpu|memory
31+
# @attr [string] metrics_dir The puppet_metrics_collector output directory.
32+
# @attr [boolean] verbose Verbose output
33+
# @attr [string] hostname Name of the host the metrics are from. In directory name and json file.
34+
#
35+
class GenerateSystemMetrics
36+
#
37+
# Initialize class
38+
#
39+
# @author Randell Pelak
40+
#
41+
# @param [integer] polling_interval Time in seconds between calls to poll the system for data.
42+
# @param [integer] file_interval Time in seconds between the creation of each output file.
43+
# @param [string] metric_type cpu|memory
44+
# @param [string] metrics_dir The puppet_metrics_collector output directory.
45+
# @param [boolean] verbose Verbose output
46+
#
47+
# @return [void]
48+
def initialize(polling_interval, file_interval, metric_type, metrics_dir, verbose = false)
49+
@polling_interval = polling_interval
50+
@file_interval = file_interval
51+
@metric_type = metric_type
52+
@metrics_dir = metrics_dir
53+
@verbose = verbose
54+
55+
@hostname = %x[hostname].strip
56+
puts "Hostname is: #{@hostname}" if @verbose
57+
FileUtils.mkdir_p(@metrics_dir) unless File.directory?(@metrics_dir)
58+
end
59+
60+
# Run sar to collect the raw system data
61+
#
62+
# @author Randell Pelak
63+
#
64+
# @return [string] raw output from sar
65+
def run_sar
66+
times_to_poll = (@file_interval / @polling_interval).round
67+
# sar inputs are polling interval and how many times to poll
68+
comm_flags = " -r" if @metric_type =~ /memory/
69+
comm = "sar #{comm_flags} #{@polling_interval} #{times_to_poll}"
70+
puts "sar command is: #{comm}" if @verbose
71+
%x[#{comm}]
72+
end
73+
74+
# Parse the sar output and extract the metrics data
75+
#
76+
# @author Randell Pelak
77+
#
78+
# @param [array] sar_output
79+
#
80+
# @raise [RuntimeError] if sar_output doesn't parse correctly
81+
#
82+
# @return [hash] The metrics data
83+
def parse_sar_output(sar_output)
84+
sar_output_arr = sar_output.split(/\n+|\r+/).reject(&:empty?).map { |line| line.split }
85+
86+
if ( @metric_type == "memory")
87+
unique_header_str = "%memused"
88+
else
89+
unique_header_str = "%user"
90+
end
91+
headers_line = sar_output_arr.find { |e| e.include? unique_header_str }
92+
93+
sar_error_missing_headers = "sar output invalid or missing headers." +
94+
"failed to find line with #{unique_header_str}."
95+
"\nFull output:\n#{sar_output}"
96+
raise(sar_error_missing_headers) if headers_line.nil?
97+
98+
averages_line = sar_output_arr.find { |e| e.include? "Average:" }
99+
sar_error_missing_averages = "sar output missing \"Average:\"\nFull output:\n#{sar_output}"
100+
raise(sar_error_missing_averages) if averages_line.nil?
101+
102+
Hash[headers_line.reverse.zip(averages_line.reverse).reverse]
103+
104+
puts "sar headers and averages:\n#{headers_line.join(",")}\n#{averages_line.join(",")}" if @verbose
105+
106+
#example of array data
107+
# 04:59:13,PM,CPU,%user,%nice,%system,%iowait,%steal,%idle
108+
# Average:,all,0.58,0.00,0.08,0.00,0.00,99.33
109+
#combine the arrays into a hash starting from the deal with the unmatched columns in the front
110+
data_hash = Hash[headers_line.reverse.zip(averages_line.reverse).reverse]
111+
# remove anything that doesn't have a number for an average like "Average:" or "all"
112+
data_hash.select{ |k,v| v =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/ }
113+
end
114+
115+
# Convert the inputted sar output into json and raise errors if the sar output is invalid
116+
#
117+
# @author Randell Pelak
118+
#
119+
# @param [string] sar_output
120+
# @param [obj] time_stamp_obj Time object to use for generating the filename
121+
#
122+
# @raise [RuntimeError] if sar_output doesn't parse correctly
123+
#
124+
# @return [string] json of sar output
125+
def convert_sar_output_to_json(sar_output, time_stamp_obj)
126+
hostkey = @hostname.gsub('.', '-')
127+
dataset = {'time_stamp_obj' => time_stamp_obj.utc.iso8601, 'servers' => {}}
128+
metrics_data = parse_sar_output(sar_output)
129+
dataset['servers'][hostkey] = {@metric_type => metrics_data}
130+
json_dataset = JSON.pretty_generate(dataset)
131+
return json_dataset
132+
end
133+
134+
# Create the file and put the json data in it
135+
#
136+
# @author Randell Pelak
137+
#
138+
# @param [string] json_dataset data in json format to put in file
139+
# @param [obj] time_stamp_obj Time object to use for generating the filename
140+
#
141+
# @return [void]
142+
def create_file(json_dataset, time_stamp_obj)
143+
filename = time_stamp_obj.utc.strftime('%Y%m%dT%H%M%SZ') + '.json'
144+
dirname = "#{@metrics_dir}/system_#{@metric_type}/#{@hostname}"
145+
file_path = "#{dirname}/#{filename}"
146+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
147+
puts "Creating json file: #{file_path}" if @verbose
148+
File.write(file_path, json_dataset)
149+
end
150+
151+
# Get the data and create the file
152+
#
153+
# @author Randell Pelak
154+
#
155+
# @return [void]
156+
def generate_system_metrics
157+
sar_output = run_sar
158+
# time stamp generated after sar run to be consistent with
159+
# pe metrics from puppet metrics collector
160+
time_stamp_obj = Time.now
161+
json_data = convert_sar_output_to_json(sar_output, time_stamp_obj)
162+
create_file(json_data, time_stamp_obj)
163+
end
164+
end
165+
end
166+
167+
if $PROGRAM_NAME == __FILE__
168+
169+
VALID_METRIC_TYPES = %w[cpu memory]
170+
FILE_INTERVAL_DEFAULT = 60 * 5
171+
POLLING_INTERVAL_DEFAULT = 1
172+
METRIC_TYPE_DEFAULT = "cpu"
173+
METRICS_DIR_DEFAULT = "/opt/puppetlabs/puppet-metrics-collector"
174+
175+
DESCRIPTION = <<-DESCRIPTION
176+
This script is intended to be run on a puppet infrastructure node as part of the
177+
puppet_metrics_collector module. It will generate system statistics in the file format that
178+
other puppet_metrics_collector stats follow. It will poll the system for data at a given
179+
interval, and then output the average to a file once per given file interval.
180+
DESCRIPTION
181+
182+
DEFAULTS = <<-DEFAULTS
183+
The following default values are used if the options are not specified:
184+
* polling_interval (-p, --polling_interval): #{POLLING_INTERVAL_DEFAULT}
185+
* file_interval (-f, --file_interval): #{FILE_INTERVAL_DEFAULT}
186+
* metric_type (-t, --metric_type): #{METRIC_TYPE_DEFAULT}
187+
* metrics_dir (-m, --metrics_dir): #{METRICS_DIR_DEFAULT}
188+
* verbose (-v, --verbose): False
189+
DEFAULTS
190+
191+
options = {}
192+
193+
OptionParser.new do |opts|
194+
opts.banner = "Usage: generate_system_stats.rb [options]"
195+
196+
opts.on("-h", "--help", "Display the help text") do
197+
puts DESCRIPTION
198+
puts opts
199+
puts DEFAULTS
200+
exit
201+
end
202+
203+
opts.on("-p", "--polling_interval seconds", Integer,
204+
"Time in seconds between calls to poll the system for data.") do |interval|
205+
options[:polling_interval] = interval
206+
end
207+
opts.on("-f", "--file_interval seconds", Integer,
208+
"Time in seconds between the creation of each output file.") do |interval|
209+
options[:file_interval] = interval
210+
end
211+
opts.on("-t", "--metric_type type", String,
212+
"One of: #{VALID_METRIC_TYPES.join(', ')}") do |type|
213+
options[:metric_type] = type.downcase
214+
end
215+
opts.on("-m", "--metrics_dir dir_path", String,
216+
"The puppet_metrics_collector output directory") do |metrics_dir|
217+
options[:metrics_dir] = metrics_dir
218+
end
219+
opts.on("-v", "--verbose", String, "Enable Verbose output") { options[:verbose] = true }
220+
end.parse!
221+
222+
if options[:metric_type]
223+
unless VALID_METRIC_TYPES.include?(options[:metric_type])
224+
options_error = "Invalid metric type #{options[:metric_type]}." +
225+
" Must be one of: #{VALID_METRIC_TYPES.join(', ')}."
226+
raise options_error
227+
end
228+
end
229+
230+
polling_interval = options[:polling_interval] || POLLING_INTERVAL_DEFAULT
231+
file_interval = options[:file_interval] || FILE_INTERVAL_DEFAULT
232+
metric_type = options[:metric_type] || METRIC_TYPE_DEFAULT
233+
metrics_dir = options[:metrics_dir] || METRICS_DIR_DEFAULT
234+
verbose = options[:verbose] || false
235+
236+
if options[:polling_interval] || options[:file_interval]
237+
options_error = "Polling interval must be less than file interval"
238+
raise options_error unless polling_interval < file_interval
239+
end
240+
241+
if verbose
242+
OPTION_SETTINGS = <<-SETTINGS
243+
The following are the resulting options settings:
244+
* polling_interval: #{polling_interval}
245+
* file_interval: #{file_interval}
246+
* metric_type: #{metric_type}
247+
* metrics_dir: #{metrics_dir}
248+
* verbose: #{verbose}
249+
SETTINGS
250+
puts OPTION_SETTINGS
251+
end
252+
253+
obj = SystemMetrics::GenerateSystemMetrics.new(polling_interval, file_interval, metric_type,
254+
metrics_dir, verbose)
255+
obj.generate_system_metrics
256+
end

0 commit comments

Comments
 (0)