├── .gitignore ├── .rspec ├── CONTRIBUTORS ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── lsperfm └── lsperfm-deps ├── examples ├── config │ ├── complex_syslog.conf │ ├── json_inout_codec.conf │ ├── json_inout_filter.conf │ ├── simple.conf │ ├── simple_grok.conf │ └── simple_json_out.conf ├── input │ ├── apache_log.txt │ ├── json_medium.txt │ ├── simple_10.txt │ └── syslog_acl_10.txt └── suite │ ├── basic_performance_long.rb │ └── basic_performance_quick.rb ├── lib ├── lsperfm.rb └── lsperfm │ ├── core.rb │ ├── core │ ├── reporter.rb │ ├── run.rb │ └── stats.rb │ ├── defaults │ ├── config │ │ ├── complex_syslog.conf │ │ ├── json_inout_codec.conf │ │ ├── json_inout_filter.conf │ │ ├── simple.conf │ │ ├── simple_grok.conf │ │ └── simple_json_out.conf │ ├── input │ │ ├── apache_log.txt │ │ ├── json_medium.txt │ │ ├── simple_10.txt │ │ └── syslog_acl_10.txt │ ├── suite.rb │ └── suite │ │ ├── long.rb │ │ └── quick.rb │ └── version.rb ├── logstash-perftool.gemspec ├── scripts ├── loader.rb ├── runner.sh └── setup.sh ├── spec ├── fixtures │ ├── basic_suite.rb │ ├── config.yml │ ├── simple.conf │ ├── simple_10.txt │ └── wrong_config.yml ├── lib │ ├── runner_spec.rb │ └── suite_spec.rb └── spec_helper.rb ├── suite.rb ├── web ├── Rakefile ├── api │ ├── Gemfile │ ├── README.markdown │ ├── Rakefile │ ├── config.rb │ ├── config.ru │ ├── config.yml │ ├── lib │ │ ├── app │ │ │ ├── decorator.rb │ │ │ └── fetcher.rb │ │ ├── application.rb │ │ └── workers │ │ │ ├── config_manager.rb │ │ │ ├── config_worker.rb │ │ │ ├── runner.rb │ │ │ └── test_worker.rb │ ├── nginx.conf │ └── test │ │ ├── application_test.rb │ │ ├── fetcher_test.rb │ │ └── test_helper.rb ├── benchmarks │ ├── config │ │ ├── complex_syslog.conf │ │ ├── json_inout_codec.conf │ │ ├── json_inout_filter.conf │ │ ├── simple.conf │ │ ├── simple_grok.conf │ │ └── simple_json_out.conf │ ├── input │ │ ├── apache_log.txt │ │ ├── json_medium.txt │ │ ├── simple_10.txt │ │ └── syslog_acl_10.txt │ └── suite.rb └── ui │ ├── README.markdown │ ├── Rakefile │ ├── data │ ├── events.json │ └── startup_time.json │ ├── images │ └── loading.png │ ├── index.html │ ├── javascripts │ ├── application.js │ ├── matrix_chart.js │ ├── startup_time.js │ └── time_line_chart.js │ ├── stylesheets │ ├── application.css │ ├── matrix_chart.css │ ├── startup_time.css │ └── time_line_chart.css │ └── test │ ├── Gemfile │ ├── README.markdown │ └── features │ ├── dashboard.feature │ ├── setup │ ├── application.rb │ └── env.rb │ └── step_definitions │ ├── common_steps.rb │ └── dashboard_steps.rb └── webapp └── config.yml /.gitignore: -------------------------------------------------------------------------------- 1 | webapp/pids/ 2 | *.lock 3 | *.gem 4 | config.txt 5 | workspace/ 6 | utils/ 7 | tmp/ 8 | coverage/ 9 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --colour 3 | --tty 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | The following is a list of people who have contributed ideas, code, bug 2 | reports, or in general have helped logstash along its way. 3 | 4 | Contributors: 5 | * Pere Urbon-Bayes (purbon) 6 | * Colin Surprenant (colin) 7 | 8 | Note: If you've sent me patches, bug reports, or otherwise contributed to 9 | logstash, and you aren't on the list above and want to be, please let me know 10 | and I'll make sure you're here. Contributions from folks like you are what make 11 | open source awesome. 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012–2015 Elasticsearch 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Performance Testing for Logstash 2 | 3 | ## Installation 4 | 5 | You can use this code as a gem within your logstash project, to proceed with the installation you can either download the code and build the gem using the next command: 6 | 7 | To run a benchmark using the Logstash Performance meter tool you will need to install this gem in your system, and to do it you can run the next command: 8 | 9 | * ```gem install logstash-perftool``` 10 | 11 | This will make the last version of this gem available to you. 12 | 13 | or, if you like to be on the edge, you can add it to your Gemfile like this: 14 | 15 | gem 'logstash-perftool', :git => 'https://github.com/elastic/logstash-performance-testing.git' 16 | 17 | and then do budler update. 18 | 19 | ## Setup and Runtime 20 | 21 | The most simple scenario you could find is using the default set of 22 | test, available in this gem. To do this you can simple run the ```lsperfm``` 23 | from the root of your Logstash installation. 24 | 25 | If you like to add you own configurations and test suites, you need the next data (you can see an example of them at the `examples/` directory): 26 | 27 | - The logstash configs, found in `..config/` 28 | - The sample input files, found in `..input/` 29 | - The suites definitions, found in `..suite/` 30 | 31 | ### Configuration 32 | 33 | If you add a file named ```.lsperfm.yml``` in your main logstash directory you can have your configuration and input files in non standard 34 | location. 35 | 36 | Example: 37 | 38 | ``` 39 | default: 40 | path: 'config-path' 41 | config: '' 42 | input: '' 43 | ``` 44 | 45 | ### Bootstrap 46 | 47 | Before you can run your test is necessary to bootstrap your logstash installation and install the test dependencies, to do that you must: 48 | 49 | If you are in 1.5.x: 50 | - Run `rake bootstrap` to setup the system. 51 | - Run `lsperfm-deps` to install the test dependencies 52 | For 1.4: 53 | - Run `bin/logstash deps` to setup everything. 54 | 55 | ## Performance tests 56 | 57 | The test are run in groups called suites. 58 | 59 | ### How to execute the default tests 60 | 61 | This is the most simple use case you can have. To run the default tests 62 | you can simply run ```lsperfm``` from the root of your Logstash 63 | installation and the tool will use the default test suite. 64 | 65 | ### How to run a custom test suite 66 | 67 | - suites examples can be found in `examples/suite/` 68 | 69 | ``` 70 | lsperfm [suite definition] 71 | ``` 72 | 73 | a suite file defines a series of tests to run. 74 | 75 | #### suite file format 76 | 77 | ```ruby 78 | # each test can be executed by either target duration using :time => N secs 79 | # or by number of events with :events => N 80 | # 81 | #[ 82 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 30}, 83 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :events => 50000}, 84 | #] 85 | # 86 | [ 87 | {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 60}, 88 | {:name => "simple line out", :config => "config/simple.conf", :input => "input/simple_10.txt", :time => 60}, 89 | {:name => "json codec", :config => "config/json_inout_codec.conf", :input => "input/json_medium.txt", :time => 60}, 90 | {:name => "json filter", :config => "config/json_inout_filter.conf", :input => "input/json_medium.txt", :time => 60}, 91 | {:name => "complex syslog", :config => "config/complex_syslog.conf", :input => "input/syslog_acl_10.txt", :time => 60}, 92 | ] 93 | ``` 94 | 95 | ## Contributing 96 | 97 | All contributions are welcome: ideas, patches, documentation, bug reports, 98 | complaints, and even something you drew up on a napkin. 99 | 100 | Programming is not a required skill. Whatever you've seen about open source and 101 | maintainers or community members saying "send patches or die" - you will not 102 | see that here. 103 | 104 | It is more important to me that you are able to contribute. 105 | 106 | ### Contribution Steps 107 | 108 | 1. Test your changes! Write test and run the test suites. 109 | 2. Please make sure you have signed our [Contributor License 110 | Agreement](http://www.elastic.co/contributor-agreement/). We are not 111 | asking you to assign copyright to us, but to give us the right to distribute 112 | your code without restriction. We ask this of all contributors in order to 113 | assure our users of the origin and continuing existence of the code. You 114 | only need to sign the CLA once. 115 | 3. Send a pull request! Push your changes to your fork of the repository and 116 | [submit a pull 117 | request](https://help.github.com/articles/using-pull-requests). In the pull 118 | request, describe what your changes do and mention any bugs/issues related 119 | to the pull request. 120 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec' 4 | require 'rspec/core/rake_task' 5 | 6 | desc "Run all specs" 7 | RSpec::Core::RakeTask.new(:spec) do |t| 8 | t.fail_on_error = true 9 | t.verbose = false 10 | end 11 | -------------------------------------------------------------------------------- /bin/lsperfm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # encoding: utf-8 4 | 5 | $LOAD_PATH << "." 6 | 7 | require 'lsperfm' 8 | 9 | LogStash::PerformanceMeter.invoke 10 | -------------------------------------------------------------------------------- /bin/lsperfm-deps: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # encoding: utf-8 4 | 5 | puts "installing dependencies..." 6 | 7 | logstash_home = ENV['LOGSTASH_HOME'] || Dir.pwd 8 | base_dir = (ARGV.size < 1 ? logstash_home : ARGV[0]) 9 | 10 | logstash = File.join(base_dir, "bin", "logstash") 11 | version = `#{logstash} --version` 12 | 13 | if version[/\d\.\d\.\d/] =~ /1\.4\./ 14 | # why do deps here? can't we assume you must have a working logstash distribution? 15 | # `#{logstash} deps 2>&1` 16 | else 17 | inputs = ['stdin'].map{|s| "input-#{s}"} 18 | outputs = ['stdout'].map{|s| "output-#{s}"} 19 | filters = ['clone', 'json', 'grok', 'syslog_pri', 'date', 'mutate'].map{|s| "filter-#{s}"} 20 | 21 | # why do bootstrap here? can't we assume you must have a working logstash distribution? 22 | # `#{rake} bootstrap` 23 | installer = File.join(base_dir, "bin", "plugin") 24 | if !File.exist?(installer) 25 | installer = File.join(base_dir, "bin", "logstash-plugin") 26 | end 27 | [inputs, outputs, filters].each do |plugins| 28 | plugins.map{|s| "logstash-#{s}"}.each do |plugin| 29 | command = "#{installer} install #{plugin}" 30 | puts "#{command}\n#{%x[#{command}]}" 31 | end 32 | end 33 | end 34 | 35 | puts "done!" 36 | -------------------------------------------------------------------------------- /examples/config/complex_syslog.conf: -------------------------------------------------------------------------------- 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 | } 47 | -------------------------------------------------------------------------------- /examples/config/json_inout_codec.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin { codec => "json_lines" } 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /examples/config/json_inout_filter.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | json { source => "message" } 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /examples/config/simple.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => line } 11 | } 12 | -------------------------------------------------------------------------------- /examples/config/simple_grok.conf: -------------------------------------------------------------------------------- 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 | } 14 | -------------------------------------------------------------------------------- /examples/config/simple_json_out.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /examples/input/apache_log.txt: -------------------------------------------------------------------------------- 1 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-search.png HTTP/1.1" 200 203023 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 2 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard3.png HTTP/1.1" 200 171717 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 3 | 83.149.9.216 - - [17/Sep/2014:07:13:44 +0000] "GET /presentations/logstash-monitorama-2013/plugin/highlight/highlight.js HTTP/1.1" 200 26185 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 4 | 83.149.9.216 - - [17/Sep/2014:07:13:44 +0000] "GET /presentations/logstash-monitorama-2013/plugin/zoom-js/zoom.js HTTP/1.1" 200 7697 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 5 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/plugin/notes/notes.js HTTP/1.1" 200 2892 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 6 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/sad-medic.png HTTP/1.1" 200 430406 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 7 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/css/fonts/Roboto-Bold.ttf HTTP/1.1" 200 38720 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 8 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/css/fonts/Roboto-Regular.ttf HTTP/1.1" 200 41820 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 9 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/images/frontend-response-codes.png HTTP/1.1" 200 52878 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 10 | 83.149.9.216 - - [17/Sep/2014:07:13:43 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard.png HTTP/1.1" 200 321631 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 11 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/Dreamhost_logo.svg HTTP/1.1" 200 2126 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 12 | 83.149.9.216 - - [17/Sep/2014:07:13:43 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard2.png HTTP/1.1" 200 394967 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 13 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/apache-icon.gif HTTP/1.1" 200 8095 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 14 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/nagios-sms5.png HTTP/1.1" 200 78075 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 15 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/redis.png HTTP/1.1" 200 25230 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 16 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/elasticsearch.png HTTP/1.1" 200 8026 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 17 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/logstashbook.png HTTP/1.1" 200 54662 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 18 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/github-contributions.png HTTP/1.1" 200 34245 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 19 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/css/print/paper.css HTTP/1.1" 200 4254 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 20 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/1983_delorean_dmc-12-pic-38289.jpeg HTTP/1.1" 200 220562 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 21 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/simple-inputs-filters-outputs.jpg HTTP/1.1" 200 1168622 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 22 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/tiered-outputs-to-inputs.jpg HTTP/1.1" 200 1079983 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 23 | 83.149.9.216 - - [17/Sep/2014:07:13:53 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 24 | 24.236.252.67 - - [17/Sep/2014:07:14:10 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0" 25 | 93.114.45.13 - - [17/Sep/2014:07:14:32 +0000] "GET /articles/dynamic-dns-with-dhcp/ HTTP/1.1" 200 18848 "http://www.google.ro/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0CCwQFjAB&url=http%3A%2F%2Fwww.semicomplete.com%2Farticles%2Fdynamic-dns-with-dhcp%2F&ei=W88AU4n9HOq60QXbv4GwBg&usg=AFQjCNEF1X4Rs52UYQyLiySTQxa97ozM4g&bvm=bv.61535280,d.d2k" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 26 | 93.114.45.13 - - [17/Sep/2014:07:14:32 +0000] "GET /reset.css HTTP/1.1" 200 1015 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 27 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /style2.css HTTP/1.1" 200 4877 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 28 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 29 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /images/jordan-80.png HTTP/1.1" 200 6146 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 30 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /images/web/2009/banner.png HTTP/1.1" 200 52315 "http://www.semicomplete.com/style2.css" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 31 | -------------------------------------------------------------------------------- /examples/input/json_medium.txt: -------------------------------------------------------------------------------- 1 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 2 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 3 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 4 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 5 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 6 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 7 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 8 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 9 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 10 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 11 | -------------------------------------------------------------------------------- /examples/input/simple_10.txt: -------------------------------------------------------------------------------- 1 | test 01 2 | test 02 3 | test 03 4 | test 04 5 | test 05 6 | test 06 7 | test 07 8 | test 08 9 | test 09 10 | test 10 -------------------------------------------------------------------------------- /examples/input/syslog_acl_10.txt: -------------------------------------------------------------------------------- 1 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 2 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 3 | <164>Oct 1 15:21:25 3.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.5/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 4 | <164>Oct 30 15:22:25 4.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.6/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 5 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 6 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 7 | <164>Oct 1 15:21:25 3.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.5/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 8 | <164>Oct 30 15:22:25 4.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.6/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 9 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 10 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 11 | -------------------------------------------------------------------------------- /examples/suite/basic_performance_long.rb: -------------------------------------------------------------------------------- 1 | # format description: 2 | # each test can be executed by either target duration using :time => N secs 3 | # or by number of events with :events => N 4 | # 5 | #[ 6 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 30}, 7 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :events => 50000}, 8 | #] 9 | # 10 | [ 11 | {:name => "simple line in/out", :config => "config/simple.conf", :input => "input/simple_10.txt", :time => 120}, 12 | {:name => "simple line in/json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 120}, 13 | {:name => "json codec in/out", :config => "config/json_inout_codec.conf", :input => "input/json_medium.txt", :time => 120}, 14 | {:name => "line in/json filter/json out", :config => "config/json_inout_filter.conf", :input => "input/json_medium.txt", :time => 120}, 15 | {:name => "apache in/json out", :config => "config/simple.conf", :input => "input/apache_log.txt", :time => 120}, 16 | {:name => "apache in/grok codec/json out", :config => "config/simple_grok.conf", :input => "input/apache_log.txt", :time => 120}, 17 | {:name => "syslog in/json out", :config => "config/complex_syslog.conf", :input => "input/syslog_acl_10.txt", :time => 120}, 18 | ] 19 | -------------------------------------------------------------------------------- /examples/suite/basic_performance_quick.rb: -------------------------------------------------------------------------------- 1 | # format description: 2 | # each test can be executed by either target duration using :time => N secs 3 | # or by number of events with :events => N 4 | # 5 | #[ 6 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 30}, 7 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :events => 50000}, 8 | #] 9 | # 10 | [ 11 | {:name => "simple line in/out", :config => "config/simple.conf", :input => "input/simple_10.txt", :time => 30}, 12 | {:name => "simple line in/json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 30}, 13 | {:name => "json codec in/out", :config => "config/json_inout_codec.conf", :input => "input/json_medium.txt", :time => 30}, 14 | {:name => "line in/json filter/json out", :config => "config/json_inout_filter.conf", :input => "input/json_medium.txt", :time => 30}, 15 | {:name => "apache in/json out", :config => "config/simple.conf", :input => "input/apache_log.txt", :time => 30}, 16 | {:name => "apache in/grok codec/json out", :config => "config/simple_grok.conf", :input => "input/apache_log.txt", :time => 30}, 17 | {:name => "syslog in/json out", :config => "config/complex_syslog.conf", :input => "input/syslog_acl_10.txt", :time => 30}, 18 | ] 19 | -------------------------------------------------------------------------------- /lib/lsperfm.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/lsperfm/core.rb: -------------------------------------------------------------------------------- 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 82 | -------------------------------------------------------------------------------- /lib/lsperfm/core/reporter.rb: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /lib/lsperfm/core/run.rb: -------------------------------------------------------------------------------- 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 140 | -------------------------------------------------------------------------------- /lib/lsperfm/core/stats.rb: -------------------------------------------------------------------------------- 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 65 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/config/complex_syslog.conf: -------------------------------------------------------------------------------- 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 | } 47 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/config/json_inout_codec.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin { codec => "json_lines" } 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/config/json_inout_filter.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | json { source => "message" } 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/config/simple.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => line } 11 | } 12 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/config/simple_grok.conf: -------------------------------------------------------------------------------- 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 | } 14 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/config/simple_json_out.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/input/apache_log.txt: -------------------------------------------------------------------------------- 1 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-search.png HTTP/1.1" 200 203023 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 2 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard3.png HTTP/1.1" 200 171717 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 3 | 83.149.9.216 - - [17/Sep/2014:07:13:44 +0000] "GET /presentations/logstash-monitorama-2013/plugin/highlight/highlight.js HTTP/1.1" 200 26185 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 4 | 83.149.9.216 - - [17/Sep/2014:07:13:44 +0000] "GET /presentations/logstash-monitorama-2013/plugin/zoom-js/zoom.js HTTP/1.1" 200 7697 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 5 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/plugin/notes/notes.js HTTP/1.1" 200 2892 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 6 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/sad-medic.png HTTP/1.1" 200 430406 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 7 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/css/fonts/Roboto-Bold.ttf HTTP/1.1" 200 38720 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 8 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/css/fonts/Roboto-Regular.ttf HTTP/1.1" 200 41820 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 9 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/images/frontend-response-codes.png HTTP/1.1" 200 52878 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 10 | 83.149.9.216 - - [17/Sep/2014:07:13:43 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard.png HTTP/1.1" 200 321631 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 11 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/Dreamhost_logo.svg HTTP/1.1" 200 2126 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 12 | 83.149.9.216 - - [17/Sep/2014:07:13:43 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard2.png HTTP/1.1" 200 394967 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 13 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/apache-icon.gif HTTP/1.1" 200 8095 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 14 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/nagios-sms5.png HTTP/1.1" 200 78075 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 15 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/redis.png HTTP/1.1" 200 25230 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 16 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/elasticsearch.png HTTP/1.1" 200 8026 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 17 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/logstashbook.png HTTP/1.1" 200 54662 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 18 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/github-contributions.png HTTP/1.1" 200 34245 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 19 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/css/print/paper.css HTTP/1.1" 200 4254 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 20 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/1983_delorean_dmc-12-pic-38289.jpeg HTTP/1.1" 200 220562 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 21 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/simple-inputs-filters-outputs.jpg HTTP/1.1" 200 1168622 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 22 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/tiered-outputs-to-inputs.jpg HTTP/1.1" 200 1079983 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 23 | 83.149.9.216 - - [17/Sep/2014:07:13:53 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 24 | 24.236.252.67 - - [17/Sep/2014:07:14:10 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0" 25 | 93.114.45.13 - - [17/Sep/2014:07:14:32 +0000] "GET /articles/dynamic-dns-with-dhcp/ HTTP/1.1" 200 18848 "http://www.google.ro/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0CCwQFjAB&url=http%3A%2F%2Fwww.semicomplete.com%2Farticles%2Fdynamic-dns-with-dhcp%2F&ei=W88AU4n9HOq60QXbv4GwBg&usg=AFQjCNEF1X4Rs52UYQyLiySTQxa97ozM4g&bvm=bv.61535280,d.d2k" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 26 | 93.114.45.13 - - [17/Sep/2014:07:14:32 +0000] "GET /reset.css HTTP/1.1" 200 1015 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 27 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /style2.css HTTP/1.1" 200 4877 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 28 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 29 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /images/jordan-80.png HTTP/1.1" 200 6146 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 30 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /images/web/2009/banner.png HTTP/1.1" 200 52315 "http://www.semicomplete.com/style2.css" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 31 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/input/json_medium.txt: -------------------------------------------------------------------------------- 1 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 2 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 3 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 4 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 5 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 6 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 7 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 8 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 9 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 10 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 11 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/input/simple_10.txt: -------------------------------------------------------------------------------- 1 | test 01 2 | test 02 3 | test 03 4 | test 04 5 | test 05 6 | test 06 7 | test 07 8 | test 08 9 | test 09 10 | test 10 -------------------------------------------------------------------------------- /lib/lsperfm/defaults/input/syslog_acl_10.txt: -------------------------------------------------------------------------------- 1 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 2 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 3 | <164>Oct 1 15:21:25 3.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.5/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 4 | <164>Oct 30 15:22:25 4.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.6/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 5 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 6 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 7 | <164>Oct 1 15:21:25 3.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.5/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 8 | <164>Oct 30 15:22:25 4.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.6/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 9 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 10 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 11 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/suite.rb: -------------------------------------------------------------------------------- 1 | module LogStash::PerformanceMeter 2 | 3 | base_dir = File.dirname(__FILE__) 4 | 5 | DEFAULT_SUITE = [ { :name => "simple line in/out", :config => "#{base_dir}/config/simple.conf", :input => "#{base_dir}/input/simple_10.txt", :time => 120 }, 6 | { :name => "simple line in/json out", :config => "#{base_dir}/config/simple_json_out.conf", :input => "#{base_dir}/input/simple_10.txt", :time => 120 }, 7 | { :name => "json codec in/out", :config => "#{base_dir}/config/json_inout_codec.conf", :input => "#{base_dir}/input/json_medium.txt", :time => 120 }, 8 | { :name => "line in/json filter/json out", :config => "#{base_dir}/config/json_inout_filter.conf", :input => "#{base_dir}/input/json_medium.txt", :time => 120 }, 9 | { :name => "apache in/json out", :config => "#{base_dir}/config/simple.conf", :input => "#{base_dir}/input/apache_log.txt", :time => 120 }, 10 | { :name => "apache in/grok codec/json out", :config => "#{base_dir}/config/simple_grok.conf", :input => "#{base_dir}/input/apache_log.txt", :time => 120 }, 11 | { :name => "syslog in/json out", :config => "#{base_dir}/config/complex_syslog.conf", :input => "#{base_dir}/input/syslog_acl_10.txt", :time => 120} ] 12 | end 13 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/suite/long.rb: -------------------------------------------------------------------------------- 1 | # format description: 2 | # each test can be executed by either target duration using :time => N secs 3 | # or by number of events with :events => N 4 | # 5 | #[ 6 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 30}, 7 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :events => 50000}, 8 | #] 9 | # 10 | [ 11 | {:name => "simple line in/out", :config => "config/simple.conf", :input => "input/simple_10.txt", :time => 120}, 12 | {:name => "simple line in/json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 120}, 13 | {:name => "json codec in/out", :config => "config/json_inout_codec.conf", :input => "input/json_medium.txt", :time => 120}, 14 | {:name => "line in/json filter/json out", :config => "config/json_inout_filter.conf", :input => "input/json_medium.txt", :time => 120}, 15 | {:name => "apache in/json out", :config => "config/simple.conf", :input => "input/apache_log.txt", :time => 120}, 16 | {:name => "apache in/grok codec/json out", :config => "config/simple_grok.conf", :input => "input/apache_log.txt", :time => 120}, 17 | {:name => "syslog in/json out", :config => "config/complex_syslog.conf", :input => "input/syslog_acl_10.txt", :time => 120}, 18 | ] 19 | -------------------------------------------------------------------------------- /lib/lsperfm/defaults/suite/quick.rb: -------------------------------------------------------------------------------- 1 | # format description: 2 | # each test can be executed by either target duration using :time => N secs 3 | # or by number of events with :events => N 4 | # 5 | #[ 6 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 30}, 7 | # {:name => "simple json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :events => 50000}, 8 | #] 9 | # 10 | [ 11 | {:name => "simple line in/out", :config => "config/simple.conf", :input => "input/simple_10.txt", :time => 30}, 12 | {:name => "simple line in/json out", :config => "config/simple_json_out.conf", :input => "input/simple_10.txt", :time => 30}, 13 | {:name => "json codec in/out", :config => "config/json_inout_codec.conf", :input => "input/json_medium.txt", :time => 30}, 14 | {:name => "line in/json filter/json out", :config => "config/json_inout_filter.conf", :input => "input/json_medium.txt", :time => 30}, 15 | {:name => "apache in/json out", :config => "config/simple.conf", :input => "input/apache_log.txt", :time => 30}, 16 | {:name => "apache in/grok codec/json out", :config => "config/simple_grok.conf", :input => "input/apache_log.txt", :time => 30}, 17 | {:name => "syslog in/json out", :config => "config/complex_syslog.conf", :input => "input/syslog_acl_10.txt", :time => 30}, 18 | ] 19 | -------------------------------------------------------------------------------- /lib/lsperfm/version.rb: -------------------------------------------------------------------------------- 1 | module LogStash 2 | module PerformanceMeter 3 | VERSION='0.1.1' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /logstash-perftool.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/lsperfm/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Pere Urbon-Bayes"] 6 | gem.email = ["pere.urbon@elastic.co"] 7 | gem.description = %q{A performance testing tool for Logstash} 8 | gem.summary = %q{With this gem you can run core performance test for your logstash-core pipeline} 9 | gem.homepage = "http://logstash.net/" 10 | gem.license = "Apache License (2.0)" 11 | 12 | gem.files = Dir.glob(["logstash-perftool.gemspec", "lib/**/*", "spec/**/*.rb", "bin/*"]) 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "logstash-perftool" 15 | gem.require_paths = ["lib"] 16 | gem.version = LogStash::PerformanceMeter::VERSION 17 | 18 | gem.executables = ["lsperfm", "lsperfm-deps"] 19 | 20 | gem.add_development_dependency "bundler", "~> 1.7" 21 | gem.add_development_dependency "rake", "~> 10.0" 22 | gem.add_development_dependency "rspec", '~> 3.3', '>= 3.3.0' #(MIT license) 23 | 24 | end 25 | -------------------------------------------------------------------------------- /scripts/loader.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'elasticsearch' 4 | require 'csv' 5 | 6 | class Loader 7 | 8 | def initialize(dir = "", debug = false) 9 | @dir = dir 10 | @debug = debug 11 | end 12 | 13 | def run 14 | Dir.entries(@dir).each do |file| 15 | absolute_path = File.join(@dir, file) 16 | next if File.directory?(absolute_path) 17 | load_file!(client, absolute_path) 18 | end 19 | end 20 | 21 | def create_index 22 | build_index client, index_config 23 | end 24 | 25 | private 26 | 27 | def client 28 | @client ||= Elasticsearch::Client.new log: @debug 29 | end 30 | 31 | def load_file!(client, file) 32 | props = scrap_props(file) 33 | puts "file: #{file} props: #{props}" if @debug 34 | CSV.foreach(file, :headers => true) do |row| 35 | puts "#{row.class}, #{row.count}, #{row.headers}" if @debug 36 | row.headers.each do |header| 37 | next if row.headers.first == header 38 | content = build_body(row, header, props) 39 | id = "#{props[:time]}#{props[:class]}#{content["type"]}#{content["kpi"]}".hash 40 | client.index(index: 'logstash-benchmark', type: 'bench', id: id, body: content) rescue puts "failure with #{row}" 41 | end 42 | end 43 | puts if @debug 44 | end 45 | 46 | def build_body(row, header, props) 47 | type = row[0] 48 | kpi = header 49 | { 50 | "class" => props[:class], 51 | "type" => type, 52 | "ts" => timestamp(props[:time]), 53 | "kpi" => kpi, 54 | "times" => row[header].to_i, 55 | "_source" => "script" 56 | } 57 | end 58 | 59 | def timestamp(time) 60 | Time.at(time.to_i).strftime("%Y-%m-%dT%H:%M:%S.%3N%z") 61 | end 62 | 63 | def scrap_props(file) 64 | match = /-(\d*.\d*)_(\d*).csv/.match(file) 65 | {:class => match[1], :time => match[2] } 66 | end 67 | 68 | def build_index(client, params) 69 | client.indices.create index: 'logstash-benchmark', body: params 70 | end 71 | 72 | def index_config 73 | {:settings => index_settings, :mappings => index_mappings } 74 | end 75 | 76 | def index_settings 77 | { analysis: {analyzer: { 78 | label: { 79 | stopwords: '_none_', 80 | type: 'standard' 81 | } 82 | }}} 83 | end 84 | 85 | def index_mappings 86 | props = { "name" => { "type" => "string" }, 87 | "class" => { "type" => "string" }, 88 | "type" => { "type" => "string", "index" => "not_analyzed" }, 89 | "kpi" => { "type" => "string", "index" => "not_analyzed" }, 90 | "ts" => { "type" => "date", "format" => "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "index" => "analyzed" }, 91 | "times" => { "type" => "integer" } } 92 | { 'bench' => { '_source' => { 'enabled' => true }, 'properties' => props } } 93 | end 94 | 95 | end 96 | 97 | @debug = !!ENV['DEBUG'] 98 | 99 | ## main function 100 | if __FILE__ == $0 101 | mode = ARGV[0] 102 | if "i" == mode 103 | puts "Loading the index definition" 104 | Loader.new.create_index 105 | puts "done" 106 | elsif "m" == mode 107 | puts "Loading data form the directory" 108 | loader = Loader.new(ARGV[1], @debug) 109 | loader.run 110 | puts "done" 111 | else 112 | raise Exception.new("IllegalArgument: USAGE: loader [m|i] [path]") 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /scripts/runner.sh: -------------------------------------------------------------------------------- 1 | export PATH="$HOME/.rbenv/bin:$PATH" 2 | eval "$(rbenv init -)" 3 | 4 | TODAY=`date +%s` 5 | DEST_DIR="workspace" 6 | mkdir -p $DEST_DIR 7 | 8 | ## Bootstrapping the environment 9 | gem install logstash-perftool 10 | cd $DEST_DIR 11 | 12 | ## Setup the current codebase 13 | rm -rf "logstash" 14 | git clone git@github.com:elastic/logstash.git 15 | echo "jruby-1.7.20" > "logstash/.ruby-version" 16 | cd logstash 17 | gem install logstash-perftool 18 | 19 | ## Running the benchmarks for each logstash repository branch 20 | ## of interest. 21 | if [ ! -z $3 ] 22 | then 23 | BRANCHES=$3 24 | else 25 | BRANCHES=(master 1.5 2.0 2.1) 26 | fi 27 | 28 | for BRANCH in "${BRANCHES[@]}" 29 | do 30 | git checkout Gemfile 31 | git checkout Gemfile.jruby-1.9.lock 32 | git checkout $BRANCH 33 | rm -rf vendor 34 | rake bootstrap 35 | lsperfm-deps 36 | lsperfm > "../logstash-$BRANCH-$TODAY.log" 37 | done 38 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | unset CDPATH 4 | 5 | TODAY=`date +%s` 6 | 7 | if [ ! -z $1 ] 8 | then 9 | DEST_DIR=$1 10 | else 11 | DEST_DIR="workspace" 12 | fi 13 | 14 | if [ ! -z $2 ] 15 | then 16 | VERSIONS=$2 17 | else 18 | VERSIONS=( 1.4.2 1.5.0 1.5.1 1.5.2 ) 19 | fi 20 | 21 | mkdir -p $DEST_DIR 22 | 23 | ## Download released versions. 24 | 25 | for VERSION in "${VERSIONS[@]}" 26 | do 27 | 28 | FILENAME="logstash-$VERSION.tar.gz" 29 | SOURCE_FILE="$DEST_DIR/$FILENAME" 30 | DOWNLOAD_URL="https://download.elasticsearch.org/logstash/logstash/$FILENAME" 31 | 32 | if [ ! -f $SOURCE_FILE ]; then 33 | echo "Downloading $DOWNLOAD_URL" 34 | wget $DOWNLOAD_URL -O $SOURCE_FILE 35 | tar -xzf $SOURCE_FILE -C $DEST_DIR 36 | cd $DEST_DIR 37 | echo "jruby-1.7.20" > "logstash-$VERSION/.ruby-version" 38 | cd - 39 | fi 40 | done 41 | 42 | 43 | ## Bootstrapping the environment 44 | gem install logstash-perftool 45 | rbenv rehash 46 | 47 | cd $DEST_DIR 48 | ## Run the report for each download package 49 | for VERSION in "${VERSIONS[@]}" 50 | do 51 | cd "logstash-$VERSION" 52 | lsperfm '' $PWD > "../logstash-$VERSION-$TODAY.log" 53 | cd .. 54 | done 55 | 56 | ## Setup the current codebase 57 | rm -rf "logstash" 58 | git clone git@github.com:elastic/logstash.git 59 | echo "jruby-1.7.20" > "logstash/.ruby-version" 60 | 61 | cd logstash 62 | 63 | ## Running the benchmarks for each logstash repository branch 64 | ## of interest. 65 | if [ ! -z $3 ] 66 | then 67 | BRANCHES=$3 68 | else 69 | BRANCHES=(master 1.5) 70 | fi 71 | 72 | for BRANCH in "${BRANCHES[@]}" 73 | do 74 | git checkout Gemfile 75 | git checkout Gemfile.jruby-1.9.lock 76 | git checkout $BRANCH 77 | rm -rf vendor 78 | rake bootstrap 79 | lsperfm-deps 80 | lsperfm > "../logstash-branch-$BRANCH-$TODAY.log" 81 | done 82 | -------------------------------------------------------------------------------- /spec/fixtures/basic_suite.rb: -------------------------------------------------------------------------------- 1 | [ 2 | {:name => "simple 1", :config => "simple.conf", :input => "simple_10.txt", :time => 5}, 3 | {:name => "simple 2", :config => "simple.conf", :input => "simple_10.txt", :time => 10}, 4 | ] 5 | -------------------------------------------------------------------------------- /spec/fixtures/config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | path: 'spec/' 3 | config: 'fixtures' 4 | input: 'fixtures' 5 | -------------------------------------------------------------------------------- /spec/fixtures/simple.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => line } 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixtures/simple_10.txt: -------------------------------------------------------------------------------- 1 | test 01 2 | test 02 3 | test 03 4 | test 04 5 | test 05 6 | test 06 7 | test 07 8 | test 08 9 | test 09 10 | test 10 -------------------------------------------------------------------------------- /spec/fixtures/wrong_config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | path: 'spec/' 3 | config: 'wrong_fixture' 4 | input: 'wrong_fixture' 5 | -------------------------------------------------------------------------------- /spec/lib/runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LogStash::PerformanceMeter::Runner do 4 | 5 | let(:config) { 'spec/fixtures/simple.conf' } 6 | let(:lines) { load_fixture('simple_10.txt').split("\n") } 7 | 8 | let(:events) { 30 } 9 | let(:time) { 10 } 10 | 11 | subject (:runner) { LogStash::PerformanceMeter::Runner.new(config) } 12 | 13 | let(:command) { [File.join(Dir.pwd, LogStash::PerformanceMeter::Runner::LOGSTASH_BIN), "-f", "spec/fixtures/simple.conf"]} 14 | 15 | it "invokes the logstash command" do 16 | Open3.should_receive(:popen3).with(*command).and_return(true) 17 | runner.run(events, 0, lines) 18 | end 19 | 20 | context "#execution with number of events" do 21 | 22 | let(:io) { double("io", :puts => true, :flush => true) } 23 | subject(:feed) { runner.feed_input_with(events, 0, lines, io) } 24 | 25 | it "feeds in terms of events" do 26 | expect(runner).to receive(:feed_input_events).with(io, events, lines, LogStash::PerformanceMeter::Runner::LAST_MESSAGE) 27 | runner.feed_input_with(events, 0, lines, io) 28 | end 29 | 30 | it "feed the input stream with events.size+1 events" do 31 | expect(feed).to eq(events.to_i) 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/suite_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LogStash::PerformanceMeter::Core do 4 | 5 | let(:config) { 'spec/fixtures/config.yml' } 6 | let(:logstash_home) { '.' } 7 | let(:suite_def) { 'spec/fixtures/basic_suite.rb' } 8 | let(:serial_runner) { double('DummySerialRunner') } 9 | let(:runner) { LogStash::PerformanceMeter::Runner } 10 | 11 | let(:run_outcome) { { :percentile => [2000] , :elapsed => 100, :events_count => 3000, :start_time => 12 } } 12 | subject(:manager) { LogStash::PerformanceMeter::Core.new(suite_def, logstash_home, config, runner) } 13 | 14 | context "with a valid configuration" do 15 | before(:each) do 16 | expect(serial_runner).to receive(:run).with(0, 5, anything()).ordered { run_outcome } 17 | expect(serial_runner).to receive(:run).with(0, 10, anything()).ordered { run_outcome } 18 | end 19 | context "using a file" do 20 | 21 | it "run each test case in a serial maner" do 22 | expect(runner).to receive(:new).with("spec/fixtures/simple.conf", false, logstash_home).twice { serial_runner } 23 | manager.run 24 | end 25 | 26 | end 27 | 28 | context "without a file" do 29 | 30 | let(:config) { '' } 31 | 32 | it "run each test case as expected" do 33 | expect(runner).to receive(:read_input_file).with('simple_10.txt').twice { [] } 34 | expect(runner).to receive(:new).with("simple.conf", false, logstash_home).twice { serial_runner } 35 | manager.run 36 | end 37 | 38 | end 39 | end 40 | 41 | context "with a wrong configuration" do 42 | 43 | let(:config) { 'spec/fixtures/wrong_config.yml' } 44 | 45 | it "run each test case as expected" do 46 | expect(runner).to receive(:new).with("spec/wrong_fixture/simple.conf", false, logstash_home).once { serial_runner } 47 | expect { manager.run }.to raise_error(LogStash::PerformanceMeter::ConfigException) 48 | end 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')) 2 | $LOAD_PATH.unshift File.join(ROOT, 'lib') 3 | $LOAD_PATH.unshift File.join(ROOT, 'spec') 4 | 5 | require 'lsperfm' 6 | 7 | def load_fixture(name) 8 | IO.read("spec/fixtures/#{name}") 9 | end 10 | -------------------------------------------------------------------------------- /suite.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require_relative 'run' 4 | 5 | RUNNER = File.join(File.expand_path(File.dirname(__FILE__)), "run.rb") 6 | BASE_DIR = File.expand_path(File.dirname(__FILE__)) 7 | 8 | ## script main 9 | 10 | if ARGV.size < 1 or ARGV.size > 2 11 | $stderr.puts("usage: ruby suite.rb [suite file] [logstash path]") 12 | exit(1) 13 | end 14 | 15 | @debug = !!ENV["DEBUG"] 16 | 17 | logstash_home = ENV['LOGSTASH_HOME'] || 'logstash' 18 | install_path = ARGV.size > 1 ? ARGV[1] : logstash_home 19 | 20 | tests = eval(IO.read(ARGV[0])) 21 | lines = ["name, #{Runner.headers.join(',')}"] 22 | first = true 23 | 24 | reporter = Thread.new do 25 | loop do 26 | $stderr.print "." 27 | sleep 1 28 | end 29 | end 30 | 31 | tests.each do |test| 32 | 33 | events = test[:events].to_i # total number of events to feed, independant of input file size 34 | time = test[:time].to_i 35 | config = File.join(BASE_DIR, test[:config]) 36 | input = File.join(BASE_DIR, test[:input]) 37 | 38 | runner = Runner.new(config, @debug, install_path) 39 | p, elaspsed, events_count = runner.run(events, time, runner.read_input_file(input)) 40 | 41 | lines << "#{test[:name]}, #{"%.2f" % elaspsed}, #{events_count}, #{"%.0f" % (events_count / elaspsed)},#{p.last}, #{"%.0f" % (p.reduce(:+) / p.size)}" 42 | first = false 43 | end 44 | 45 | reporter.kill 46 | puts lines.join("\n") 47 | -------------------------------------------------------------------------------- /web/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | task :default do 4 | system 'rake --tasks' 5 | end 6 | 7 | desc "Run tests for UI and API" 8 | task :test do 9 | puts '='*80, "Running tests for the API", '='*80 10 | sh "cd api && rake test" 11 | puts '='*80, "Running tests for the UI", '='*80 12 | sh "cd ui && rake test" 13 | end 14 | -------------------------------------------------------------------------------- /web/api/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'sinatra' 5 | gem 'elasticsearch' 6 | gem 'elasticsearch-dsl' 7 | gem 'multi_json' 8 | gem 'jbuilder' 9 | gem 'sidekiq' 10 | gem 'puma' 11 | 12 | group :test do 13 | gem 'mocha', require: false 14 | gem 'shoulda-context', require: false 15 | gem 'rack-test' 16 | gem 'minitest-reporters' 17 | gem 'simplecov', require: false 18 | end 19 | -------------------------------------------------------------------------------- /web/api/README.markdown: -------------------------------------------------------------------------------- 1 | # Logstash benchmarks API 2 | 3 | This application provides an HTTP API for the continuous benchmarks 4 | for the [Logstash](https://www.elastic.co/products/logstash) project. 5 | 6 | ## Endpoints 7 | 8 | * `events.json` returns information about the throughput for each measured version and configuration 9 | 10 | * `startup_time.json` returns information about the startup time for each measured version 11 | 12 | ## Configuration 13 | 14 | Export the `ELASTICSEARCH_URL` variable to point to the Elasticsearch cluster containing 15 | the saved measurements. If you use shield is good enought to pass the 16 | user and password as URL parameters, see: 17 | 18 | ELASTICSEARCH_URL="http://my:pass@localhost:9200" 19 | 20 | as example. 21 | 22 | You can configure the `Microsite::Runner` class with environment variables as well: 23 | 24 | * LSPERF_RUNNER_RELEASES 25 | * LSPERF_RUNNER_BRANCHES 26 | * LSPERF_RUNNER_WORKSPACE 27 | * LSPERF_RUNNER_REPO 28 | * LSPERF_RUNNER_RUBY 29 | * LSPERF_RUNNER_GEMSET 30 | * LSPERF_RUNNER_SETUP 31 | 32 | Also you can configure the config manager with 33 | 34 | * LSPERF_CONFIG : Location of the config file to be managed. 35 | * LSPERF_GITPATH : Local git clone path. 36 | 37 | ----- 38 | 39 | (c) 2016 Elastic.co 40 | -------------------------------------------------------------------------------- /web/api/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 4 | 5 | task :default do 6 | system 'rake --tasks' 7 | end 8 | 9 | require 'rake/testtask' 10 | Rake::TestTask.new(:test) do |test| 11 | test.libs << 'test' 12 | test.test_files = FileList['test/**/*_test.rb'] 13 | test.verbose = true 14 | # test.warning = true 15 | end 16 | -------------------------------------------------------------------------------- /web/api/config.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env puma 2 | 3 | # The directory to operate out of. 4 | # 5 | # The default is the current directory. 6 | # 7 | # directory '/u/apps/lolcat' 8 | 9 | # Use an object or block as the rack application. This allows the 10 | # config file to be the application itself. 11 | # 12 | # app do |env| 13 | # puts env 14 | # 15 | # body = 'Hello, World!' 16 | # 17 | # [200, { 'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s }, [body]] 18 | # end 19 | 20 | # Load "path" as a rackup file. 21 | # 22 | # The default is "config.ru". 23 | # 24 | # rackup '/u/apps/lolcat/config.ru' 25 | 26 | # Set the environment in which the rack's app will run. The value must be a string. 27 | # 28 | # The default is "development". 29 | # 30 | # environment 'production' 31 | 32 | # Daemonize the server into the background. Highly suggest that 33 | # this be combined with "pidfile" and "stdout_redirect". 34 | # 35 | # The default is "false". 36 | # 37 | # daemonize 38 | daemonize true 39 | 40 | # Store the pid of the server in the file at "path". 41 | # 42 | pidfile './pids/puma.pid' 43 | 44 | # Use "path" as the file to store the server info state. This is 45 | # used by "pumactl" to query and control the server. 46 | # 47 | # state_path '/u/apps/lolcat/tmp/pids/puma.state' 48 | 49 | # Redirect STDOUT and STDERR to files specified. The 3rd parameter 50 | # ("append") specifies whether the output is appended, the default is 51 | # "false". 52 | # 53 | # stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr' 54 | # stdout_redirect './log/stdout', './log/stderr', true 55 | 56 | 57 | # Disable request logging. 58 | # 59 | # The default is "false". 60 | # 61 | # quiet 62 | 63 | # Configure "min" to be the minimum number of threads to use to answer 64 | # requests and "max" the maximum. 65 | # 66 | # The default is "0, 16". 67 | # 68 | # threads 0, 16 69 | 70 | # Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only 71 | # accepted protocols. 72 | # 73 | # The default is "tcp://0.0.0.0:9292". 74 | # 75 | bind 'tcp://0.0.0.0:9292' 76 | # bind 'unix:///var/run/puma.sock' 77 | # bind 'unix:///var/run/puma.sock?umask=0111' 78 | # bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert' 79 | 80 | # Instead of "bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'" you 81 | # can also use the "ssl_bind" option. 82 | # 83 | # ssl_bind '127.0.0.1', '9292', { key: path_to_key, cert: path_to_cert } 84 | 85 | # Code to run before doing a restart. This code should 86 | # close log files, database connections, etc. 87 | # 88 | # This can be called multiple times to add code each time. 89 | # 90 | # on_restart do 91 | # puts 'On restart...' 92 | # end 93 | 94 | # Command to use to restart puma. This should be just how to 95 | # load puma itself (ie. 'ruby -Ilib bin/puma'), not the arguments 96 | # to puma, as those are the same as the original process. 97 | # 98 | # restart_command '/u/app/lolcat/bin/restart_puma' 99 | 100 | # === Cluster mode === 101 | 102 | # How many worker processes to run. 103 | # 104 | # The default is "0". 105 | # 106 | # workers 2 107 | 108 | # Code to run when a worker boots to setup the process before booting 109 | # the app. 110 | # 111 | # This can be called multiple times to add hooks. 112 | # 113 | # on_worker_boot do 114 | # puts 'On worker boot...' 115 | # end 116 | 117 | # Code to run when a worker boots to setup the process after booting 118 | # the app. 119 | # 120 | # This can be called multiple times to add hooks. 121 | # 122 | # after_worker_boot do 123 | # puts 'After worker boot...' 124 | # end 125 | 126 | # Code to run when a worker shutdown. 127 | # 128 | # 129 | # on_worker_shutdown do 130 | # puts 'On worker shutdown...' 131 | # end 132 | 133 | # Allow workers to reload bundler context when master process is issued 134 | # a USR1 signal. This allows proper reloading of gems while the master 135 | # is preserved across a phased-restart. (incompatible with preload_app) 136 | # (off by default) 137 | 138 | # prune_bundler 139 | 140 | # Preload the application before starting the workers; this conflicts with 141 | # phased restart feature. (off by default) 142 | 143 | # preload_app! 144 | 145 | # Additional text to display in process listing 146 | # 147 | # tag 'app name' 148 | # 149 | # If you do not specify a tag, Puma will infer it. If you do not want Puma 150 | # to add a tag, use an empty string. 151 | 152 | # Change the default timeout of workers 153 | # 154 | # worker_timeout 60 155 | 156 | # === Puma control rack application === 157 | 158 | # Start the puma control rack application on "url". This application can 159 | # be communicated with to control the main server. Additionally, you can 160 | # provide an authentication token, so all requests to the control server 161 | # will need to include that token as a query parameter. This allows for 162 | # simple authentication. 163 | # 164 | # Check out https://github.com/puma/puma/blob/master/lib/puma/app/status.rb 165 | # to see what the app has available. 166 | # 167 | # activate_control_app 'unix:///var/run/pumactl.sock' 168 | # activate_control_app 'unix:///var/run/pumactl.sock', { auth_token: '12345' } 169 | # activate_control_app 'unix:///var/run/pumactl.sock', { no_token: true } 170 | -------------------------------------------------------------------------------- /web/api/config.ru: -------------------------------------------------------------------------------- 1 | ROOT = File.expand_path(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift File.join(ROOT, 'lib') 3 | Dir.glob('lib/**').each{ |d| $LOAD_PATH.unshift(File.join(ROOT, d)) } 4 | 5 | 6 | env = ENV.fetch('RACK_ENV', :development).to_sym 7 | 8 | require 'bundler' 9 | begin 10 | Bundler.setup(:default, env) 11 | rescue Bundler::BundlerError => e 12 | $stderr.puts e.message 13 | $stderr.puts "Run `bundle install` to install missing gems" 14 | exit e.status_code 15 | end 16 | 17 | require 'application' 18 | 19 | run Application 20 | -------------------------------------------------------------------------------- /web/api/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | branches: 3 | - "master" 4 | - "5.0" 5 | - "2.x" 6 | - "2.4" 7 | - "2.3" 8 | - "2.2" 9 | prs: 10 | - "5560" 11 | -------------------------------------------------------------------------------- /web/api/lib/app/decorator.rb: -------------------------------------------------------------------------------- 1 | module Microsite 2 | 3 | class Decorator 4 | 5 | def self.as_event_list(data, versions) 6 | list = Hash.new([]) 7 | data["aggregations"]["tests"]["buckets"].each do |bucket| 8 | test_case = bucket["key"] 9 | events = [] 10 | bucket["timestamps"]["buckets"].each do |event| 11 | hash = {} 12 | hash["time"] = event["key_as_string"] 13 | hash["values"] = versions.inject(Hash.new(0)) do |acc, v| 14 | acc[v] = 0 15 | acc 16 | end 17 | event["versions"]["buckets"].each do |version| 18 | hash["values"][version["key"]] = version["stats"]["avg"] 19 | end 20 | events << hash 21 | end 22 | list[test_case] = events 23 | end 24 | list 25 | end 26 | 27 | def self.as_chart(es) 28 | 29 | fills = [ "#FFF1AB", "#FFD2AB", "#FFE9AB", "#A2F2E9", "#F9A3C9", "#BD7DEC" ] 30 | stroke = [ "#7B59DC", "#5392D8", "#66F9AB", "#FF9D54", "#C1F09B", "#FFC5A5" ] 31 | 32 | data = { 'labels' => [], 'datasets' => []} 33 | datasets = {} 34 | es['aggregations']['timestamps']['buckets'].each_with_index do |bucket, index| 35 | data['labels'] << bucket['key_as_string'] 36 | bucket['test_cases']['buckets'].each do |sbucket| 37 | datasets[sbucket['key']] ||= [] 38 | datasets[sbucket['key']][index] = sbucket['stats']['avg'] 39 | end 40 | end 41 | i = 0 42 | datasets.each_pair do |label, values| 43 | values.map! { |value| value.to_i } 44 | colors = { :fill => fills[i%fills.size], :stroke => stroke[i%stroke.size] } 45 | data['datasets'] << build_dataset(label,values, colors) 46 | i=i+1 47 | end 48 | data 49 | end 50 | 51 | private 52 | 53 | def self.build_dataset(label, values, colors) 54 | { 55 | :label => label, 56 | :fillColor => "rgba(220,220,220,0)", 57 | :strokeColor => colors[:stroke], 58 | :pointColor => colors[:stroke], 59 | :pointStrokeColor => colors[:stroke], 60 | :pointHighlightFill => "#fff", 61 | :pointHighlightStroke => colors[:stroke], 62 | :data => values 63 | } 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /web/api/lib/app/fetcher.rb: -------------------------------------------------------------------------------- 1 | require "elasticsearch" 2 | require "elasticsearch-dsl" 3 | 4 | module Microsite 5 | class Fetcher 6 | include Elasticsearch::DSL 7 | 8 | attr_reader :time_frame 9 | 10 | def initialize(type="") 11 | @time_frame = "now-90d" 12 | @query_method = method("query_#{type}".to_sym) 13 | end 14 | 15 | def self.fetch(type="") 16 | fetcher = self.new(type) 17 | fetcher.query 18 | end 19 | 20 | def self.find_versions 21 | fetcher = self.new("events") 22 | fetcher.find_versions 23 | end 24 | 25 | def query(params=nil) 26 | definition = (params.nil? ? @query_method.call : @query_method.call(params)) 27 | client_search definition 28 | end 29 | 30 | def find_versions 31 | tests = client_search(query_bundles) 32 | tests["aggregations"]["series"]["buckets"].map do |bucket| 33 | bucket["key"] 34 | end 35 | end 36 | 37 | private 38 | 39 | def query_events(versions="master") 40 | search do 41 | query do 42 | filtered do 43 | query do 44 | match label: versions 45 | end 46 | filter do 47 | range :@timestamp do 48 | gte 'now-90d' 49 | end 50 | end 51 | end 52 | end 53 | aggregation :tests do 54 | terms do 55 | field 'name.raw' 56 | 57 | aggregation :timestamps do 58 | date_histogram do 59 | field '@timestamp' 60 | interval 'day' 61 | format 'yyyy-MM-dd' 62 | 63 | aggregation :versions do 64 | terms do 65 | field 'label.raw' 66 | 67 | aggregation :stats do 68 | stats do 69 | field 'events' 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | size 0 79 | end 80 | end 81 | 82 | def query_start_time(versions="master") 83 | search do 84 | query do 85 | filtered do 86 | query do 87 | match label: versions 88 | end 89 | filter do 90 | range :@timestamp do 91 | gte 'now-90d' 92 | end 93 | end 94 | end 95 | end 96 | 97 | aggregation :timestamps do 98 | date_histogram do 99 | field '@timestamp' 100 | interval 'day' 101 | format 'yyyy-MM-dd' 102 | 103 | aggregation :test_cases do 104 | terms do 105 | field 'label.raw' 106 | size 10 107 | 108 | aggregation :stats do 109 | stats do 110 | field 'start time' 111 | end 112 | end 113 | end 114 | end 115 | end 116 | end 117 | 118 | size 0 119 | end 120 | end 121 | 122 | def query_tests 123 | search do 124 | aggregation :series do 125 | terms do 126 | field 'name.raw' 127 | size 10 128 | end 129 | end 130 | size 0 131 | end 132 | end 133 | 134 | def query_bundles 135 | search do 136 | query do 137 | match "tags" => 'branch' 138 | end 139 | aggregation :series do 140 | terms field: "label.raw", order: { _term: "desc" }, size: 5 141 | end 142 | size 0 143 | end 144 | end 145 | 146 | def client_search(body) 147 | client.search(:index => "logstash-*", :body => body.to_hash) 148 | end 149 | 150 | def client 151 | @client ||= Elasticsearch::Client.new log: @debug 152 | end 153 | 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /web/api/lib/application.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'sidekiq' 3 | require 'json' 4 | 5 | require 'app/fetcher' 6 | require 'app/decorator' 7 | 8 | require "workers/config_worker" 9 | require "workers/test_worker" 10 | 11 | class Application < Sinatra::Application 12 | 13 | set :protection, except: :path_traversal 14 | 15 | before do 16 | headers( 17 | 'Access-Control-Allow-Origin' => '*', 18 | 'Access-Control-Allow-Methods' => [:post, :get, :options], 19 | 'Access-Control-Allow-Headers' => ["*", "Content-Type", "Accept", "AUTHORIZATION", "Cache-Control"].join(', ') 20 | ) 21 | end 22 | 23 | get "/" do 24 | prefix = request.env['HTTP_X_PROXY_CLIENT'] == 'nginx' ? '/api' : '' 25 | host = "#{request.scheme}://#{request.host_with_port}" 26 | respond_with events_url: "#{host}#{prefix}/events.json", 27 | startup_time_url: "#{host}#{prefix}/startup_time.json" 28 | end 29 | 30 | #get the events stored for a given period of time 31 | get "/events.json" do 32 | fetcher = Microsite::Fetcher.new("events") 33 | versions = Microsite::Fetcher.find_versions 34 | data = fetcher.query(versions.join(' ')) 35 | events = Microsite::Decorator.as_event_list(data, versions) 36 | respond_with(events) 37 | end 38 | 39 | # gets you the startup time for a given period of time 40 | get "/startup_time.json" do 41 | fetcher = Microsite::Fetcher.new("start_time") 42 | versions = Microsite::Fetcher.find_versions 43 | data = fetcher.query(versions.join(' ')) 44 | events = Microsite::Decorator.as_chart(data) 45 | respond_with(events) 46 | end 47 | 48 | get "/bundles.json" do 49 | data = Microsite::Fetcher.fetch("bundles") 50 | respond_with(data) 51 | end 52 | 53 | get "/tests.json" do 54 | data = Microsite::Fetcher.fetch("tests") 55 | respond_with(data) 56 | end 57 | 58 | # hook that lets you run the api process 59 | post "/hook/pull" do 60 | body = JSON.parse(request.body.read) 61 | Microsite::TestWorker.perform_async('pull_hook', body) 62 | end 63 | 64 | # hook that runs the configuration update process 65 | post "/hook/pull_config" do 66 | body = JSON.parse(request.body.read) 67 | Microsite::ConfigWorker.perform_async('config_pull_hook', body) 68 | end 69 | 70 | private 71 | 72 | def respond_with(data={}) 73 | halt 404 if data.empty? 74 | data.to_json 75 | end 76 | end 77 | 78 | Application.run! if __FILE__ == $0 79 | -------------------------------------------------------------------------------- /web/api/lib/workers/config_manager.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "json" 3 | require "elasticsearch" 4 | 5 | module Microsite 6 | class ConfManager 7 | 8 | attr_reader :config_file, :git_path 9 | 10 | def initialize 11 | @config_file = ENV.fetch("LSPERF_CONFIG", ::File.join(::File.dirname(__FILE__), "..", "..", "config.yml")) 12 | @git_path = ENV.fetch("LSPERF_GITPATH") 13 | end 14 | 15 | def perform 16 | update_git_repository 17 | update_config 18 | end 19 | 20 | private 21 | 22 | def update_git_repository 23 | system("cd #{git_path}; git pull") 24 | end 25 | 26 | def update_config 27 | payload = ::YAML.load_file(config_file) 28 | client.index index: 'benchmarks-config', type: 'config', id: 1, body: payload 29 | end 30 | 31 | def client 32 | @client ||= ::Elasticsearch::Client.new log: @debug 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /web/api/lib/workers/config_worker.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "config_manager" ) 2 | 3 | module Microsite 4 | 5 | class ConfigWorker 6 | include Sidekiq::Worker 7 | 8 | def perform(name, body) 9 | r = Microsite::ConfManager.new 10 | r.perform 11 | end 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /web/api/lib/workers/runner.rb: -------------------------------------------------------------------------------- 1 | module Microsite 2 | class Runner 3 | 4 | attr_reader :releases, :branches, :workspace, :repo_name 5 | 6 | def initialize 7 | @releases = ENV.fetch('LSPERF_RUNNER_RELEASES').to_s.split(',') 8 | @branches = ENV.fetch('LSPERF_RUNNER_BRANCHES').to_s.split(',') 9 | @workspace = ENV.fetch('LSPERF_RUNNER_WORKSPACE') 10 | @repo_name = ENV.fetch('LSPERF_RUNNER_REPO') 11 | @ruby = ENV.fetch('LSPERF_RUNNER_RUBY') 12 | @gemset = ENV.fetch('LSPERF_RUNNER_GEMSET') 13 | end 14 | 15 | def perform 16 | setup_script = ENV.fetch('LSPERF_RUNNER_SETUP', '/Users/purbon/work/logstash-perf-testing/scripts/setup.sh') 17 | cmd = "#{setup_script} #{workspace}" 18 | system(cmd) 19 | end 20 | 21 | def update_releases 22 | releases.each do |release| 23 | filename = "logstash-#{release}.tar.gz" 24 | download_url = "https://download.elasticsearch.org/logstash/logstash/#{filename}" 25 | source_file = "#{workspace}/#{filename}" 26 | next if File.exist?(source_file) 27 | `wget #{download_url} -O #{source_file}` 28 | `tar -xvzf #{source_file} -C #{workspace}` 29 | install_perftool File.join(workspace, "logstash-#{release}") 30 | end 31 | true 32 | end 33 | 34 | def update_gitrepo 35 | repo_path = File.join(workspace, "logstash") 36 | `rm -rf #{repo_path}` 37 | `git clone #{repo_name} #{repo_path}` 38 | install_perftool repo_path 39 | end 40 | 41 | def install_perftool(base_path) 42 | File.write(File.join(base_path, ".ruby-version"),@ruby) 43 | File.write(File.join(base_path, ".ruby-gemset"),@gemset) 44 | `cd #{base_path}; gem install logstash-perftool; cd -` 45 | end 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /web/api/lib/workers/test_worker.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "runner" ) 2 | 3 | module Microsite 4 | 5 | class TestWorker 6 | include Sidekiq::Worker 7 | 8 | def perform(name, body) 9 | r = Microsite::Runner.new 10 | r.perform 11 | end 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /web/api/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | error_log /tmp/nginx/logs/error.log notice; 6 | 7 | http { 8 | include /opt/nginx/conf/mime.types; 9 | 10 | proxy_cache_path /tmp/nginx/cache keys_zone=LSPERF_API:10m inactive=24h max_size=500m; 11 | 12 | upstream lsperf_api { 13 | server 127.0.0.1:9292; 14 | } 15 | 16 | server { 17 | listen 8080; 18 | server_name logstash-meter.elastic.co; 19 | 20 | location / { 21 | root /tmp/logstash-performance-testing/web/ui; 22 | index index.html; 23 | } 24 | 25 | location /api/ { 26 | proxy_pass http://lsperf_api/; 27 | proxy_read_timeout 300; 28 | proxy_connect_timeout 180; 29 | proxy_redirect off; 30 | 31 | proxy_set_header Client-IP $remote_addr; 32 | proxy_set_header Host $http_host; 33 | proxy_set_header X-Real-IP $remote_addr; 34 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 35 | proxy_set_header X-Forwarded-Proto $scheme; 36 | proxy_set_header X-Proxy-Client nginx; 37 | 38 | proxy_cache LSPERF_API; 39 | proxy_cache_valid 200 302 1h; 40 | proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; 41 | proxy_cache_lock on; 42 | add_header X-Cache-Status $upstream_cache_status; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /web/api/test/application_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'rack/test' 4 | require 'sinatra' 5 | 6 | require File.expand_path('../../lib/application', __FILE__) 7 | 8 | module Microsite 9 | 10 | class ApplicationTest < MiniTest::Test 11 | include Rack::Test::Methods 12 | alias :response :last_response 13 | 14 | def app 15 | Application.new 16 | end 17 | 18 | context "Application" do 19 | setup do 20 | Microsite::Fetcher.stubs(:find_versions) 21 | end 22 | 23 | should "get the list of APIs" do 24 | get '/' 25 | assert response.ok?, response.status.to_s 26 | assert_match %r{http://example.org/events.json}, response.body 27 | end 28 | 29 | should "set the prefix for requests coming from Nginx" do 30 | get '/', {}, { 'HTTP_X_PROXY_CLIENT' => 'nginx' } 31 | assert_match %r{http://example.org/api/events.json}, response.body 32 | end 33 | 34 | should "set CORS headers for Ajax requests" do 35 | get '/', {}, { "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" } 36 | 37 | assert last_request.xhr? 38 | assert response.ok?, response.status.to_s 39 | assert_equal '*', response.headers['Access-Control-Allow-Origin'] 40 | end 41 | 42 | should "get events.json" do 43 | Microsite::Fetcher.expects(:fetch).with('events') 44 | 45 | Microsite::Decorator.expects(:as_event_list) 46 | .returns({ 47 | "apache in/grok codec/json out" => \ 48 | [{"time"=>"2015-10-08", "values"=>{"master"=>6417180.0, "1.5"=>2863740.0, "2.0"=>0, "2.1"=>0}}] 49 | }) 50 | 51 | get '/events.json' 52 | assert response.ok?, response.status.to_s 53 | assert_match %r{"apache in/grok codec/json out"}, response.body 54 | end 55 | 56 | should "get startup_time.json" do 57 | Microsite::Fetcher.expects(:fetch).with('start_time') 58 | 59 | Microsite::Decorator.expects(:as_chart) 60 | .returns({ 61 | "labels" => ["2015-09-23", "2015-09-24", "2015-12-18"], 62 | "datasets" => [ { "label" => "1.5", "data" => [ 3, 2, 1 ] } ] 63 | }) 64 | 65 | get '/startup_time.json' 66 | assert response.ok?, response.status.to_s 67 | assert_match %r{"labels"}, response.body 68 | assert_match %r{"datasets"}, response.body 69 | end 70 | end 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /web/api/test/fetcher_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require File.expand_path('../../lib/app/fetcher', __FILE__) 4 | 5 | module Microsite 6 | 7 | class FetcherTest < MiniTest::Test 8 | 9 | def assert_same_hash(a, b, m=nil) 10 | a = a.is_a?(String) ? MultiJson.load(a) : MultiJson.load(MultiJson.dump(a.to_hash)) 11 | b = b.is_a?(String) ? MultiJson.load(b) : MultiJson.load(MultiJson.dump(b.to_hash)) 12 | assert_equal a, b, m 13 | end 14 | 15 | context "Fetcher" do 16 | should "be initialized with a value" do 17 | assert_raises NameError do 18 | Microsite::Fetcher.new 19 | end 20 | 21 | assert_raises NameError do 22 | Microsite::Fetcher.new 'foobar' 23 | end 24 | 25 | Microsite::Fetcher.new 'events' 26 | end 27 | 28 | context "queries" do 29 | should "return query for events" do 30 | json = <<-JSON 31 | { 32 | "query":{ 33 | "filtered":{ 34 | "filter":{ 35 | "range":{ 36 | "@timestamp":{ 37 | "gte":"now-90d" 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | 44 | "aggregations":{ 45 | "tests":{ 46 | "terms":{ 47 | "field":"name.raw" 48 | }, 49 | "aggregations":{ 50 | "timestamps":{ 51 | "date_histogram":{ 52 | "field":"@timestamp", 53 | "interval":"day", 54 | "format":"yyyy-MM-dd" 55 | }, 56 | "aggregations":{ 57 | "versions":{ 58 | "terms":{ 59 | "field":"label.raw" 60 | }, 61 | "aggregations":{ 62 | "stats":{ 63 | "stats":{ 64 | "field":"events" 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | 75 | "size":0 76 | } 77 | JSON 78 | 79 | fetcher = Microsite::Fetcher.new("events") 80 | 81 | fetcher.__send__(:client).expects(:search).with do |arguments| 82 | assert_same_hash json, arguments[:body] 83 | end 84 | 85 | fetcher.query 86 | end 87 | 88 | should "return query for start time" do 89 | json = <<-JSON 90 | { 91 | "query":{ 92 | "filtered":{ 93 | "filter":{ 94 | "range":{ 95 | "@timestamp":{ 96 | "gte":"now-90d" 97 | } 98 | } 99 | } 100 | } 101 | }, 102 | 103 | "aggregations":{ 104 | "timestamps":{ 105 | "date_histogram":{ 106 | "field":"@timestamp", 107 | "interval":"day", 108 | "format":"yyyy-MM-dd" 109 | }, 110 | "aggregations":{ 111 | "test_cases":{ 112 | "terms":{ 113 | "field":"label.raw", 114 | "size":10 115 | }, 116 | "aggregations":{ 117 | "stats":{ 118 | "stats":{ 119 | "field":"start time" 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | 128 | "size":0 129 | } 130 | JSON 131 | 132 | fetcher = Microsite::Fetcher.new("start_time") 133 | 134 | fetcher.__send__(:client).expects(:search).with do |arguments| 135 | assert_same_hash json, arguments[:body] 136 | end 137 | 138 | fetcher.query 139 | end 140 | 141 | should "return query for tests" do 142 | json = <<-JSON 143 | { 144 | "aggregations":{ 145 | "series":{ 146 | "terms":{ 147 | "field":"name.raw", 148 | "size":10 149 | } 150 | } 151 | }, 152 | "size":0 153 | } 154 | JSON 155 | 156 | fetcher = Microsite::Fetcher.new("tests") 157 | 158 | fetcher.__send__(:client).expects(:search).with do |arguments| 159 | assert_same_hash json, arguments[:body] 160 | end 161 | 162 | fetcher.query 163 | end 164 | 165 | should "return query for bundles" do 166 | json = <<-JSON 167 | { 168 | "aggregations":{ 169 | "series":{ 170 | "terms":{ 171 | "field":"label.raw", 172 | "size":10 173 | } 174 | } 175 | }, 176 | "size":0 177 | } 178 | JSON 179 | 180 | fetcher = Microsite::Fetcher.new("bundles") 181 | 182 | fetcher.__send__(:client).expects(:search).with do |arguments| 183 | assert_same_hash json, arguments[:body] 184 | end 185 | 186 | fetcher.query 187 | end 188 | end 189 | end 190 | end 191 | 192 | end 193 | -------------------------------------------------------------------------------- /web/api/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'minitest/autorun' 5 | require 'shoulda/context' 6 | require 'mocha/setup' 7 | 8 | ENV['RACK_ENV'] = 'test' 9 | 10 | require 'minitest/reporters' 11 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 12 | -------------------------------------------------------------------------------- /web/benchmarks/config/complex_syslog.conf: -------------------------------------------------------------------------------- 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 | } 47 | -------------------------------------------------------------------------------- /web/benchmarks/config/json_inout_codec.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin { codec => "json_lines" } 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /web/benchmarks/config/json_inout_filter.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | json { source => "message" } 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /web/benchmarks/config/simple.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => line } 11 | } 12 | -------------------------------------------------------------------------------- /web/benchmarks/config/simple_grok.conf: -------------------------------------------------------------------------------- 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 | } 14 | -------------------------------------------------------------------------------- /web/benchmarks/config/simple_json_out.conf: -------------------------------------------------------------------------------- 1 | input { 2 | stdin {} 3 | } 4 | 5 | filter { 6 | clone {} 7 | } 8 | 9 | output { 10 | stdout { codec => json_lines } 11 | } 12 | -------------------------------------------------------------------------------- /web/benchmarks/input/apache_log.txt: -------------------------------------------------------------------------------- 1 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-search.png HTTP/1.1" 200 203023 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 2 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard3.png HTTP/1.1" 200 171717 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 3 | 83.149.9.216 - - [17/Sep/2014:07:13:44 +0000] "GET /presentations/logstash-monitorama-2013/plugin/highlight/highlight.js HTTP/1.1" 200 26185 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 4 | 83.149.9.216 - - [17/Sep/2014:07:13:44 +0000] "GET /presentations/logstash-monitorama-2013/plugin/zoom-js/zoom.js HTTP/1.1" 200 7697 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 5 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/plugin/notes/notes.js HTTP/1.1" 200 2892 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 6 | 83.149.9.216 - - [17/Sep/2014:07:13:42 +0000] "GET /presentations/logstash-monitorama-2013/images/sad-medic.png HTTP/1.1" 200 430406 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 7 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/css/fonts/Roboto-Bold.ttf HTTP/1.1" 200 38720 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 8 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/css/fonts/Roboto-Regular.ttf HTTP/1.1" 200 41820 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 9 | 83.149.9.216 - - [17/Sep/2014:07:13:45 +0000] "GET /presentations/logstash-monitorama-2013/images/frontend-response-codes.png HTTP/1.1" 200 52878 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 10 | 83.149.9.216 - - [17/Sep/2014:07:13:43 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard.png HTTP/1.1" 200 321631 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 11 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/Dreamhost_logo.svg HTTP/1.1" 200 2126 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 12 | 83.149.9.216 - - [17/Sep/2014:07:13:43 +0000] "GET /presentations/logstash-monitorama-2013/images/kibana-dashboard2.png HTTP/1.1" 200 394967 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 13 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/apache-icon.gif HTTP/1.1" 200 8095 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 14 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/nagios-sms5.png HTTP/1.1" 200 78075 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 15 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/redis.png HTTP/1.1" 200 25230 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 16 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/elasticsearch.png HTTP/1.1" 200 8026 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 17 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/logstashbook.png HTTP/1.1" 200 54662 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 18 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/github-contributions.png HTTP/1.1" 200 34245 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 19 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/css/print/paper.css HTTP/1.1" 200 4254 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 20 | 83.149.9.216 - - [17/Sep/2014:07:13:47 +0000] "GET /presentations/logstash-monitorama-2013/images/1983_delorean_dmc-12-pic-38289.jpeg HTTP/1.1" 200 220562 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 21 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/simple-inputs-filters-outputs.jpg HTTP/1.1" 200 1168622 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 22 | 83.149.9.216 - - [17/Sep/2014:07:13:46 +0000] "GET /presentations/logstash-monitorama-2013/images/tiered-outputs-to-inputs.jpg HTTP/1.1" 200 1079983 "http://semicomplete.com/presentations/logstash-monitorama-2013/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 23 | 83.149.9.216 - - [17/Sep/2014:07:13:53 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36" 24 | 24.236.252.67 - - [17/Sep/2014:07:14:10 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0" 25 | 93.114.45.13 - - [17/Sep/2014:07:14:32 +0000] "GET /articles/dynamic-dns-with-dhcp/ HTTP/1.1" 200 18848 "http://www.google.ro/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0CCwQFjAB&url=http%3A%2F%2Fwww.semicomplete.com%2Farticles%2Fdynamic-dns-with-dhcp%2F&ei=W88AU4n9HOq60QXbv4GwBg&usg=AFQjCNEF1X4Rs52UYQyLiySTQxa97ozM4g&bvm=bv.61535280,d.d2k" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 26 | 93.114.45.13 - - [17/Sep/2014:07:14:32 +0000] "GET /reset.css HTTP/1.1" 200 1015 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 27 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /style2.css HTTP/1.1" 200 4877 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 28 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 29 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /images/jordan-80.png HTTP/1.1" 200 6146 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 30 | 93.114.45.13 - - [17/Sep/2014:07:14:33 +0000] "GET /images/web/2009/banner.png HTTP/1.1" 200 52315 "http://www.semicomplete.com/style2.css" "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0" 31 | -------------------------------------------------------------------------------- /web/benchmarks/input/json_medium.txt: -------------------------------------------------------------------------------- 1 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 2 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 3 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 4 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 5 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 6 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 7 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 8 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 9 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 10 | {"_scroll_id":"xxx", "took":5, "timed_out":false, "_shards":{"total":15,"successful":15,"failed":0}, "hits":{"total":1000050, "max_score":1.0, "hits":[{"_index":"logstash2", "_type":"logs", "_id":"AmaqL7VuSWKF-F6N_Gz72g", "_score":1.0, "_source" : {"message":"foobar", "@version":"1", "@timestamp":"2014-05-19T21:08:39.000Z", "host":"colin-mbp13r"} } ] } } 11 | -------------------------------------------------------------------------------- /web/benchmarks/input/simple_10.txt: -------------------------------------------------------------------------------- 1 | test 01 2 | test 02 3 | test 03 4 | test 04 5 | test 05 6 | test 06 7 | test 07 8 | test 08 9 | test 09 10 | test 10 -------------------------------------------------------------------------------- /web/benchmarks/input/syslog_acl_10.txt: -------------------------------------------------------------------------------- 1 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 2 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 3 | <164>Oct 1 15:21:25 3.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.5/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 4 | <164>Oct 30 15:22:25 4.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.6/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 5 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 6 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 7 | <164>Oct 1 15:21:25 3.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.5/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 8 | <164>Oct 30 15:22:25 4.2.3.4 %ASA-4-106023: Allow tcp src DRAC:10.1.2.6/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 9 | <164>Oct 26 15:19:25 1.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.3/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 10 | <164>Oct 6 15:20:25 2.2.3.4 %ASA-4-106023: Deny udp src DRAC:10.1.2.4/43434 dst outside:192.168.0.1/53 by access-group "acl_drac" [0x0, 0x0] 11 | -------------------------------------------------------------------------------- /web/benchmarks/suite.rb: -------------------------------------------------------------------------------- 1 | module LogStash::PerformanceMeter 2 | 3 | ## Base path for the benchmarks, will be different for every 4 | ## system this suite is deployed. 5 | base_dir = "/home/purbon/logstash-performance-testing/benchmarks" 6 | 7 | DEFAULT_SUITE = [ { :name => "simple line in/out", :config => "#{base_dir}/config/simple.conf", :input => "#{base_dir}/input/simple_10.txt", :time => 120 }, 8 | { :name => "simple line in/json out", :config => "#{base_dir}/config/simple_json_out.conf", :input => "#{base_dir}/input/simple_10.txt", :time => 120 }, 9 | { :name => "json codec in/out", :config => "#{base_dir}/config/json_inout_codec.conf", :input => "#{base_dir}/input/json_medium.txt", :time => 120 }, 10 | { :name => "line in/json filter/json out", :config => "#{base_dir}/config/json_inout_filter.conf", :input => "#{base_dir}/input/json_medium.txt", :time => 120 }, 11 | { :name => "apache in/json out", :config => "#{base_dir}/config/simple.conf", :input => "#{base_dir}/input/apache_log.txt", :time => 120 }, 12 | { :name => "apache in/grok codec/json out", :config => "#{base_dir}/config/simple_grok.conf", :input => "#{base_dir}/input/apache_log.txt", :time => 120 }, 13 | { :name => "syslog in/json out", :config => "#{base_dir}/config/complex_syslog.conf", :input => "#{base_dir}/input/syslog_acl_10.txt", :time => 120} ] 14 | end 15 | -------------------------------------------------------------------------------- /web/ui/README.markdown: -------------------------------------------------------------------------------- 1 | # Logstash benchmarks UI 2 | 3 | This application provides a UI for the continuous benchmarks 4 | for the [Logstash](https://www.elastic.co/products/logstash) project. 5 | 6 | The visualizations are implemented in [D3.js](http://d3js.org), 7 | and jQuery is used for the interactivity. 8 | 9 | Integration tests using Cucumber and PhantomJS are contained 10 | in the `test` folder. 11 | 12 | ----- 13 | 14 | (c) 2015 Elastic.co 15 | -------------------------------------------------------------------------------- /web/ui/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | task :default do 4 | system 'rake --tasks' 5 | end 6 | 7 | desc "Run server for development" 8 | task :server do 9 | require 'logger' 10 | require 'thin' 11 | 12 | port = ENV['PORT'] || 8000 13 | 14 | logger = Logger.new(STDERR) 15 | logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\e[0m\n" } 16 | 17 | Thin::Logging.logger = logger 18 | Thin::Server.start port, lambda { |env| 19 | if env['PATH_INFO'] == '/' 20 | [200, {'Content-Type' => 'text/html'}, File.new('index.html')] 21 | else 22 | Rack::Directory.new('.').(env) 23 | end 24 | } 25 | end 26 | 27 | desc "Run Cucumber features" 28 | task :test do 29 | sh "cd test && cucumber" 30 | end 31 | -------------------------------------------------------------------------------- /web/ui/data/startup_time.json: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "2015-09-15", 4 | "2015-09-16", 5 | "2015-09-18", 6 | "2015-09-23", 7 | "2015-09-24", 8 | "2015-09-25", 9 | "2015-09-26", 10 | "2015-09-30", 11 | "2015-10-02", 12 | "2015-10-06", 13 | "2015-10-07", 14 | "2015-10-08", 15 | "2015-10-09", 16 | "2015-10-12", 17 | "2015-10-13", 18 | "2015-10-14", 19 | "2015-10-15", 20 | "2015-10-16", 21 | "2015-10-19", 22 | "2015-10-20", 23 | "2015-10-21", 24 | "2015-10-22", 25 | "2015-10-23", 26 | "2015-10-24", 27 | "2015-10-26", 28 | "2015-10-27", 29 | "2015-10-28", 30 | "2015-10-29", 31 | "2015-10-30", 32 | "2015-10-31", 33 | "2015-11-01", 34 | "2015-11-02", 35 | "2015-11-03", 36 | "2015-11-04", 37 | "2015-11-05", 38 | "2015-11-06", 39 | "2015-11-07", 40 | "2015-11-08", 41 | "2015-11-09", 42 | "2015-11-10", 43 | "2015-11-11", 44 | "2015-11-12", 45 | "2015-11-13", 46 | "2015-11-14", 47 | "2015-11-15", 48 | "2015-11-16", 49 | "2015-11-17", 50 | "2015-11-18", 51 | "2015-11-19", 52 | "2015-11-20", 53 | "2015-11-21", 54 | "2015-11-22", 55 | "2015-11-23", 56 | "2015-11-24", 57 | "2015-11-25", 58 | "2015-11-26", 59 | "2015-11-27", 60 | "2015-11-28", 61 | "2015-11-29", 62 | "2015-11-30", 63 | "2015-12-01", 64 | "2015-12-02", 65 | "2015-12-03", 66 | "2015-12-04", 67 | "2015-12-05", 68 | "2015-12-06", 69 | "2015-12-07", 70 | "2015-12-08", 71 | "2015-12-09", 72 | "2015-12-10", 73 | "2015-12-11" 74 | ], 75 | "datasets": [ 76 | { 77 | "label": "1.5", 78 | "fillColor": "rgba(220,220,220,0)", 79 | "strokeColor": "#7B59DC", 80 | "pointColor": "#7B59DC", 81 | "pointStrokeColor": "#7B59DC", 82 | "pointHighlightFill": "#fff", 83 | "pointHighlightStroke": "#7B59DC", 84 | "data": [ 85 | 5, 86 | 3, 87 | 4, 88 | 0, 89 | 0, 90 | 0, 91 | 0, 92 | 0, 93 | 0, 94 | 0, 95 | 0, 96 | 3, 97 | 3, 98 | 3, 99 | 3, 100 | 3, 101 | 3, 102 | 3, 103 | 6, 104 | 3, 105 | 3, 106 | 3, 107 | 3, 108 | 3, 109 | 3, 110 | 3, 111 | 3, 112 | 3, 113 | 3, 114 | 3, 115 | 3, 116 | 3, 117 | 3, 118 | 3, 119 | 3, 120 | 3, 121 | 3, 122 | 3, 123 | 3, 124 | 3, 125 | 3, 126 | 3, 127 | 3, 128 | 3, 129 | 3, 130 | 3, 131 | 3, 132 | 3, 133 | 3, 134 | 3, 135 | 3, 136 | 3, 137 | 3, 138 | 3, 139 | 3, 140 | 3, 141 | 3, 142 | 3, 143 | 3, 144 | 3, 145 | 3, 146 | 3, 147 | 3, 148 | 3, 149 | 3, 150 | 3, 151 | 3, 152 | 3, 153 | 3, 154 | 3, 155 | 3 156 | ] 157 | }, 158 | { 159 | "label": "master", 160 | "fillColor": "rgba(220,220,220,0)", 161 | "strokeColor": "#5392D8", 162 | "pointColor": "#5392D8", 163 | "pointStrokeColor": "#5392D8", 164 | "pointHighlightFill": "#fff", 165 | "pointHighlightStroke": "#5392D8", 166 | "data": [ 167 | 5, 168 | 2, 169 | 4, 170 | 0, 171 | 0, 172 | 0, 173 | 0, 174 | 0, 175 | 0, 176 | 0, 177 | 0, 178 | 3, 179 | 3, 180 | 5, 181 | 4, 182 | 4, 183 | 0, 184 | 3, 185 | 6, 186 | 3, 187 | 3, 188 | 3, 189 | 3, 190 | 3, 191 | 2, 192 | 3, 193 | 3, 194 | 3, 195 | 2, 196 | 3, 197 | 2, 198 | 2, 199 | 3, 200 | 3, 201 | 0, 202 | 0, 203 | 2, 204 | 3, 205 | 2, 206 | 3, 207 | 3, 208 | 3, 209 | 3, 210 | 3, 211 | 3, 212 | 3, 213 | 3, 214 | 3, 215 | 3, 216 | 3, 217 | 3, 218 | 3, 219 | 3, 220 | 3, 221 | 3, 222 | 3, 223 | 3, 224 | 3, 225 | 3, 226 | 3, 227 | 3, 228 | 3, 229 | 3, 230 | 2, 231 | 3, 232 | 3, 233 | 3, 234 | 3, 235 | 2, 236 | 3, 237 | 3 238 | ] 239 | }, 240 | { 241 | "label": "2.0", 242 | "fillColor": "rgba(220,220,220,0)", 243 | "strokeColor": "#66F9AB", 244 | "pointColor": "#66F9AB", 245 | "pointStrokeColor": "#66F9AB", 246 | "pointHighlightFill": "#fff", 247 | "pointHighlightStroke": "#66F9AB", 248 | "data": [ 249 | 0, 250 | 0, 251 | 0, 252 | 0, 253 | 0, 254 | 0, 255 | 0, 256 | 0, 257 | 0, 258 | 0, 259 | 0, 260 | 0, 261 | 0, 262 | 0, 263 | 0, 264 | 0, 265 | 0, 266 | 0, 267 | 0, 268 | 3, 269 | 3, 270 | 3, 271 | 3, 272 | 3, 273 | 3, 274 | 3, 275 | 3, 276 | 3, 277 | 3, 278 | 3, 279 | 3, 280 | 3, 281 | 3, 282 | 3, 283 | 3, 284 | 3, 285 | 3, 286 | 3, 287 | 3, 288 | 3, 289 | 3, 290 | 3, 291 | 3, 292 | 3, 293 | 3, 294 | 3, 295 | 3, 296 | 3, 297 | 3, 298 | 3, 299 | 3, 300 | 3, 301 | 3, 302 | 3, 303 | 3, 304 | 3, 305 | 3, 306 | 3, 307 | 3, 308 | 3, 309 | 3, 310 | 3, 311 | 3, 312 | 3, 313 | 3, 314 | 3, 315 | 3, 316 | 3, 317 | 3, 318 | 3, 319 | 3 320 | ] 321 | }, 322 | { 323 | "label": "2.1", 324 | "fillColor": "rgba(220,220,220,0)", 325 | "strokeColor": "#FF9D54", 326 | "pointColor": "#FF9D54", 327 | "pointStrokeColor": "#FF9D54", 328 | "pointHighlightFill": "#fff", 329 | "pointHighlightStroke": "#FF9D54", 330 | "data": [ 331 | 0, 332 | 0, 333 | 0, 334 | 0, 335 | 0, 336 | 0, 337 | 0, 338 | 0, 339 | 0, 340 | 0, 341 | 0, 342 | 0, 343 | 0, 344 | 0, 345 | 0, 346 | 0, 347 | 0, 348 | 0, 349 | 0, 350 | 3, 351 | 3, 352 | 3, 353 | 3, 354 | 3, 355 | 3, 356 | 3, 357 | 3, 358 | 3, 359 | 3, 360 | 3, 361 | 3, 362 | 3, 363 | 3, 364 | 3, 365 | 3, 366 | 3, 367 | 3, 368 | 3, 369 | 3, 370 | 3, 371 | 3, 372 | 3, 373 | 3, 374 | 3, 375 | 3, 376 | 3, 377 | 3, 378 | 3, 379 | 3, 380 | 3, 381 | 3, 382 | 3, 383 | 3, 384 | 3, 385 | 3, 386 | 3, 387 | 3, 388 | 3, 389 | 3, 390 | 3, 391 | 3, 392 | 3, 393 | 3, 394 | 3, 395 | 3, 396 | 3, 397 | 3, 398 | 3, 399 | 3, 400 | 3, 401 | 3 402 | ] 403 | } 404 | ] 405 | } 406 | -------------------------------------------------------------------------------- /web/ui/images/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elastic/logstash-performance-testing/b577a48503057b455b61811586327d6867d66f1c/web/ui/images/loading.png -------------------------------------------------------------------------------- /web/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logstash Benchmarks 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Logstash Benchmarks

