dstat_plot 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/dstat-plot +4 -0
  3. data/lib/dstat_plot.rb +328 -0
  4. metadata +60 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2bb90a5fad4eb9f82a3e6186125642b257e0b08b
4
+ data.tar.gz: 6f1bca5b9ce0e72119d2ba23ec6201f0d438d472
5
+ SHA512:
6
+ metadata.gz: c7b09179a626d3228c2f6e6750450b30ee7b190ce32b1b19ea89765ac85c0d6b336f68a9ccd35616cccff6837f4bc0621f21f7506eee69f6a9aaaac56892bf24
7
+ data.tar.gz: 4b59a65a81082ca5516b765c020fb806c9760cee8f65bef4e092c1eb1330adb3c96ec4b75e74050711ceabb8843d549e932b572d59cd8142b6594443eb568946
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dstat_plot'
4
+ DstatPlot.run
@@ -0,0 +1,328 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require 'gnuplot'
4
+ require 'csv'
5
+ require 'optparse'
6
+
7
+ """
8
+ dstat_plot
9
+ plots csv data generated by dstat
10
+ """
11
+
12
+ $verbose = false
13
+ Y_DEFAULT = 105.0
14
+
15
+ def plot(dataset_container, category, field, dry, filename)
16
+ Gnuplot.open do |gp|
17
+ Gnuplot::Plot.new(gp) do |plot|
18
+ plot.title dataset_container[:plot_title].gsub('_', '\\\\\\\\_')
19
+ plot.xlabel "Time in seconds"
20
+ plot.ylabel "#{category}: #{field}"
21
+ plot.yrange "[0:#{dataset_container[:y_max] * 1.05}]"
22
+ if dataset_container[:autoscale] then plot.set "autoscale" end
23
+ plot.key "out vert right top"
24
+
25
+ unless dry
26
+ format = filename.split('.')[-1]
27
+ plot.terminal format + ' size 1600,800 enhanced font "Helvetica,11"'
28
+ plot.output filename
29
+ puts "Saving plot to '#{filename}'"
30
+ end
31
+
32
+ plot.data = dataset_container[:datasets]
33
+ end
34
+ end
35
+ end
36
+
37
+ def generate_filename(output, column, category, field, target_dir)
38
+ if output.nil? || File.directory?(output) # if an output file is not explicitly stated or if it's a directory
39
+ # generate filename
40
+ if column
41
+ generated_filename = "dstat-column#{column}.png"
42
+ else
43
+ generated_filename = "#{category}-#{field}.png".sub("/", "_")
44
+ end
45
+
46
+ # add directory portion
47
+ if output.nil?
48
+ filename = File.join(target_dir, generated_filename)
49
+ elsif File.directory?(output)
50
+ filename = File.join(output, generated_filename)
51
+ end
52
+ else # specific path+file is given so just use that
53
+ filename = output
54
+ end
55
+ end
56
+
57
+ # Calculate the average of groups of values from data
58
+ # Params:
59
+ # +data:: Array containing the data
60
+ # +slice_size:: number of values each group of data should contain
61
+ def average(data, slice_size)
62
+ reduced_data = []
63
+ data.each_slice(slice_size) do |slice|
64
+ reduced_data.push(slice.reduce(:+) / slice.size)
65
+ end
66
+ reduced_data
67
+ end
68
+
69
+ # Preprocesses the data contained in all datasets in the dataset_container
70
+ # Groups of values are averaged with respect to timecode and actual data
71
+ # Params:
72
+ # +dataset_container:: Hash that holds the datasets and further information
73
+ # +slice_size:: size of the group that averages are supposed to be calculated from
74
+ def data_preprocessing(dataset_container, slice_size)
75
+ dataset_container[:datasets].each do |dataset|
76
+ timecode = dataset.data[0]
77
+ reduced_timecode = average(timecode, slice_size)
78
+
79
+ values = dataset.data[1].map { |value| value.to_f }
80
+ reduced_values = average(values, slice_size)
81
+
82
+ dataset.data = [reduced_timecode, reduced_values]
83
+ end
84
+ end
85
+
86
+ # Create the GnuplotDataSet that is going to be printed.
87
+ # Params:
88
+ # +timecode:: Array containing the timestamps
89
+ # +values:: Array containing the actual values
90
+ # +no_plot_key:: boolean to de-/activate plotkey
91
+ # +smooth:: nil or smoothing algorithm
92
+ # +file:: file
93
+ def create_gnuplot_dataset(timecode, values, no_plot_key, smooth, file)
94
+ Gnuplot::DataSet.new([timecode, values]) do |gp_dataset|
95
+ gp_dataset.with = "lines"
96
+ if no_plot_key then
97
+ gp_dataset.notitle
98
+ else
99
+ gp_dataset.title = (File.basename file).gsub('_', '\\_')
100
+ end
101
+ gp_dataset.smooth = smooth unless smooth.nil?
102
+ end
103
+ end
104
+
105
+ def analyze_header_create_plot_title(prefix, smooth, inversion, csv_header)
106
+ plot_title = "#{prefix} over time"
107
+ if smooth then plot_title += " (smoothing: #{smooth})" end
108
+ if csv_header[2].index("Host:")
109
+ plot_title += '\n' + "(Host: #{csv_header[2][1]} User: #{csv_header[2][6]} Date: #{csv_header[3].last})"
110
+ end
111
+ if inversion then plot_title += '\n(inverted)' end
112
+ plot_title
113
+ end
114
+
115
+ def translate_to_column(category, field, csv)
116
+ category_index = csv[5].index category
117
+ if category_index.nil?
118
+ puts "'#{category}' is not a valid parameter for 'category'."
119
+ puts "Allowed categories: #{csv[5].reject{ |elem| elem == nil }.inspect}"
120
+ exit 0
121
+ end
122
+
123
+ field_index = csv[6].drop(category_index).index field
124
+ if field_index.nil?
125
+ puts "'#{field}' is not a valid parameter for 'field'."
126
+ puts "Allowed fields: #{csv[6].reject{ |elem| elem == nil }.inspect}"
127
+ exit 0
128
+ end
129
+
130
+ if $verbose then puts "'#{category}-#{field}' was translated to #{category_index + field_index}." end
131
+ column = category_index + field_index
132
+ end
133
+
134
+ # returns the values from a headerless csv file
135
+ def read_data_from_csv(files, category, field, column, no_plot_key, y_max, inversion, title, smooth)
136
+ plot_title = nil
137
+ datasets = []
138
+ autoscale = false
139
+ overall_max = y_max.nil? ? Y_DEFAULT : y_max
140
+
141
+ files.each do |file|
142
+ csv = CSV.read(file)
143
+
144
+ if column
145
+ if $verbose then puts "Reading from csv to get column #{column}." end
146
+ prefix = "dstat-column #{column}"
147
+ else
148
+ if $verbose then puts "Reading from csv to get #{category}-#{field}." end
149
+ column = translate_to_column(category, field, csv)
150
+ prefix = "#{category}-#{field}"
151
+ end
152
+
153
+ if plot_title.nil? # this only needs to be done for the first file
154
+ if title
155
+ plot_title = title
156
+ else
157
+ plot_title = analyze_header_create_plot_title(prefix, smooth, inversion != 0.0, csv[0..6])
158
+ end
159
+ end
160
+
161
+ if csv[2].index "Host:"
162
+ csv = csv.drop(7)
163
+ end
164
+
165
+ begin
166
+ csv = csv.transpose
167
+ rescue IndexError => e
168
+ puts 'ERROR: It appears that your csv file is malformed. Check for incomplete lines, empty lines etc.'
169
+ puts e.backtrace[0] + e.message
170
+ exit
171
+ end
172
+
173
+ timecode = csv[0].map { |timestamp| timestamp.to_f - csv[0].first.to_f }
174
+
175
+ values = csv[column]
176
+ if inversion != 0.0
177
+ values.map! { |value| (value.to_f - inversion).abs }
178
+ overall_max = inversion
179
+ end
180
+
181
+ if y_max.nil?
182
+ local_maximum = values.max { |a, b| a.to_f <=> b.to_f }.to_f
183
+ if local_maximum > overall_max then overall_max = local_maximum end
184
+ end
185
+
186
+ dataset = create_gnuplot_dataset(timecode, values, no_plot_key, smooth, file)
187
+ datasets.push dataset
188
+ end
189
+
190
+ if $verbose then puts "datasets: #{datasets.count} \nplot_title: #{plot_title} \ny_max: #{y_max} \nautoscale: #{autoscale}" end
191
+ dataset_container = { :datasets => datasets, :plot_title => plot_title, :y_max => overall_max, :autoscale => autoscale }
192
+ end
193
+
194
+ def read_options_and_arguments
195
+ opts = {} # Hash that holds all the options
196
+
197
+ optparse = OptionParser.new do |parser|
198
+ # banner that is displayed at the top
199
+ parser.banner = "Usage: \b
200
+ dstat_plot.rb [options] -c CATEGORY -f FIELD [directory | file1 file2 ...] or \b
201
+ dstat_plot.rb [options] -l COLUMN [directory | file1 file2 ...]\n\n"
202
+
203
+ ### options and what they do
204
+ parser.on('-v', '--verbose', 'Output more information') do
205
+ $verbose = true
206
+ end
207
+
208
+ opts[:inversion] = 0.0
209
+ parser.on('-i', '--invert [VALUE]', Float, 'Invert the graph such that inverted(x) = VALUE - f(x),', 'default is 100.') do |value|
210
+ opts[:inversion] = value.nil? ? 100.0 : value
211
+ end
212
+
213
+ opts[:no_plot_key] = false
214
+ parser.on('-n', '--no-key', 'No plot key is printed.') do
215
+ opts[:no_plot_key] = true
216
+ end
217
+
218
+ opts[:dry] = false
219
+ parser.on('-d', '--dry', 'Dry run. Plot is not saved to file but instead displayed with gnuplot.') do
220
+ opts[:dry] = true
221
+ end
222
+
223
+ opts[:output] = nil
224
+ parser.on('-o','--output FILE|DIR', 'File or Directory that plot should be saved to. ' \
225
+ 'If a directory is given', 'the filename will be generated. Default is csv file directory.') do |path|
226
+ opts[:output] = path
227
+ end
228
+
229
+ opts[:y_max]
230
+ parser.on('-y', '--y-range RANGE', Float, 'Sets the y-axis range. Default is 105. ' \
231
+ 'If a value exceeds this range,', '"autoscale" is enabled.') do |range|
232
+ opts[:y_max] = range
233
+ end
234
+
235
+ opts[:title] = nil
236
+ parser.on('-t', '--title TITLE', 'Override the default title of the plot.') do |title|
237
+ opts[:title] = title
238
+ end
239
+
240
+ opts[:smooth] = nil
241
+ parser.on('-s', '--smoothing ALGORITHM', 'Smoothes the graph using the given algorithm.') do |algorithm|
242
+ algorithms = [ 'unique', 'frequency', 'cumulative', 'cnormal', 'kdensity', 'unwrap',
243
+ 'csplines', 'acsplines', 'mcsplines', 'bezier', 'sbezier' ]
244
+ if algorithms.index(algorithm)
245
+ opts[:smooth] = algorithm
246
+ else
247
+ puts "#{algorithm} is not a valid option as an algorithm."
248
+ exit
249
+ end
250
+ end
251
+
252
+ opts[:slice_size] = nil
253
+ parser.on('-a', '--average-over SLICE_SIZE', Integer, 'Calculates the everage for slice_size large groups of values.',"\n") do |slice_size|
254
+ opts[:slice_size] = slice_size
255
+ end
256
+
257
+ opts[:category] = nil
258
+ parser.on('-c', '--category CATEGORY', 'Select the category.') do |category|
259
+ opts[:category] = category
260
+ end
261
+
262
+ opts[:field] = nil
263
+ parser.on('-f', '--field FIELD' , 'Select the field.') do |field|
264
+ opts[:field] = field
265
+ end
266
+
267
+ opts[:column] = nil
268
+ parser.on('-l', '--column COLUMN', 'Select the desired column directly.', "\n") do |column|
269
+ unless opts[:category] && opts[:field] # -c and -f override -l
270
+ opts[:column] = column.to_i
271
+ end
272
+ end
273
+
274
+ # This displays the help screen
275
+ parser.on_tail('-h', '--help', 'Display this screen.' ) do
276
+ puts parser
277
+ exit
278
+ end
279
+ end
280
+
281
+ # there are two forms of the parse method. 'parse'
282
+ # simply parses ARGV, while 'parse!' parses ARGV
283
+ # and removes all options and parameters found. What's
284
+ # left is the list of files
285
+ optparse.parse!
286
+ if $verbose then puts "opts: #{opts.inspect}" end
287
+
288
+ if opts[:category].nil? || opts[:category].nil?
289
+ if opts[:column].nil?
290
+ puts "[Error] (-c CATEGORY and -f FIELD) or (-l COLUMN) are mandatory parameters.\n\n"
291
+ puts optparse
292
+ exit
293
+ end
294
+ end
295
+
296
+ # if ARGV is empty at this point no directory or file(s) is specified
297
+ # and the current working directory is used
298
+ if ARGV.empty? then ARGV.push "." end
299
+
300
+ files = []
301
+ if File.directory?(ARGV.last) then
302
+ opts[:target_dir] = ARGV.last.chomp("/") # cuts of "/" from the end if present
303
+ files = Dir.glob "#{opts[:target_dir]}/*.csv"
304
+ files = files.sort
305
+ else
306
+ opts[:target_dir] = File.dirname ARGV.first
307
+ ARGV.each do |filename|
308
+ files.push filename
309
+ end
310
+ end
311
+ puts "Plotting data from #{files.count} file(s)."
312
+ opts[:files] = files
313
+ if $verbose then puts "files: #{files.count} #{files.inspect}" end
314
+
315
+ # opts = { :inversion, :no_plot_key, :dry, :output, :y_max, :title, :category, :field, :column, :target_dir, :files }
316
+ opts
317
+ end
318
+
319
+ class DstatPlot
320
+ def self.run
321
+ opts = read_options_and_arguments
322
+ dataset_container = read_data_from_csv(opts[:files],opts[:category], opts[:field], opts[:column],
323
+ opts[:no_plot_key], opts[:y_max], opts[:inversion], opts[:title], opts[:smooth])
324
+ data_preprocessing(dataset_container, opts[:slice_size]) unless opts[:slice_size].nil?
325
+ filename = generate_filename(opts[:output], opts[:column], opts[:category], opts[:field], opts[:target_dir])
326
+ plot(dataset_container, opts[:category], opts[:field], opts[:dry], filename)
327
+ end
328
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dstat_plot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.2
5
+ platform: ruby
6
+ authors:
7
+ - joh-mue
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gnuplot
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ description: Uses gnuplot to plot csv data generated by mvneves' dstat-monitor.
28
+ email: yesiamkeen@gmail.com
29
+ executables:
30
+ - dstat-plot
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - bin/dstat-plot
35
+ - lib/dstat_plot.rb
36
+ homepage: http://github.com/citlab/dstat-tools
37
+ licenses:
38
+ - MIT
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 2.5.1
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Plot dstat-monitor data with gnuplot
60
+ test_files: []