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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +3 -0
  4. data/CONTRIBUTORS +11 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +13 -0
  7. data/README.md +119 -0
  8. data/Rakefile +10 -0
  9. data/bin/lsperfm +9 -0
  10. data/bin/lsperfm-deps +32 -0
  11. data/examples/config/complex_syslog.conf +46 -0
  12. data/examples/config/json_inout_codec.conf +11 -0
  13. data/examples/config/json_inout_filter.conf +11 -0
  14. data/examples/config/simple.conf +11 -0
  15. data/examples/config/simple_grok.conf +13 -0
  16. data/examples/config/simple_json_out.conf +11 -0
  17. data/examples/input/apache_log.txt +30 -0
  18. data/examples/input/json_medium.txt +10 -0
  19. data/examples/input/simple_10.txt +10 -0
  20. data/examples/input/syslog_acl_10.txt +10 -0
  21. data/examples/suite/basic_performance_long.rb +18 -0
  22. data/examples/suite/basic_performance_quick.rb +18 -0
  23. data/lib/lsperfm.rb +22 -0
  24. data/lib/lsperfm/core.rb +81 -0
  25. data/lib/lsperfm/core/reporter.rb +21 -0
  26. data/lib/lsperfm/core/run.rb +139 -0
  27. data/lib/lsperfm/core/stats.rb +64 -0
  28. data/lib/lsperfm/defaults/config/complex_syslog.conf +46 -0
  29. data/lib/lsperfm/defaults/config/json_inout_codec.conf +11 -0
  30. data/lib/lsperfm/defaults/config/json_inout_filter.conf +11 -0
  31. data/lib/lsperfm/defaults/config/simple.conf +11 -0
  32. data/lib/lsperfm/defaults/config/simple_grok.conf +13 -0
  33. data/lib/lsperfm/defaults/config/simple_json_out.conf +11 -0
  34. data/lib/lsperfm/defaults/input/apache_log.txt +30 -0
  35. data/lib/lsperfm/defaults/input/json_medium.txt +10 -0
  36. data/lib/lsperfm/defaults/input/simple_10.txt +10 -0
  37. data/lib/lsperfm/defaults/input/syslog_acl_10.txt +10 -0
  38. data/lib/lsperfm/defaults/suite.rb +12 -0
  39. data/lib/lsperfm/defaults/suite/long.rb +18 -0
  40. data/lib/lsperfm/defaults/suite/quick.rb +18 -0
  41. data/lib/lsperfm/version.rb +5 -0
  42. data/logstash-perftool.gemspec +24 -0
  43. data/scripts/loader.rb +114 -0
  44. data/scripts/setup.sh +63 -0
  45. data/spec/fixtures/basic_suite.rb +4 -0
  46. data/spec/fixtures/config.yml +4 -0
  47. data/spec/fixtures/simple.conf +11 -0
  48. data/spec/fixtures/simple_10.txt +10 -0
  49. data/spec/fixtures/wrong_config.yml +4 -0
  50. data/spec/lib/runner_spec.rb +35 -0
  51. data/spec/lib/suite_spec.rb +51 -0
  52. data/spec/spec_helper.rb +9 -0
  53. data/suite.rb +46 -0
  54. 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
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ input {
2
+ stdin { codec => "json_lines" }
3
+ }
4
+
5
+ filter {
6
+ clone {}
7
+ }
8
+
9
+ output {
10
+ stdout { codec => json_lines }
11
+ }
@@ -0,0 +1,11 @@
1
+ input {
2
+ stdin {}
3
+ }
4
+
5
+ filter {
6
+ json { source => "message" }
7
+ }
8
+
9
+ output {
10
+ stdout { codec => json_lines }
11
+ }
@@ -0,0 +1,11 @@
1
+ input {
2
+ stdin {}
3
+ }
4
+
5
+ filter {
6
+ clone {}
7
+ }
8
+
9
+ output {
10
+ stdout { codec => line }
11
+ }
@@ -0,0 +1,13 @@
1
+ input {
2
+ stdin { type => "apache" }
3
+ }
4
+
5
+ filter {
6
+ grok {
7
+ match => {"message" => "%{COMBINEDAPACHELOG}"}
8
+ }
9
+ }
10
+
11
+ output {
12
+ stdout { codec => line }
13
+ }
@@ -0,0 +1,11 @@
1
+ input {
2
+ stdin {}
3
+ }
4
+
5
+ filter {
6
+ clone {}
7
+ }
8
+
9
+ output {
10
+ stdout { codec => json_lines }
11
+ }