logstash-perftool 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }