logstash-perftool 0.1.0
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/.gitignore +4 -0
- data/.rspec +3 -0
- data/CONTRIBUTORS +11 -0
- data/Gemfile +3 -0
- data/LICENSE +13 -0
- data/README.md +119 -0
- data/Rakefile +10 -0
- data/bin/lsperfm +9 -0
- data/bin/lsperfm-deps +32 -0
- data/examples/config/complex_syslog.conf +46 -0
- data/examples/config/json_inout_codec.conf +11 -0
- data/examples/config/json_inout_filter.conf +11 -0
- data/examples/config/simple.conf +11 -0
- data/examples/config/simple_grok.conf +13 -0
- data/examples/config/simple_json_out.conf +11 -0
- data/examples/input/apache_log.txt +30 -0
- data/examples/input/json_medium.txt +10 -0
- data/examples/input/simple_10.txt +10 -0
- data/examples/input/syslog_acl_10.txt +10 -0
- data/examples/suite/basic_performance_long.rb +18 -0
- data/examples/suite/basic_performance_quick.rb +18 -0
- data/lib/lsperfm.rb +22 -0
- data/lib/lsperfm/core.rb +81 -0
- data/lib/lsperfm/core/reporter.rb +21 -0
- data/lib/lsperfm/core/run.rb +139 -0
- data/lib/lsperfm/core/stats.rb +64 -0
- data/lib/lsperfm/defaults/config/complex_syslog.conf +46 -0
- data/lib/lsperfm/defaults/config/json_inout_codec.conf +11 -0
- data/lib/lsperfm/defaults/config/json_inout_filter.conf +11 -0
- data/lib/lsperfm/defaults/config/simple.conf +11 -0
- data/lib/lsperfm/defaults/config/simple_grok.conf +13 -0
- data/lib/lsperfm/defaults/config/simple_json_out.conf +11 -0
- data/lib/lsperfm/defaults/input/apache_log.txt +30 -0
- data/lib/lsperfm/defaults/input/json_medium.txt +10 -0
- data/lib/lsperfm/defaults/input/simple_10.txt +10 -0
- data/lib/lsperfm/defaults/input/syslog_acl_10.txt +10 -0
- data/lib/lsperfm/defaults/suite.rb +12 -0
- data/lib/lsperfm/defaults/suite/long.rb +18 -0
- data/lib/lsperfm/defaults/suite/quick.rb +18 -0
- data/lib/lsperfm/version.rb +5 -0
- data/logstash-perftool.gemspec +24 -0
- data/scripts/loader.rb +114 -0
- data/scripts/setup.sh +63 -0
- data/spec/fixtures/basic_suite.rb +4 -0
- data/spec/fixtures/config.yml +4 -0
- data/spec/fixtures/simple.conf +11 -0
- data/spec/fixtures/simple_10.txt +10 -0
- data/spec/fixtures/wrong_config.yml +4 -0
- data/spec/lib/runner_spec.rb +35 -0
- data/spec/lib/suite_spec.rb +51 -0
- data/spec/spec_helper.rb +9 -0
- data/suite.rb +46 -0
- metadata +154 -0
data/lib/lsperfm.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'lsperfm/core'
|
3
|
+
|
4
|
+
module LogStash
|
5
|
+
module PerformanceMeter
|
6
|
+
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def invoke
|
10
|
+
debug = !!ENV['DEBUG']
|
11
|
+
headers = !!ENV['HEADERS']
|
12
|
+
|
13
|
+
install_path = ARGV.size > 1 ? ARGV[1] : Dir.pwd
|
14
|
+
definition = ARGV.size > 0 ? ARGV[0] : ""
|
15
|
+
|
16
|
+
runner = LogStash::PerformanceMeter::Core.new(definition, install_path)
|
17
|
+
runner.config = '.lsperfm' if File.exist?('.lsperfm.yml')
|
18
|
+
puts runner.run(debug, headers).join("\n")
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/lib/lsperfm/core.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'lsperfm/core/reporter'
|
2
|
+
require 'lsperfm/core/run'
|
3
|
+
|
4
|
+
module LogStash::PerformanceMeter
|
5
|
+
|
6
|
+
class ConfigException < Exception; end
|
7
|
+
|
8
|
+
class Core
|
9
|
+
|
10
|
+
attr_reader :definition, :install_path, :runner, :config
|
11
|
+
|
12
|
+
def initialize(definition, install_path, config='', runner = LogStash::PerformanceMeter::Runner)
|
13
|
+
@definition = definition
|
14
|
+
@install_path = install_path
|
15
|
+
@runner = runner
|
16
|
+
@config = load_config(config)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(debug=false, headers=false)
|
20
|
+
tests = load_tests(definition)
|
21
|
+
lines = (headers ? ["name, #{runner.headers.join(',')}"] : [])
|
22
|
+
reporter = LogStash::PerformanceMeter::Reporter.new.start
|
23
|
+
tests.each do |test|
|
24
|
+
events = test[:events].to_i
|
25
|
+
time = test[:time].to_i
|
26
|
+
|
27
|
+
manager = runner.new(find_test_config(test[:config]), debug, install_path)
|
28
|
+
metrics = manager.run(events, time, runner.read_input_file(find_test_input(test[:input])))
|
29
|
+
lines << formatter(test[:name], metrics)
|
30
|
+
end
|
31
|
+
lines
|
32
|
+
rescue Errno::ENOENT => e
|
33
|
+
raise ConfigException.new(e)
|
34
|
+
ensure
|
35
|
+
reporter.stop if reporter
|
36
|
+
end
|
37
|
+
|
38
|
+
def config=(config)
|
39
|
+
@config = load_config(config)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def load_tests(definition)
|
45
|
+
return load_default_tests if definition.empty?
|
46
|
+
eval(IO.read(definition))
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_default_tests
|
50
|
+
require 'lsperfm/defaults/suite.rb'
|
51
|
+
LogStash::PerformanceMeter::DEFAULT_SUITE
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_config(config)
|
55
|
+
::YAML::load_file(config)['default'] rescue {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_test_config(file)
|
59
|
+
return file if config.empty?
|
60
|
+
File.join(config['path'], config['config'], file)
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_test_input(file)
|
64
|
+
return file if config.empty?
|
65
|
+
File.join(config['path'], config['input'], file)
|
66
|
+
end
|
67
|
+
|
68
|
+
def formatter(test_name, args={})
|
69
|
+
percentile = args[:percentile]
|
70
|
+
[
|
71
|
+
"%s" % test_name,
|
72
|
+
"%.2f" % args[:start_time],
|
73
|
+
"%.2f" % args[:elapsed],
|
74
|
+
"%.0f" % args[:events_count],
|
75
|
+
"%.0f" % (args[:events_count] / args[:elapsed]),
|
76
|
+
"%.2f" % percentile.last,
|
77
|
+
"%.0f" % (percentile.reduce(:+) / percentile.size)
|
78
|
+
].join(',')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module LogStash
|
4
|
+
module PerformanceMeter
|
5
|
+
class Reporter
|
6
|
+
def start
|
7
|
+
@reporter = Thread.new do
|
8
|
+
loop do
|
9
|
+
$stderr.print '.'
|
10
|
+
sleep 1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
@reporter.kill
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "benchmark"
|
3
|
+
require "thread"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
require 'lsperfm/core/stats'
|
7
|
+
|
8
|
+
Thread.abort_on_exception = true
|
9
|
+
|
10
|
+
module LogStash::PerformanceMeter
|
11
|
+
|
12
|
+
class Runner
|
13
|
+
LOGSTASH_BIN = File.join("bin", "logstash").freeze
|
14
|
+
|
15
|
+
INITIAL_MESSAGE = ">>> lorem ipsum start".freeze
|
16
|
+
LAST_MESSAGE = ">>> lorem ipsum stop".freeze
|
17
|
+
REFRESH_COUNT = 100
|
18
|
+
|
19
|
+
attr_reader :command
|
20
|
+
|
21
|
+
def initialize(config, debug = false, logstash_home = Dir.pwd)
|
22
|
+
@debug = debug
|
23
|
+
@command = [File.join(logstash_home, LOGSTASH_BIN), "-f", config]
|
24
|
+
end
|
25
|
+
|
26
|
+
def run(required_events_count, required_run_time, input_lines)
|
27
|
+
puts("launching #{command.join(" ")} #{required_events_count} #{required_run_time}") if @debug
|
28
|
+
stats = LogStash::PerformanceMeter::Stats.new
|
29
|
+
real_events_count = 0
|
30
|
+
Open3.popen3(*@command) do |i, o, e|
|
31
|
+
puts("sending initial event") if @debug
|
32
|
+
start_time = Benchmark.realtime do
|
33
|
+
i.puts(INITIAL_MESSAGE)
|
34
|
+
i.flush
|
35
|
+
|
36
|
+
puts("waiting for initial event") if @debug
|
37
|
+
expect_output(o, /#{INITIAL_MESSAGE}/)
|
38
|
+
end
|
39
|
+
|
40
|
+
puts("starting output reader thread") if @debug
|
41
|
+
reader = stats.detach_output_reader(o, /#{LAST_MESSAGE}/)
|
42
|
+
puts("starting feeding input") if @debug
|
43
|
+
|
44
|
+
elapsed = Benchmark.realtime do
|
45
|
+
real_events_count = feed_input_with(required_events_count, required_run_time, input_lines, i)
|
46
|
+
puts("waiting for output reader to complete") if @debug
|
47
|
+
reader.join
|
48
|
+
end
|
49
|
+
{ :percentile => percentile(stats.stats, 0.80) , :elapsed => elapsed, :events_count => real_events_count, :start_time => start_time }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.headers
|
54
|
+
["start time", "elapsed", "events", "avg tps", "best tps", "avg top 20% tps"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def feed_input_with(required_events_count, required_run_time, input_lines, i)
|
58
|
+
if required_events_count > 0
|
59
|
+
feed_input_events(i, [required_events_count, input_lines.size].max, input_lines, LAST_MESSAGE)
|
60
|
+
else
|
61
|
+
feed_input_interval(i, required_run_time, input_lines, LAST_MESSAGE)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.read_input_file(file_path)
|
66
|
+
IO.readlines(file_path).map(&:chomp)
|
67
|
+
end
|
68
|
+
|
69
|
+
def feed_input_events(io, events_count, lines, last_message)
|
70
|
+
loop_count = (events_count / lines.size).ceil # how many time we send the input file over
|
71
|
+
(1..loop_count).each{lines.each {|line| io.puts(line)}}
|
72
|
+
|
73
|
+
io.puts(last_message)
|
74
|
+
io.flush
|
75
|
+
|
76
|
+
loop_count * lines.size
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def feed_input_interval(io, seconds, lines, last_message)
|
82
|
+
loop_count = (2000 / lines.size).ceil # check time every ~2000(ceil) input lines
|
83
|
+
lines_per_iteration = loop_count * lines.size
|
84
|
+
start_time = Time.now
|
85
|
+
count = 0
|
86
|
+
|
87
|
+
while true
|
88
|
+
(1..loop_count).each{lines.each {|line| io.puts(line)}}
|
89
|
+
count += lines_per_iteration
|
90
|
+
break if (Time.now - start_time) >= seconds
|
91
|
+
end
|
92
|
+
|
93
|
+
io.puts(last_message)
|
94
|
+
io.flush
|
95
|
+
|
96
|
+
count
|
97
|
+
end
|
98
|
+
|
99
|
+
def expect_output(io, regex)
|
100
|
+
io.each_line do |line|
|
101
|
+
puts("received: #{line}") if @debug
|
102
|
+
yield if block_given?
|
103
|
+
break if line =~ regex
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def percentile(array, percentile)
|
108
|
+
count = (array.length * (1.0 - percentile)).floor
|
109
|
+
array.sort[-count..-1]
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
def extract_options(args)
|
115
|
+
options = {}
|
116
|
+
while !args.empty?
|
117
|
+
config = args.shift.to_s.strip
|
118
|
+
option = args.shift.to_s.strip
|
119
|
+
raise(IllegalArgumentException, "invalid option for #{config}") if option.empty?
|
120
|
+
case config
|
121
|
+
when "--events"
|
122
|
+
options[:events] = option
|
123
|
+
when "--time"
|
124
|
+
options[:time] = option
|
125
|
+
when "--config"
|
126
|
+
options[:config] = option
|
127
|
+
when "--input"
|
128
|
+
options[:input] = option
|
129
|
+
when "--headers"
|
130
|
+
options[:headers] = option
|
131
|
+
else
|
132
|
+
raise(IllegalArgumentException, "invalid config #{config}")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
options
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
Thread.abort_on_exception = true
|
5
|
+
|
6
|
+
module LogStash::PerformanceMeter
|
7
|
+
|
8
|
+
class Stats
|
9
|
+
|
10
|
+
REFRESH_COUNT = 100
|
11
|
+
|
12
|
+
attr_accessor :stats
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@stats = []
|
16
|
+
end
|
17
|
+
# below stats counter and output reader threads are sharing state using
|
18
|
+
# the @stats_lock mutex, @stats_count and @stats. this is a bit messy and should be
|
19
|
+
# refactored into a proper class eventually
|
20
|
+
|
21
|
+
def detach_stats_counter
|
22
|
+
Thread.new do
|
23
|
+
loop do
|
24
|
+
start = @stats_lock.synchronize{@stats_count}
|
25
|
+
sleep(1)
|
26
|
+
@stats_lock.synchronize{@stats << (@stats_count - start)}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# detach_output_reader spawns a thread that will fill in the @stats instance var with tps samples for every seconds
|
32
|
+
# @stats access is synchronized using the @stats_lock mutex but can be safely used
|
33
|
+
# once the output reader thread is completed.
|
34
|
+
def detach_output_reader(io, regex)
|
35
|
+
Thread.new(io, regex) do |io, regex|
|
36
|
+
i = 0
|
37
|
+
@stats = []
|
38
|
+
@stats_count = 0
|
39
|
+
@stats_lock = Mutex.new
|
40
|
+
t = detach_stats_counter
|
41
|
+
|
42
|
+
expect_output(io, regex) do
|
43
|
+
i += 1
|
44
|
+
# avoid mutex synchronize on every loop cycle, using REFRESH_COUNT = 100 results in
|
45
|
+
# much lower mutex overhead and still provides a good resolution since we are typically
|
46
|
+
# have 2000..100000 tps
|
47
|
+
@stats_lock.synchronize{@stats_count = i} if (i % REFRESH_COUNT) == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
@stats_lock.synchronize{t.kill}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def expect_output(io, regex)
|
55
|
+
io.each_line do |line|
|
56
|
+
puts("received: #{line}") if @debug
|
57
|
+
yield if block_given?
|
58
|
+
break if line =~ regex
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
input {
|
2
|
+
stdin {
|
3
|
+
type => syslog
|
4
|
+
}
|
5
|
+
}
|
6
|
+
|
7
|
+
filter {
|
8
|
+
if [type] == "syslog" {
|
9
|
+
grok {
|
10
|
+
match => { "message" => "<%{POSINT:syslog_pri}>%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{PROG:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{GREEDYDATA:syslog_message}" }
|
11
|
+
add_field => [ "received_at", "%{@timestamp}" ]
|
12
|
+
add_field => [ "received_from", "%{syslog_hostname}" ]
|
13
|
+
}
|
14
|
+
syslog_pri { }
|
15
|
+
date {
|
16
|
+
match => ["syslog_timestamp", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ]
|
17
|
+
}
|
18
|
+
|
19
|
+
if [syslog_timestamp] {
|
20
|
+
mutate {
|
21
|
+
add_field => [ "[times][created_at]", "%{syslog_timestamp}"]
|
22
|
+
add_field => [ "[times][received_at]", "%{@timestamp}"]
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
mutate {
|
27
|
+
add_field => [ "[hosts][source]", "%{received_from}"]
|
28
|
+
add_field => [ "[level][facility]", "%{syslog_facility}"]
|
29
|
+
add_field => [ "[level][severity]", "%{syslog_severity}"]
|
30
|
+
}
|
31
|
+
|
32
|
+
if !("_grokparsefailure" in [tags]) {
|
33
|
+
mutate {
|
34
|
+
replace => [ "@source_host", "%{syslog_hostname}" ]
|
35
|
+
replace => [ "@message", "%{syslog_message}" ]
|
36
|
+
}
|
37
|
+
}
|
38
|
+
mutate {
|
39
|
+
remove_field => [ "syslog_hostname", "syslog_message", "syslog_timestamp" ]
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
output {
|
45
|
+
stdout { codec => json_lines }
|
46
|
+
}
|