dstat_plot 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/dstat-plot +4 -0
- data/lib/dstat_plot.rb +328 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/dstat-plot
ADDED
data/lib/dstat_plot.rb
ADDED
@@ -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: []
|