14 |

Displaying the number of events Logstash is able to generate when running for two minutes.

15 |

16 |
17 |
18 | 19 |
20 |
21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/ui/javascripts/application.js: -------------------------------------------------------------------------------- 1 | var App = App || {} 2 | 3 | App.run = function() { 4 | 5 | if (document.location.host == 'localhost:8000') { 6 | App.host = './data/' 7 | } else { 8 | App.host = '/api/' 9 | } 10 | 11 | var configuration = decodeURIComponent( document.location.hash.substr(document.location.hash.indexOf('#')+1) ); 12 | 13 | App.__load_startup_time(); 14 | App.__load_main_chart(configuration); 15 | App.__load_matrix_chart(configuration); 16 | 17 | App.Dispatcher(); 18 | 19 | if ( configuration.length > 0 ) { 20 | $('#loaded-configuration').html('Showing the ' + configuration + ' configuration.') 21 | } else { 22 | $('#loaded-configuration').html('Showing aggregated values acrross all configurations — click one of the smaller charts below to display a specific configuration.') 23 | } 24 | 25 | return this; 26 | } 27 | 28 | App.parameterize = function(s) { 29 | return s.toLowerCase().replace(/[^a-z0-9]/g, "-") 30 | }; 31 | 32 | App.__load_startup_time = function() { 33 | d3.json(App.host + 'startup_time.json', function(error, json) { 34 | if (error) return console.warn(error.responseText); 35 | 36 | App.startup_time_chart = App.startupTimeChart(); 37 | d3.select('#startup-time-chart').datum(json).call(App.startup_time_chart) 38 | }); 39 | } 40 | 41 | App.__load_main_chart = function(configuration) { 42 | d3.json(App.host + 'events.json', function(error, json) { 43 | App.main_chart = App.timeLineChart(json, { container: d3.select("#main-chart") }); 44 | App.main_chart.draw(configuration) 45 | 46 | $(document).trigger('application.loaded') 47 | }); 48 | } 49 | 50 | App.__load_matrix_chart = function(configuration) { 51 | d3.json(App.host + 'events.json', function(error, json) { 52 | App.matrix_chart = App.matrixChart(json); 53 | App.matrix_chart.draw(configuration) 54 | if (configuration.length > 0) { 55 | App.matrix_chart.selection.select( '#chart-' + App.parameterize(configuration) ).classed('selected', true) 56 | } 57 | }); 58 | } 59 | 60 | App.Dispatcher = function(params) { 61 | $(document).on('application.loaded', function(event) { 62 | $('body').removeClass('loading') 63 | }) 64 | 65 | $(document).on('timeline.date.focus', function(event, date, index) { 66 | App.startup_time_chart.focus(date, index) 67 | App.main_chart.focus(date, index) 68 | App.matrix_chart.focus(date, index) 69 | }) 70 | 71 | $(document).on('timeline.date.unfocus', function(event, date, index) { 72 | App.startup_time_chart.unfocus(date, index) 73 | App.main_chart.unfocus(date, index) 74 | App.matrix_chart.unfocus(date, index) 75 | }) 76 | 77 | $(document).on('timeline.configuration.load', function(event, name) { 78 | App.main_chart.draw(name) 79 | App.matrix_chart.selection.selectAll('.chart').classed('selected', false) 80 | App.matrix_chart.selection.select( '#chart-' + App.parameterize(name) ).classed('selected', true) 81 | 82 | $('#loaded-configuration').html('Showing the ' + name + ' configuration.') 83 | 84 | document.location.hash = encodeURIComponent(name) 85 | }) 86 | 87 | return this 88 | }; 89 | -------------------------------------------------------------------------------- /web/ui/javascripts/matrix_chart.js: -------------------------------------------------------------------------------- 1 | var App = App || {} 2 | 3 | App.matrixChart = function(json, options) { 4 | if ( typeof json === 'undefined' ) { throw( new Error('The `json` argument is required')) }; 5 | if ( typeof options === 'undefined' ) { var options = {} }; 6 | 7 | var selection = options.selection || d3.select("#matrix-chart"); 8 | 9 | var parameterize = function(s) { 10 | return s.toLowerCase().replace(/[^a-z0-9]/g, "-") 11 | }; 12 | 13 | function focus(date) { 14 | var elements = selection.selectAll('.' + parameterize('matrix-chart-tick-' + date)); 15 | elements.classed('over', true); 16 | 17 | return this 18 | }; 19 | 20 | function unfocus(date) { 21 | var elements = selection.selectAll('.' + parameterize('matrix-chart-tick-' + date)); 22 | elements.classed('over', false); 23 | 24 | return this 25 | }; 26 | 27 | var draw = function() { 28 | var all_values = d3.values(json) 29 | .map( 30 | function(a) { 31 | return a.map( function(b) { return d3.values(b.values) } ) 32 | .reduce( function(i,j) { return i.concat(j) } ) } ) 33 | .reduce( function(i,j) { return i.concat(j) } ); 34 | 35 | var min = d3.min(all_values); 36 | var max = d3.max(all_values); 37 | var mean = Math.round(d3.mean(all_values)); 38 | var median = Math.round(d3.median(all_values)); 39 | 40 | for ( configuration in json ) { 41 | var data = { 42 | label: configuration, 43 | data: json[configuration], 44 | min: min, 45 | max: max, 46 | mean: mean, 47 | median: median 48 | } 49 | draw_single(data) 50 | } 51 | 52 | return this 53 | }; 54 | 55 | var draw_single = function(data) { 56 | selection.datum(data).call(area_chart); 57 | }; 58 | 59 | var area_chart = function(selection, options) { 60 | selection.each(function(data) { 61 | // Setup 62 | var options = options || {} 63 | 64 | var margin = { 65 | top: options.margin_top || 20, 66 | right: options.margin_right || 20, 67 | bottom: options.margin_bottom || 20, 68 | left: options.margin_left || 20 69 | }, 70 | width = 235, 71 | height = 50; 72 | 73 | var x = d3.time.scale().range([0, width]); 74 | var y = d3.scale.linear().range([height, 0]); 75 | var xAxis = d3.svg.axis().scale(x).orient('bottom').tickSize(5, 0).ticks(d3.time.day, 1); 76 | var yAxis = d3.svg.axis().scale(y).orient('left').tickSize(5, 0).ticks(4).tickFormat(d3.format('s')); 77 | 78 | var area = d3.svg.area() 79 | .x(function(d) { return x(d.time); }) 80 | .y0(height) 81 | .y1(function(d) { return y(d.mean); }) 82 | .interpolate('basis'); 83 | 84 | var svg = selection 85 | .append('div') 86 | .attr('id', function(d) { return parameterize('chart-' + d.label) }) 87 | .attr('data-name', function(d) { return d.label }) 88 | .attr('class', 'chart') 89 | .append('svg') 90 | .attr('width', width + margin.left + margin.right) 91 | .attr('height', height + margin.top + margin.bottom) 92 | .append('g') 93 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 94 | 95 | var label = data.label, 96 | name = label.toUpperCase().replace(/\//g, ' / '), 97 | 98 | data_normalized = data.data.map( function(d, i) { 99 | var values = d3.values(d.values).filter(function(d) { return +d > 0}) 100 | 101 | return { 102 | time: d3.time.format('%Y-%m-%d').parse(d.time), 103 | values: values, 104 | min: data.min, 105 | max: data.max, 106 | mean: Math.round(d3.mean(values) || 0), 107 | median: data.median 108 | } 109 | }); 110 | 111 | x.domain(d3.extent(data_normalized, function(d) { return d.time; })).ticks(d3.time.day, 1); 112 | y.domain([0, data.max]); 113 | 114 | // Draw 115 | 116 | svg.append('path') 117 | .datum(data_normalized) 118 | .attr('class', 'area') 119 | .attr('d', area); 120 | 121 | svg.append('g') 122 | .attr('class', 'y axis') 123 | .attr('transform', 'translate(0,' + x.range()[0] + ')') 124 | .call(yAxis); 125 | 126 | svg.select('.y.axis text').style('display', 'none'); // Hide bottom zero 127 | svg.selectAll('.y.axis .tick text').attr('x', '-5'); // Adjust tick text 128 | 129 | var tick = svg.append('g') 130 | .attr('class', 'x axis') 131 | .attr('transform', 'translate(0,' + x.range()[0] + ')') 132 | .call(xAxis) 133 | 134 | tick.selectAll('text').remove(); // Remove x-axis tick text 135 | tick.selectAll('.domain').remove(); // Remove x-axis domain 136 | 137 | tick.selectAll('.x.axis g.tick') 138 | .attr('class', function(d) { return 'tick ' + parameterize('matrix-chart-tick-' + d) }) 139 | .append('line') 140 | .attr('y1', -12) 141 | .attr('y2', height) 142 | .classed('ruler', true); 143 | 144 | tick.selectAll('.x.axis g.tick') 145 | .append('rect') 146 | .attr('x', 0) 147 | .attr('y', 0) 148 | .attr('width', function(d) { 149 | tickArr = x.ticks() 150 | difference = x(tickArr[tickArr.length - 1]) - x(tickArr[tickArr.length - 2]) 151 | return difference/7; 152 | }) 153 | .attr('height', height) 154 | .classed('overlay', true) 155 | .style('opacity', 0); 156 | 157 | tick.selectAll('.x.axis g.tick') 158 | .append('text') 159 | .attr('class', 'label') 160 | .attr('dx', function(d) { return x(d) < width/2 ? 3 : -3 }) 161 | .attr('dy', -6) 162 | .style('text-anchor', function(d) { return x(d) < width/2 ? 'start' : 'end' }) 163 | .text(function(d,i) { 164 | var found = data_normalized.filter(function(e) { return d.getTime() == e.time.getTime() })[0] 165 | return found ? d3.format(',')(found.mean) : 'N/A' 166 | }); 167 | 168 | svg.append('text') 169 | .attr('class', 'legend') 170 | .attr('x', width) 171 | .attr('y', height+13) 172 | .style('text-anchor', 'end ') 173 | .text(name); 174 | 175 | svg.append('line') 176 | .attr('y1', function(d) { return y(d.median) }) 177 | .attr('y2', function(d) { return y(d.median) }) 178 | .attr('x1', 0) 179 | .attr('x2', width) 180 | .attr('stroke-dasharray', '1, 1') 181 | .classed('average', true); 182 | 183 | // Interactivity 184 | 185 | svg.selectAll('.x.axis g.tick .overlay') 186 | .on('mouseover', function(d, i) { 187 | focus.call(this, d) 188 | $(document).trigger('timeline.date.focus', d, i) 189 | }) 190 | .on('mouseout', function(d, i) { 191 | unfocus.call(this, d) 192 | $(document).trigger('timeline.date.unfocus', d, i) 193 | }) 194 | 195 | selection.selectAll('div.chart') 196 | .on('click', function(d) { 197 | $(document).trigger('timeline.configuration.load', d3.select(this).attr('data-name')) 198 | }) 199 | }) 200 | 201 | return this 202 | }; 203 | 204 | return { 205 | json: json, 206 | options: options, 207 | selection: selection, 208 | draw: draw, 209 | focus: focus, 210 | unfocus: unfocus 211 | } 212 | }; 213 | -------------------------------------------------------------------------------- /web/ui/javascripts/startup_time.js: -------------------------------------------------------------------------------- 1 | var App = App || {}; 2 | 3 | App.startupTimeChart = function (options) { 4 | if ( typeof options === 'undefined' ) { var options = {} } 5 | 6 | // Setup 7 | // 8 | var margin = { top: options.top || 10, 9 | right: options.right || 20, 10 | bottom: options.bottom || 10, 11 | left: options.left || 5 }, 12 | width = options.width || 320, 13 | height = options.height || 80; 14 | 15 | var x = d3.time.scale().range([0, width - margin.left - margin.right]); 16 | 17 | var y = d3.scale.linear().range([height - margin.top - margin.bottom, 0]); 18 | 19 | var yAxis = d3.svg.axis() 20 | .scale(y) 21 | .orient("right") 22 | .tickSize(5, 0) 23 | .ticks(4) 24 | .tickFormat(function(d) { return d3.format("s")(d) + 's'}); 25 | 26 | var parameterize = function(s) { 27 | return s.toLowerCase().replace(/[^a-z0-9]/g, "-") 28 | }; 29 | 30 | var aggregated_data = null; 31 | 32 | // Draw 33 | // 34 | var draw = function(svg, data) { 35 | var labels = svg.selectAll('g.label') 36 | .data(data, function(d) { return d.time }); 37 | 38 | var label = labels.enter() 39 | .append('g') 40 | .attr('id', function(d,i) { return parameterize('startup-time-label-' + d.time) }) 41 | .attr('class', 'label') 42 | .attr('transform', function(d) { return 'translate(' + x(d.time) + ', -5)' } ) 43 | 44 | label.append('line') 45 | .attr('y1', 5) 46 | .attr('y2', height-margin.top-margin.bottom) 47 | .attr('stroke-dasharray', '1,1') 48 | .attr('transform', 'translate(1,0)' ) 49 | .classed('ruler', true) 50 | 51 | label.append('text') 52 | 53 | labels.exit() 54 | .remove() 55 | 56 | labels.select('text').text(function(d) { return d.value + 's' }); 57 | 58 | 59 | var bar = svg.selectAll('line.bar') 60 | .data(data, function(d) { return d.time }); 61 | 62 | bar.enter() 63 | .append("line") 64 | .attr('id', function(d,i) { return parameterize('startup-time-line-' + d.time) }) 65 | .attr('class', 'bar'); 66 | 67 | bar 68 | .attr('data-value', function(d) { return d.value}) 69 | .transition() 70 | .duration(250) 71 | .ease('circle') 72 | .attr("y1", function(d) { return y(d.value) }) 73 | .attr("x1", function(d) { return x(d.time) }) 74 | .attr("x2", function(d) { return x(d.time) }) 75 | .attr("y2", function(d) { return height-margin.bottom-margin.top }); 76 | 77 | bar.exit() 78 | .remove(); 79 | 80 | bar.on('mouseover', function(d,i) { 81 | chart.focus.call(this, d.time, i) 82 | $(document).trigger('timeline.date.focus', d.time, i) 83 | }); 84 | bar.on('mouseout', function(d,i) { 85 | chart.unfocus.call(this, d.time, i) 86 | $(document).trigger('timeline.date.unfocus', d.time, i) 87 | }); 88 | 89 | return this 90 | }; 91 | 92 | // The main function 93 | 94 | var chart = function(selection) { 95 | var svg = selection 96 | .append('svg') 97 | .attr('width', width + margin.left + margin.right) 98 | .attr('height', height + margin.top + margin.bottom) 99 | .append('g') 100 | .classed('container', true) 101 | .attr("transform", "translate(" + (margin.left+margin.right) + "," + (margin.top + margin.bottom) + ")"); 102 | 103 | selection.each(function(data) { 104 | var versions = data['datasets'].map( function(d) { return d.label } ).sort().reverse(); 105 | 106 | var max = d3.max(data.datasets.map( function(d) { return d.data } ).reduce( function(a, b) { return a.concat(b) })); 107 | 108 | aggregated_data = data['labels'].map(function(d, i) { 109 | return { 110 | time: d3.time.format("%Y-%m-%d").parse(d), 111 | value: d3.mean(data['datasets'].map( function(e) { return e['data'][i] }) ) 112 | } 113 | }); 114 | 115 | x.domain(d3.extent(data.labels, function(d) { return d3.time.format("%Y-%m-%d").parse(d) })); 116 | y.domain([0, max]); 117 | 118 | var legend = svg.append('g').attr('class', 'legend') 119 | .selectAll('g') 120 | .data(versions) 121 | .enter() 122 | .append('g') 123 | .classed('version', true) 124 | .attr('id', function(d) { return parameterize('legend-' + d) }) 125 | .attr('transform', function(d, i) { 126 | switch(d) { 127 | case 'master': 128 | var x = 0; 129 | break; 130 | case '2.1': 131 | var x = 50; 132 | break; 133 | default: 134 | var x = 50+11 + (i*11)+((i-2)*22) 135 | }; 136 | var y = height-margin.bottom-5; 137 | 138 | return 'translate(' + x + ', ' + y + ')' 139 | }); 140 | 141 | legend.append('rect') 142 | .attr('width', function(d) { return d == 'master' ? '2.8em' : d.length * 0.6 + 'em' }) 143 | .attr('height', 14) 144 | .attr('rx', 5) 145 | .attr('ry', 5); 146 | 147 | legend.append('text') 148 | .attr('dx', '1.1em') 149 | .attr('dy', '1.25em') 150 | .text(function(d) { return d }); 151 | 152 | legend.on('click', function(d) { 153 | if ( d3.select(this).classed('selected') ) { 154 | d3.select(this).classed('selected', false) 155 | draw(svg, aggregated_data) 156 | } 157 | 158 | else { 159 | d3.selectAll('.legend .version').classed('selected', false) 160 | d3.select(this).classed('selected', true) 161 | 162 | var dataset = data.datasets.filter( function(e) { return e.label == d } )[0]; 163 | 164 | var dataset_normalized = data['labels'].map(function(date, i) { 165 | return { 166 | time: d3.time.format("%Y-%m-%d").parse(date), 167 | value: dataset.data[i] 168 | } 169 | }) 170 | 171 | draw(svg, dataset_normalized) 172 | } 173 | }) 174 | 175 | svg.append("g") 176 | .attr("class", "y axis") 177 | .attr("transform", "translate(" + (width - margin.right - margin.left) + "," + 0 + ")") 178 | .call(yAxis); 179 | svg.select('.y.axis text').style('display', 'none'); // Hide bottom zero 180 | svg.select('.y.axis').attr('transform', 'translate(' + (width - margin.left - margin.right - 2) + ',3)') // Tighten the ticks a bit to the chart 181 | 182 | svg.append("line") 183 | .attr('x1', -1) 184 | .attr('x2', width-margin.left-margin.right+1) 185 | .attr('y1', height - margin.top - margin.bottom) 186 | .attr('y2', height - margin.top - margin.bottom) 187 | .classed('legend-ruler', true) 188 | 189 | svg.append("text") 190 | .attr('x', width - margin.right - 3) 191 | .attr('y', height - margin.top + 5) 192 | .attr('class', 'chart-legend') 193 | .text('STARTUP TIME') 194 | 195 | draw(svg, aggregated_data) 196 | }); 197 | }; 198 | 199 | chart.focus = function(date, index) { 200 | var element = d3.select('#' + parameterize('startup-time-line-' + date)); 201 | 202 | element.classed('over', true) 203 | d3.select( '#' + parameterize('startup-time-label-' + date) ).classed('over', true) 204 | 205 | return this 206 | }; 207 | 208 | chart.unfocus = function(date, index) { 209 | var element = d3.select('#' + parameterize('startup-time-line-' + date)); 210 | 211 | element.classed('over', false) 212 | d3.select( '#' + parameterize('startup-time-label-' + date) ).classed('over', false) 213 | 214 | return this 215 | } 216 | 217 | return chart; 218 | }; 219 | -------------------------------------------------------------------------------- /web/ui/javascripts/time_line_chart.js: -------------------------------------------------------------------------------- 1 | var App = App || {}; 2 | 3 | App.timeLineChart = function(json, options) { 4 | if ( typeof json === 'undefined' ) { throw( new Error('The `json` argument is required')) }; 5 | if ( typeof options === 'undefined' ) { var options = {} } 6 | 7 | var container = options.container || d3.select("#chart"), 8 | 9 | margin = { top: options.top || 40, 10 | right: options.right || 120, 11 | bottom: options.bottom || 60, 12 | left: options.left ||40 }, 13 | width = options.width || 1300, 14 | height = options.height || 400, 15 | 16 | xValue = function(d) { return d[0]; }, 17 | yValue = function(d) { return d[1]; }, 18 | 19 | xScale = d3.time.scale(), 20 | yScale = d3.scale.linear(), 21 | 22 | xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(5, 0).ticks(d3.time.day, 1), 23 | yAxis = d3.svg.axis().scale(yScale).orient("left").tickSize(5, 0).ticks(10).tickFormat(d3.format("s")), 24 | 25 | color = d3.scale.ordinal().range(['#7ec700', '#146655', '#1f9981', '#29ccab', '#33ffd6']), 26 | 27 | line = d3.svg.line().interpolate('basis').x(function(d) { return xScale(d.time)}).y(function(d) { return yScale(d.value)}), 28 | 29 | parameterize = function(s) { 30 | return s.toLowerCase().replace(/[^a-z0-9]/g, "-") 31 | }; 32 | 33 | var all_values = d3.values(json) 34 | .map( 35 | function(a) { 36 | return a.map( function(b) { return d3.values(b.values) } ) 37 | .reduce( function(i,j) { return i.concat(j) } ) } ) 38 | .reduce( function(i,j) { return i.concat(j) } ); 39 | 40 | var min = d3.min(all_values); 41 | var max = d3.max(all_values); 42 | var mean = Math.round(d3.mean(all_values)); 43 | var median = Math.round(d3.median(all_values)); 44 | 45 | // Aggregate data for the default view (no configuration selected) 46 | // 47 | var aggregated_data = function() { 48 | var aggregated_data = json[d3.keys(json)[0]].map( function(d) { return { time: d.time, values: {}, __values: [] } } ) 49 | 50 | aggregated_data.forEach( function(d, i) { 51 | for ( configuration in json ) { 52 | var data = json[configuration].filter( function(e) { return e.time == d.time } )[0]; 53 | // console.log(d.time, configuration, data.values); 54 | d.__values.push(data.values) 55 | } 56 | }) 57 | 58 | aggregated_data.forEach( function(d, i) { 59 | d.__values.reduce( function(a, b) { 60 | for (var prop in b) { 61 | if (!a[prop]) { d.values[prop] = [] }; 62 | d.values[prop].push(b[prop]) }; 63 | return d.values; 64 | }, {} ) 65 | }) 66 | 67 | aggregated_data.forEach( function(d, i) { 68 | for ( var version in d.values ) { 69 | var mean = Math.round(d3.mean(d.values[version])); 70 | d.values[version] = mean 71 | } 72 | }) 73 | 74 | return aggregated_data; 75 | } 76 | 77 | function draw(configuration) { 78 | container.datum( 79 | configuration ? json[configuration] : aggregated_data() 80 | ).call(line_chart); 81 | return this 82 | }; 83 | 84 | function focus(date) { 85 | var element = container.select('#' + parameterize('timeline-tick-' + date)); 86 | 87 | if ( element.classed('locked') ) { return } 88 | 89 | element.select('.ruler').classed('off', false) 90 | container.select('#' + parameterize('label-' + date)).classed('off', false) 91 | element.select('.ruler').classed('on', true) 92 | container.select('#' + parameterize('label-' + date)).classed('on', true) 93 | 94 | return this 95 | } 96 | 97 | function unfocus(date) { 98 | var element = container.select('#' + parameterize('timeline-tick-' + date)); 99 | 100 | if ( element.classed('locked') ) { return } 101 | 102 | element.select('.ruler').classed('off', true) 103 | container.select('#' + parameterize('label-' + date)).classed('off', true) 104 | element.select('.ruler').classed('on', false) 105 | container.select('#' + parameterize('label-' + date)).classed('on', false) 106 | 107 | return this 108 | } 109 | 110 | // The main function 111 | // 112 | function line_chart(selection) { 113 | selection.each(function(data) { 114 | // Normalize and manipulate the data 115 | // 116 | // 1. Convert date strings to time 117 | data = data.map( function(d) { return { time: d3.time.format("%Y-%m-%d").parse(d.time), values: d.values } }); 118 | 119 | // 2. Replace 0s with previous value, to disable "dips" in the chart 120 | data = data.map(function(d, i) { 121 | for (prop in d.values) { 122 | if (d.values[prop] === 0 && data[i-1] && data[i-1].values[prop]) { 123 | d.values[prop] = data[i-1].values[prop] 124 | } 125 | } 126 | return d 127 | }); 128 | 129 | var times = data.map( function(d) { return d.time } ), 130 | values = data.map( function(d) { return d.values } ); 131 | 132 | xScale 133 | .domain(d3.extent(times)) 134 | .range([0, width - margin.left - margin.right]).nice(d3.time.day); 135 | 136 | yScale 137 | .domain([0, max]) 138 | .range([height - margin.top - margin.bottom, 0]); 139 | 140 | color 141 | .domain(d3.keys(data[0].values).sort().reverse()); 142 | 143 | var tick_width = function() { 144 | tickArr = xScale.ticks() 145 | difference = xScale(tickArr[tickArr.length - 1]) - xScale(tickArr[tickArr.length - 2]) 146 | return Math.round((difference/7)+0.25) 147 | }() 148 | 149 | // TEMP: Remove old containers 150 | selection.selectAll('div.labels').remove() 151 | selection.selectAll('svg').remove() 152 | 153 | // [1] Labels 154 | var labels = selection 155 | .append('div') 156 | .attr('class', 'labels') 157 | .style({top: (margin.top+3)+'px', left: (margin.left+3)+'px'}) 158 | 159 | // [2] Chart 160 | var svg = selection.append('svg'); 161 | svg.attr('width', width) 162 | .attr('height', height) 163 | .append('g').classed('container', true); 164 | 165 | // [3] Container 166 | var container = svg.select('g.container') 167 | .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 168 | 169 | container.append('g').attr('class', 'y axis') 170 | container.append('g').attr('class', 'x axis') 171 | container.append('g').attr('class', 'lines-dimmed') 172 | container.append('g').attr('class', 'lines') 173 | container.append('g').attr('class', 'lines-dotted') 174 | container.append('g').attr('class', 'legend') 175 | 176 | // [4] Line charts 177 | var versions = color.domain().sort().map(function(name) { 178 | return { 179 | name: name, 180 | values: data.map(function(d) { 181 | return {time: d.time, value: d.values[name]}; 182 | }) 183 | }; 184 | }); 185 | 186 | // a. Clipping paths 187 | var existing_values = color.domain().map(function(version) { 188 | var dates = xScale.ticks(d3.time.day, 1).filter(function(d) { 189 | var found = data.filter(function(e) { return e.time.getTime() == d.getTime() })[0]; 190 | var zero = found ? +found.values[version] < 1 : null 191 | return !!found && !!!zero 192 | }) 193 | return { version: version, dates: dates } 194 | }) 195 | 196 | var clips = container 197 | .append('g') 198 | .attr('class', 'clips existing') 199 | 200 | var clip = clips 201 | .selectAll('clipPath') 202 | .data(existing_values) 203 | .enter() 204 | .append('clipPath') 205 | .attr('id', function(d) { return 'clip-existing-' + d.version }); 206 | 207 | clip.selectAll('rect') 208 | .data(function(d) { return d.dates}) 209 | .enter() 210 | .append('rect') 211 | .attr('width', tick_width) 212 | .attr('height', height-margin.top) 213 | .attr('x', function(d) { return xScale(d) }) 214 | .attr('y', -margin.bottom) 215 | .attr('fill', 'yellow') 216 | .style('opacity', 0.1); 217 | 218 | var missing_values = color.domain().map(function(version) { 219 | var dates = xScale.ticks(d3.time.day, 1).filter(function(d) { 220 | var found = data.filter(function(e) { return e.time.getTime() == d.getTime() })[0]; 221 | var zero = found ? +found.values[version] < 1 : null 222 | return !!!found || !!zero 223 | }) 224 | return { version: version, dates: dates } 225 | }) 226 | 227 | var clips = container 228 | .append('g') 229 | .attr('class', 'clips') 230 | 231 | var clip = clips 232 | .selectAll('clipPath') 233 | .data(missing_values) 234 | .enter() 235 | .append('clipPath') 236 | .attr('id', function(d) { return 'clip-' + d.version }); 237 | 238 | clip.selectAll('rect') 239 | .data(function(d) { return d.dates}) 240 | .enter() 241 | .append('rect') 242 | .attr('width', tick_width) 243 | .attr('height', height-margin.top) 244 | .attr('x', function(d) { return xScale(d) }) 245 | .attr('y', -margin.bottom) 246 | .attr('fill', 'yellow') 247 | .style('opacity', 0.1); 248 | 249 | // b. Dimmed lines (1px grey) 250 | container.select('g.lines-dimmed').selectAll('g') 251 | .data(versions) 252 | .enter() 253 | .append('g') 254 | .attr('id', function(d) { return parameterize('line-dimmed-' + d.name) }) 255 | .attr('class', 'line dimmed') 256 | .append('path') 257 | .attr('d', function(d) { return line(d.values) }) 258 | .attr('opacity', '0') 259 | .transition() 260 | .duration(500) 261 | .attr('opacity', '1'); 262 | 263 | // c. Solid line charts 264 | var version = container.select('g.lines').selectAll('g') 265 | .data(versions) 266 | .enter().append('g') 267 | .attr('id', function(d) { return parameterize('line-' + d.name) }) 268 | .attr('class', 'line'); 269 | 270 | version 271 | .append('path') 272 | .attr('clip-path', function(d) { return 'url(#clip-existing-' + d.name + ')' }) 273 | .style('stroke', function(d) { return color(d.name) }) 274 | .attr('d', function(d) { return line(d.values) }) 275 | .attr('opacity', '0') 276 | .transition() 277 | .duration(500) 278 | .attr('opacity', '1'); 279 | 280 | // d. Dotted line charts 281 | container.select('g.lines-dotted').selectAll('g.line-dotted') 282 | .data(versions) 283 | .enter().append('g') 284 | .attr('class', 'line dotted') 285 | .attr('id', function(d) { return parameterize('line-dotted-' + d.name) }) 286 | .append('path') 287 | .attr('clip-path', function(d) { return 'url(#clip-' + d.name + ')' }) 288 | .attr('d', function(d) { return line(d.values) }) 289 | .attr('stroke-dasharray', '2, 4') 290 | .style('stroke', function(d) { return color(d.name) }) 291 | .attr('opacity', '0') 292 | .transition() 293 | .duration(500) 294 | .attr('opacity', '1'); 295 | 296 | // [5] Series legends 297 | var legend = container.select('g.legend') 298 | .selectAll('g') 299 | .data(color.domain().sort()) 300 | .enter() 301 | .append('g') 302 | .classed('series legend', true) 303 | .attr('id', function(d) { return parameterize('legend-' + d) }) 304 | .attr('transform', function(d, i) { 305 | return 'translate(' 306 | + ( width-margin.left-margin.right+15 ) 307 | + ', ' 308 | + ( (height-margin.top-margin.bottom-20) - i*25 ) 309 | + ')' 310 | }); 311 | 312 | legend.append('rect') 313 | .attr('width', function(d) { return d.length * 0.75 + 'em' }) 314 | .attr('height', 20) 315 | .attr('rx', 10) 316 | .attr('ry', 10) 317 | .attr('style', function(d) { return 'fill:' + color(d) }); 318 | 319 | legend.append('text') 320 | .attr('dx', '1.1em') 321 | .attr('dy', '1.25em') 322 | .style('fill', '#fff') 323 | .text(function(d) { return d }); 324 | 325 | // [6] X-Axis 326 | container.select(".x.axis") 327 | .attr("transform", "translate(0," + yScale.range()[0] + ")") 328 | .call(xAxis) 329 | .selectAll('text') 330 | .attr("transform", "rotate(-90) translate(-10,-11)") 331 | .style('text-anchor', 'end') 332 | .style('font-weight', function(d) { return d.getDate() == 1 ? 'bolder' : 'normal' }); 333 | 334 | container.selectAll('.x.axis g.tick') 335 | .attr('id', function(d) { return parameterize('timeline-tick-'+d) }) 336 | .append('line') 337 | .attr('id', function(d) { return parameterize('ruler-'+d) }) 338 | .attr('y1', -height-margin.top) 339 | .attr('y2', 0) 340 | .classed('ruler', true); 341 | container.selectAll('.x.axis g.tick') 342 | .append('rect') 343 | .attr('x', -5) 344 | .attr('y', -(height-margin.top-margin.bottom)) 345 | .attr('width', function(d) { 346 | tickArr = xScale.ticks() 347 | difference = xScale(tickArr[tickArr.length - 1]) - xScale(tickArr[tickArr.length - 2]) 348 | return difference/7; 349 | }) 350 | .attr('height', height+margin.bottom) 351 | .classed('overlay', true) 352 | .style('opacity', 0); 353 | 354 | // [7] Y-Axis 355 | container.select(".y.axis") 356 | .attr("transform", "translate(0," + xScale.range()[0] + ")") 357 | .call(yAxis); 358 | container.select('.y.axis text').style({display: 'none'}); // Hide bottom zero 359 | 360 | container.selectAll('.y.axis g.tick') 361 | .append('line') 362 | .attr('x2', width-margin.left-margin.right) 363 | .attr('y2', 0) 364 | .classed('ruler', true) 365 | 366 | // [8] Labels 367 | var label = labels.selectAll('div').data(data) 368 | .enter() 369 | .append('div') 370 | .attr('id', function(d) { return parameterize('label-' + d.time) }) 371 | .classed('label', true) 372 | .classed('off', true) 373 | .style('left', function(d) { 374 | var position = Math.round(xScale(d.time)) 375 | if (position > width/2) { position = width - (width - position) - 500 - 10 } 376 | return position +'px' 377 | }) 378 | .style('text-align', function(d) { 379 | var position = Math.round(xScale(d.time)-4) 380 | return (position > width/2) ? 'right' : 'left' 381 | }) 382 | 383 | label.append('p') 384 | .classed('date', true) 385 | .attr('style', function(d) { 386 | var position = Math.round(xScale(d.time)) 387 | if (position > width/2) { 388 | return 'right:-59px' 389 | } else { 390 | return 'left:-1em' 391 | } 392 | }) 393 | .html(function(d) { return d3.time.format('%b %d')(d.time) }) 394 | 395 | label.append('p') 396 | .classed('metrics', true) 397 | .html(function(d) { 398 | var result = [] 399 | d3.keys(d.values).sort().reverse().forEach( function(version) { 400 | result.push( 401 | '' 402 | + '' 404 | + version 405 | + '' 406 | + '' 407 | + d3.format(',')(d.values[version]) 408 | + '' 409 | + '' 410 | ) 411 | }) 412 | return result.join('\n') 413 | }); 414 | 415 | var missing_dates = xScale.ticks(d3.time.day, 1).filter(function(d) { 416 | return !!! data.filter(function(e) { return e.time.getTime() == d.getTime() })[0]; 417 | }) 418 | 419 | var label_missing = labels.selectAll('div.missing').data(missing_dates) 420 | .enter() 421 | .append('div') 422 | .attr('id', function(d) { return parameterize('label-' + d) }) 423 | .classed('label missing', true) 424 | .classed('off', true) 425 | .style('left', function(d) { 426 | var position = Math.round(xScale(d)) 427 | if (position > width/2) { position = width - (width - position) - 500 - 10 } 428 | return position +'px' 429 | }) 430 | .style('text-align', function(d) { 431 | var position = Math.round(xScale(d)-4) 432 | return (position > width/2) ? 'right' : 'left' 433 | }) 434 | 435 | label_missing.append('p') 436 | .classed('date', true) 437 | .attr('style', function(d) { 438 | var position = Math.round(xScale(d)) 439 | if (position > width/2) { 440 | return 'right:-59px' 441 | } else { 442 | return 'left:-1em' 443 | } 444 | }) 445 | .html(function(d) { return d3.time.format('%b %d')(d) }) 446 | 447 | label_missing.append('p') 448 | .classed('metrics', true) 449 | .html('N/A'); 450 | 451 | // Interactivity 452 | container.selectAll('.x.axis g.tick .overlay') 453 | .on('mouseover', function(d, i) { 454 | focus.call(this, d) 455 | $(document).trigger('timeline.date.focus', d, i) 456 | }) 457 | .on('mouseout', function(d, i) { 458 | unfocus.call(this, d) 459 | $(document).trigger('timeline.date.unfocus', d, i) 460 | }) 461 | .on('click', function(d) { 462 | var tick = d3.select(this.parentNode) 463 | 464 | if ( tick.classed('locked') ) { 465 | tick.classed('locked', false) 466 | selection.select('#' + parameterize('label-' + d)).classed('locked', false) 467 | return this 468 | } 469 | 470 | tick.classed('locked', true) 471 | selection.select('#' + parameterize('label-' + d)).classed('locked', true) 472 | }); 473 | 474 | legend.on('click', function(d) { 475 | if ( 476 | !d3.select(this).classed('dimmed') && 477 | d3.selectAll('.series.legend')[0].some( function(d) { return d3.select(d).classed('dimmed') } ) 478 | ) 479 | // Toggle: Reset the line charts 480 | { 481 | d3.selectAll('.series.legend').classed('dimmed', false) 482 | d3.selectAll('.lines .line').classed('on', false) 483 | d3.selectAll('.lines .line').classed('off', false) 484 | d3.selectAll('.lines-dotted .line').classed('on', false) 485 | d3.selectAll('.lines-dotted .line').classed('off', false) 486 | 487 | d3.selectAll('.lines .line').classed('highlighted', false) 488 | d3.selectAll('.lines-dotted .line').classed('highlighted', false) 489 | 490 | d3.selectAll('.series.readout').classed('on', false) 491 | d3.selectAll('.series.readout').classed('off', true) 492 | 493 | d3.selectAll('.metrics .metric').classed('off', false) 494 | d3.selectAll('.metrics .metric').classed('on', false) 495 | } else 496 | // Highlight corresponding line chart 497 | { 498 | d3.selectAll('.series.legend').classed('dimmed', true) 499 | d3.select(this).classed('dimmed', false) 500 | 501 | d3.selectAll('.lines .line').classed('off', true) 502 | d3.selectAll('.lines-dotted .line').classed('off', true) 503 | 504 | d3.select('#line-'+parameterize(d)).classed('off', false) 505 | d3.select('#line-'+parameterize(d)).classed('on', true) 506 | d3.select('#line-'+parameterize(d)).classed('highlighted', true) 507 | 508 | d3.select('#line-dotted-'+parameterize(d)).classed('off', false) 509 | d3.select('#line-dotted-'+parameterize(d)).classed('on', true) 510 | d3.select('#line-dotted-'+parameterize(d)).classed('highlighted', true) 511 | 512 | d3.select('#readout-'+parameterize(d)).classed('off', false) 513 | d3.select('#readout-'+parameterize(d)).classed('on', true) 514 | 515 | d3.selectAll('.metrics .metric').classed('off', true) 516 | d3.selectAll('.metrics .' + parameterize('metric-'+d)).classed('off', false) 517 | d3.selectAll('.metrics .' + parameterize('metric-'+d)).classed('on', true) 518 | } 519 | }) 520 | 521 | }); 522 | }; 523 | 524 | return { 525 | json: json, 526 | options: options, 527 | draw: draw, 528 | focus: focus, 529 | unfocus: unfocus, 530 | }; 531 | } 532 | -------------------------------------------------------------------------------- /web/ui/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | color: #222; 8 | background: #fcfcfc; 9 | font-family: 'Open Sans', sans-serif; 10 | margin: 2em 2em 2em 2em; 11 | } 12 | body .on { display: block !important; } 13 | body .off { display: none !important; } 14 | 15 | body.loading { 16 | background: #fcfcfc url('../images/loading.png') 50% 50% no-repeat; 17 | } 18 | 19 | body.loading * { 20 | opacity: 0.5; 21 | } 22 | 23 | a { 24 | color: #222; 25 | } 26 | 27 | header { 28 | margin: 2em; 29 | position: relative; 30 | } 31 | 32 | header h1 { 33 | color: #59627b; 34 | font-size: 28px; 35 | font-weight: lighter; 36 | letter-spacing: -1px; 37 | margin: 0; 38 | padding: 0 0 0.1em 0; 39 | } 40 | 41 | header h1 a { 42 | color: #59627b; 43 | text-decoration: none; 44 | } 45 | 46 | header p { 47 | color: #444; 48 | font-size: 12px; 49 | margin: 0 0 0.25em 0; 50 | padding: 0; 51 | max-width: 800px; 52 | } 53 | 54 | header p small { 55 | color: #6c6c6c; 56 | } 57 | 58 | header p strong { 59 | color: #222; 60 | background: #e1e5e5; 61 | font-size: 10px; 62 | font-family: 'Menlo', monospace; 63 | padding: 0.3em 0.75em 0.25em 0.75em; 64 | margin: 0 0.1em 0 0.1em; 65 | border-radius: 5px; 66 | transition: 250ms; 67 | } 68 | 69 | #startup-time-chart { 70 | position: absolute; 71 | right: 103px; 72 | top: -27px; 73 | } 74 | 75 | #main-chart { 76 | position: relative; 77 | } 78 | 79 | #matrix-chart { 80 | } 81 | 82 | footer { 83 | color: #999; 84 | font-size: 10px; 85 | text-align: right; 86 | border-top: 1px solid #e0e0e0; 87 | line-height: 1.5em; 88 | margin: 0 3.25em 0 7em; 89 | position: absolute; left: 0; bottom: 0; 90 | width: 81.60%; 91 | height: 3em; 92 | overflow: hidden; 93 | } 94 | 95 | footer a { 96 | color: #999; 97 | } 98 | -------------------------------------------------------------------------------- /web/ui/stylesheets/matrix_chart.css: -------------------------------------------------------------------------------- 1 | #matrix-chart { 2 | max-width: 1350px; 3 | padding: 0 0 0 5px; 4 | } 5 | #matrix-chart .chart { 6 | background: transparent; 7 | margin: 5px; 8 | padding: 0 0.5em 0 0.5em; 9 | border-radius: 5px; 10 | display: inline-block; 11 | } 12 | #matrix-chart .chart:hover { 13 | background: #e3e5e5; 14 | cursor: pointer; 15 | } 16 | #matrix-chart .chart:hover svg { 17 | cursor: pointer; 18 | } 19 | #matrix-chart .area { 20 | fill: #808080; 21 | } 22 | #matrix-chart .axis path, 23 | #matrix-chart .axis line { 24 | fill: none; 25 | stroke: none; 26 | shape-rendering: crispEdges; 27 | } 28 | #matrix-chart .axis .domain { 29 | stroke: #666; 30 | stroke-width: 1px; 31 | } 32 | #matrix-chart .axis text { 33 | fill: #6c6c6c; 34 | font-family: sans-serif; 35 | font-size: 8px; 36 | font-weight: lighter; 37 | shape-rendering: crispEdges; 38 | } 39 | #matrix-chart .x.axis .tick .ruler { 40 | stroke: #444; 41 | stroke-width: 1px; 42 | display: none; 43 | } 44 | #matrix-chart .x.axis .tick.over .ruler { 45 | display: block; 46 | } 47 | #matrix-chart .x.axis .tick .label { 48 | fill: #444; 49 | font-family: 'Open Sans', sans-serif; 50 | font-size: 8px; 51 | display: none; 52 | } 53 | #matrix-chart .x.axis .tick.over .label { 54 | display: block; 55 | } 56 | #matrix-chart text.legend { 57 | fill: #444; 58 | font-family: 'Open Sans', sans-serif; 59 | font-size: 10px; 60 | font-weight: lighter; 61 | } 62 | #matrix-chart .chart line.average { 63 | stroke: #000; 64 | stroke-width: 1px; 65 | shape-rendering: crispEdges; 66 | opacity: 0.1; 67 | } 68 | 69 | #matrix-chart .chart.selected { 70 | background: #e1e5e5; 71 | transition: 250ms; 72 | } 73 | #matrix-chart .chart.selected text.legend { 74 | fill: #000; 75 | transition: 250ms; 76 | } 77 | -------------------------------------------------------------------------------- /web/ui/stylesheets/startup_time.css: -------------------------------------------------------------------------------- 1 | #startup-time-chart path { 2 | fill: #999; 3 | shape-rendering: crispEdges; 4 | } 5 | #startup-time-chart line { 6 | shape-rendering: crispEdges; 7 | } 8 | #startup-time-chart line.bar { 9 | stroke: #cacccc; 10 | stroke-width: 2px; 11 | } 12 | #startup-time-chart line.missing { 13 | stroke: #ccc; 14 | } 15 | #startup-time-chart line.over { 16 | stroke: #666; 17 | } 18 | #startup-time-chart line.ruler { 19 | stroke: #ccc; 20 | stroke-width: 1px; 21 | } 22 | #startup-time-chart line.legend-ruler { 23 | stroke: #cacccc; 24 | stroke-width: 2px; 25 | } 26 | #startup-time-chart .chart-legend { 27 | fill: #bdbfbf; 28 | font-size: 10px; 29 | font-weight: lighter; 30 | text-anchor: end; 31 | } 32 | #startup-time-chart .axis path, 33 | #startup-time-chart .axis line { 34 | fill: none; 35 | stroke: none; 36 | } 37 | #startup-time-chart .axis text { 38 | fill: #c1c1c1; 39 | font-family: sans-serif; 40 | font-size: 8px; 41 | } 42 | #startup-time-chart .axis line { 43 | display: none; 44 | } 45 | #startup-time-chart .x.axis .domain { 46 | stroke: #333; 47 | stroke-width: 1px; 48 | } 49 | #startup-time-chart .y.axis { 50 | display: none; 51 | } 52 | #startup-time-chart:hover .y.axis { 53 | display: block; 54 | } 55 | #startup-time-chart g.label { 56 | color: #666; 57 | font-size: 8px; 58 | display: none; 59 | } 60 | #startup-time-chart g.label.over { 61 | display: block; 62 | } 63 | #startup-time-chart g.label text { 64 | text-anchor: middle; 65 | } 66 | #startup-time-chart g.legend g.version { 67 | cursor: pointer; 68 | } 69 | #startup-time-chart g.legend g.version rect { 70 | fill: #cacccc; 71 | } 72 | #startup-time-chart g.legend g.version:hover rect { 73 | fill: #a4a6a5; 74 | } 75 | #startup-time-chart g.legend g.version.selected rect { 76 | fill: #7e807f; 77 | } 78 | #startup-time-chart g.legend g.version text { 79 | fill: #fff; 80 | background-color: #ccc; 81 | font-size: 8px; 82 | } 83 | -------------------------------------------------------------------------------- /web/ui/stylesheets/time_line_chart.css: -------------------------------------------------------------------------------- 1 | #main-chart svg text { font-size: 10px; } 2 | #main-chart .line path { fill: none; stroke-linecap: round; } 3 | #main-chart .lines .line path { stroke: #000; stroke-width: 2px; } 4 | #main-chart .lines .line.highlighted path, 5 | #main-chart .lines-dotted .line.highlighted path { stroke-width: 4px; } 6 | #main-chart .lines-dimmed .line { display: block; } 7 | #main-chart .lines-dimmed .line path { stroke: #999; stroke-width: 1px; } 8 | #main-chart .lines-dotted .line path { stroke: #666; stroke-width: 2px; } 9 | #main-chart .lines .readout { stroke: none; font-weight: normal; font-size: 18px; letter-spacing: -0.05em; } 10 | #main-chart .series.legend { cursor: pointer; } 11 | #main-chart .series.legend.dimmed rect { fill: #999 !important; } 12 | #main-chart .tick line { stroke: #000000; shape-rendering: crispedges; } 13 | #main-chart .tick text { text-anchor: right !important; } 14 | #main-chart .tick line.ruler { stroke: #efefef; } 15 | #main-chart .tick.locked line.ruler { stroke: #666 !important; } 16 | #main-chart .x.axis .tick line { stroke: #ccc; } 17 | #main-chart .x.axis .tick text { font-size: 8px; fill: #999; } 18 | #main-chart .x.axis .tick line.ruler { stroke: #ccc; display: none; } 19 | #main-chart .y.axis .domain { display: none; } 20 | #main-chart .y.axis .tick text { font-size: 9px; fill: #b3b3b3; } 21 | #main-chart .y.axis .tick line { display: none; } 22 | #main-chart .y.axis .tick line.ruler { display: block; } 23 | #main-chart .labels { position: relative; top: 0; left: 0; z-index: -1; } 24 | #main-chart .labels div.label { 25 | color: #444; 26 | background: #fcfcfc; 27 | font-size: 12px; 28 | padding: 0.5em 0.25em 0.5em 5.25em; 29 | margin: 0 0 0 -5em; 30 | min-width: 30em; 31 | width: 500px; 32 | position: absolute; top: -50px; 33 | /*border: 1px dotted red;*/ } 34 | #main-chart .labels .label p { 35 | padding: 0; 36 | margin: 0; } 37 | #main-chart .labels .label p.date { 38 | color: #999; 39 | background: #fcfcfc; 40 | font-size: 16px; 41 | font-weight: lighter; 42 | padding: 0.25em 0 0.25em 1em; 43 | margin-bottom: 0.25em; 44 | display: block; 45 | position: absolute; top: 1px; } 46 | #main-chart .labels .label.locked p.date { 47 | color: #444; } 48 | #main-chart .labels .label span.version { 49 | color: #fff; 50 | background-color: #999; 51 | font-size: 90%; 52 | padding: 0.25em 0.5em; 53 | margin: 0; 54 | border-radius: 0.5em; } 55 | #main-chart .labels .label span.value { 56 | color: #444; 57 | background: #fcfcfc; 58 | font-size: 100%; 59 | padding: 0.25em 0.5em; 60 | margin: 0; 61 | top: 1px; 62 | position: relative; } 63 | #main-chart .labels .label.missing p.metrics { 64 | color: #999; 65 | margin-top: 0.1em; 66 | padding-right: 0.3em; 67 | } 68 | -------------------------------------------------------------------------------- /web/ui/test/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cucumber' 4 | gem 'capybara' 5 | gem 'poltergeist' 6 | gem 'launchy' 7 | gem 'thin' 8 | gem 'rake' 9 | -------------------------------------------------------------------------------- /web/ui/test/README.markdown: -------------------------------------------------------------------------------- 1 | # Integration tests for the JavaScript application 2 | 3 | This folder contains integration tests which verify the behaviour of 4 | the JavaScript application using Cucumber, Capybara and the PhantomJS 5 | Ruby wrapper. 6 | 7 | ## Installation 8 | 9 | bundle install 10 | 11 | ## Running the test suite 12 | 13 | bundle exec cucumber 14 | 15 | -------------------------------------------------------------------------------- /web/ui/test/features/dashboard.feature: -------------------------------------------------------------------------------- 1 | Feature: Displaying the dashboard 2 | 3 | Scenario: Displaying the dashboard 4 | When I load the application 5 | Then I should see 9 'svg' elements 6 | And I should see the '#startup-time-chart svg' element 7 | And I should see the '#main-chart svg' element 8 | And I should see the '#matrix-chart .chart:first-child svg' element 9 | 10 | Scenario: Loading a configuration 11 | When I load the application 12 | And I click the '#chart-apache-in-json-out' link 13 | Then I should see the '#loaded-configuration strong' element 14 | And I should see 'apache in/json out' on the page 15 | -------------------------------------------------------------------------------- /web/ui/test/features/setup/application.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | require 'net/http' 3 | require 'thin' 4 | 5 | # Start a simple Thin server to serve the application for Capybara 6 | # 7 | begin 8 | Net::HTTP.get( URI('http://localhost:8000') ) 9 | STDERR.puts "\e[2mApplication already running...\e[0m" 10 | 11 | rescue Errno::ECONNREFUSED 12 | STDERR.puts "\e[2mStarting the Rack server for the application...\e[0m" 13 | 14 | Timeout.timeout(10) do 15 | @pid = Process.fork do 16 | Thin::Logging.silent = true 17 | Thin::Server.start 8000, lambda { |env| 18 | if env['PATH_INFO'] == '/' 19 | [200, {'Content-Type' => 'text/html'}, File.new('../index.html')] 20 | else 21 | Rack::Directory.new('..').(env) 22 | end 23 | } 24 | end 25 | end 26 | 27 | at_exit do 28 | STDERR.puts "\e[2mStopping the Rack server with PID #{@pid}...\e[0m" 29 | Process.kill 'TERM', @pid 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /web/ui/test/features/setup/env.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit/assertions' 2 | require 'cucumber' 3 | require 'capybara' 4 | require 'capybara/cucumber' 5 | require 'capybara/poltergeist' 6 | 7 | Capybara.default_selector = :css 8 | Capybara.default_driver = :poltergeist 9 | Capybara.app_host = 'http://localhost:8000' 10 | Capybara.default_max_wait_time = 5 11 | Capybara.run_server = false 12 | 13 | World(Test::Unit::Assertions) 14 | 15 | Before do |scenario| 16 | @current_feature_id = scenario.feature.name.downcase.gsub(/\s/, '-') 17 | @current_scenario_id = scenario.name.downcase.gsub(/\s/, '-') 18 | end 19 | 20 | After do |scenario| 21 | page.driver.render [File.expand_path('../../../tmp', __FILE__), 22 | "#{@current_feature_id};FAILED.png"].join('/') if scenario.failed? 23 | end 24 | -------------------------------------------------------------------------------- /web/ui/test/features/step_definitions/common_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I load the application$/ do 2 | visit '/index.html' 3 | end 4 | 5 | Then /^show me the page$/ do 6 | save_and_open_page 7 | end 8 | 9 | Then /^save a screenshot$/ do 10 | page.driver.render "tmp/cucumber-#{@current_scenario_id};#{Time.now.to_i}.png" 11 | end 12 | -------------------------------------------------------------------------------- /web/ui/test/features/step_definitions/dashboard_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I click the '(.+?)' link?$/ do |selector| 2 | page.find(selector).click 3 | end 4 | 5 | Then /^I should see (\d+) '(\S+)' elements?$/ do |number, selector| 6 | # assert_equal number.to_i, page.all(selector).size 7 | page.assert_selector selector, count: number 8 | end 9 | 10 | Then /^I should see the '(.+?)' element?$/ do |selector| 11 | assert page.find(selector).visible?, "Page doesn't show the '#{selector}' element" 12 | end 13 | 14 | Then /^I should see '(.+?)' on the page?$/ do |content| 15 | assert_text content 16 | end 17 | -------------------------------------------------------------------------------- /webapp/config.yml: -------------------------------------------------------------------------------- 1 | hosts: '9c6c5de1b6dabef919e771c659ca6813.us-east-1.aws.found.io' 2 | runner: 3 | setup: '/Users/purbon/work/logstash-perf-testing/scripts/setup.sh' 4 | repo: 'git@github.com:elastic/logstash.git' 5 | workspace: '/Users/purbon/work/logstash-perf-testing/workspace' 6 | releases: 7 | - 1.4.2 8 | - 1.5.0 9 | - 1.5.1 10 | - 1.5.2 11 | branches: 12 | - master 13 | - 1.5 14 | rvm: 15 | ruby: "jruby-1.17.20" 16 | gemset: "lsperf-webapp" 17 | --------------------------------------------------------------------------------