litestack 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Gemfile +2 -0
  4. data/README.md +5 -11
  5. data/assets/event_page.png +0 -0
  6. data/assets/index_page.png +0 -0
  7. data/assets/topic_page.png +0 -0
  8. data/bench/bench_jobs_rails.rb +1 -1
  9. data/bench/bench_jobs_raw.rb +1 -1
  10. data/bin/liteboard +81 -0
  11. data/lib/action_cable/subscription_adapter/litecable.rb +1 -11
  12. data/lib/generators/litestack/install/USAGE +11 -0
  13. data/lib/generators/litestack/install/install_generator.rb +35 -0
  14. data/lib/generators/litestack/install/templates/cable.yml +11 -0
  15. data/lib/generators/litestack/install/templates/database.yml +34 -0
  16. data/lib/litestack/liteboard/liteboard.rb +305 -0
  17. data/lib/litestack/liteboard/views/event.erb +32 -0
  18. data/lib/litestack/liteboard/views/index.erb +54 -0
  19. data/lib/litestack/liteboard/views/layout.erb +303 -0
  20. data/lib/litestack/liteboard/views/litecable.erb +118 -0
  21. data/lib/litestack/liteboard/views/litecache.erb +144 -0
  22. data/lib/litestack/liteboard/views/litedb.erb +168 -0
  23. data/lib/litestack/liteboard/views/litejob.erb +151 -0
  24. data/lib/litestack/liteboard/views/topic.erb +48 -0
  25. data/lib/litestack/litecable.rb +25 -35
  26. data/lib/litestack/litecable.sql.yml +1 -1
  27. data/lib/litestack/litecache.rb +31 -28
  28. data/lib/litestack/litedb.rb +124 -1
  29. data/lib/litestack/litejob.rb +2 -2
  30. data/lib/litestack/litejobqueue.rb +8 -8
  31. data/lib/litestack/litemetric.rb +177 -88
  32. data/lib/litestack/litemetric.sql.yml +312 -42
  33. data/lib/litestack/litemetric_collector.sql.yml +56 -0
  34. data/lib/litestack/litequeue.rb +28 -29
  35. data/lib/litestack/litequeue.sql.yml +11 -0
  36. data/lib/litestack/litesupport.rb +137 -57
  37. data/lib/litestack/railtie.rb +10 -0
  38. data/lib/litestack/version.rb +1 -1
  39. data/lib/litestack.rb +1 -0
  40. data/lib/sequel/adapters/litedb.rb +1 -1
  41. data/template.rb +7 -0
  42. metadata +81 -5
  43. data/lib/litestack/metrics_app.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73ffde7dabefb83339815ca79679b0916637d50a6c70a43d13e34f8b431f2914
4
- data.tar.gz: 44ef247be2a245801d6d5f25a383031941b6c6dc496fac551eca525b274da178
3
+ metadata.gz: a1125a2cb5a2d9ea277fb9a0774632eb297ed4d322882dd33776d8a8f1c6471e
4
+ data.tar.gz: 9cb917c999850b2bd668e8826a4be4f091ee4e8e0f963b54ed87f5b7a7b060d6
5
5
  SHA512:
6
- metadata.gz: 8fe11c9b2d655dc39fadedb20782701ad8fe00cf8d548b1312a031122c2af5d6e66c26ab69ad22f6beb39e72419c33cf0d4b30bfd058d5a93db261122f6b29ae
7
- data.tar.gz: 2ba9faecce2a926e9a8b65efe11c0766eb3ef4021510fb786e3a7a9483ff2d24f463968cb5ddab07465cf3eae2b04f28edb428589f3c95b1c7ebe6c4f826cc54
6
+ metadata.gz: e51460d99e66732e3dcd72e9444f1d739df7ae37804eb9b308cdae0faa7c9c34d4e7554e8f9d0bf91905f7ae3e9b4d7470509a84653da2ee736582b268fe188a
7
+ data.tar.gz: 873b188bd6db924f1e56034c22f5cc465b01d653ac925a2d30de648271b0b64a705894667b90005bd3d457ad318d3ee4736b16a60fa128c34d894172bc9b8e16
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2023-08-13
4
+
5
+ - Reworked the Litecable thread safety model
6
+ - Fixed multiple litejob bugs (thanks Stephen Margheim)
7
+ - Fixed Railtie dependency (thanks Marco Roth)
8
+ - Litesupport fixes (thanks Stephen Margheim)
9
+ - Much improved metrics reporting for Litedb, Litecache, Litejob & Litecable
10
+ - Removed (for now, will come again later) litemetric reporting support for ad-hoc modules
11
+
12
+ ## [0.2.6] - 2023-07-16
13
+
14
+ - Much improved database location setting (thanks Brad Gessler)
15
+ - A Rails generator for better Rails Litestack defaults (thanks Brad Gessler)
16
+ - Revamped Litemetric, now much faster and more accurate (still experimental)
17
+ - Introduced Liteboard, a dashboard for viewing Litemetric data
18
+
3
19
  ## [0.2.3] - 2023-05-20
4
20
 
5
21
  - Cut back on options defined in the Litejob Rails adapter
data/Gemfile CHANGED
@@ -6,3 +6,5 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
 
9
+
10
+ gem "rack", "~> 3.0"
data/README.md CHANGED
@@ -31,19 +31,13 @@ Litestack is still pretty young and under heavy development, but you are welcome
31
31
 
32
32
  ## Installation
33
33
 
34
- Add this line to your application's Gemfile:
34
+ Add the `litestack` gem line to your application's Gemfile:
35
35
 
36
- ```ruby
37
- gem 'litestack'
38
- ```
39
-
40
- And then execute:
41
-
42
- $ bundle install
36
+ $ bundle add litestack
43
37
 
44
- Or install it yourself as:
38
+ To configure a Rails application to run the full litestack, run:
45
39
 
46
- $ gem install litestack
40
+ $ rails generate litestack:install
47
41
 
48
42
  ## Usage
49
43
 
@@ -189,7 +183,7 @@ production:
189
183
 
190
184
  ## Contributing
191
185
 
192
- Bug reports are welcome on GitHub at https://github.com/oldmoe/litestack. Please note that this is not an open contribution project and that we don't accept pull requests.
186
+ Bug reports and pull requests are welcome on GitHub at https://github.com/oldmoe/litestack.
193
187
 
194
188
  ## License
195
189
 
Binary file
Binary file
Binary file
@@ -29,7 +29,7 @@ if env == "a" # threaded
29
29
  end
30
30
 
31
31
  require_relative '../lib/active_job/queue_adapters/litejob_adapter'
32
- puts Litesupport.environment
32
+ puts Litesupport.scheduler
33
33
 
34
34
  RailsJob.queue_adapter = :litejob
35
35
  t = Time.now.to_f
@@ -28,7 +28,7 @@ end
28
28
 
29
29
  require './uljob.rb'
30
30
 
31
- STDERR.puts "litejob started in #{Litesupport.environment} environmnet"
31
+ STDERR.puts "litejob started in #{Litesupport.scheduler} environmnet"
32
32
 
33
33
  t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
34
34
  bench("enqueuing litejobs", count) do |i|
data/bin/liteboard ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require_relative '../lib/litestack/liteboard/liteboard'
6
+ DEFAULTS = {
7
+ config_path: Litemetric::DEFAULT_OPTIONS[:config_path],
8
+ path: Litemetric::DEFAULT_OPTIONS[:path]
9
+ }
10
+
11
+ options = {
12
+ config_path: nil,
13
+ path: nil,
14
+ deamonize: false,
15
+ Port: 9292,
16
+ Host: 'localhost',
17
+ environment: 'production',
18
+ quiet: false
19
+ }
20
+
21
+ OptionParser.new do |parser|
22
+ parser.banner = "Usage: liteboard [options]"
23
+ parser.on("-d", "--database PATH", "path to SQLite file (default: #{DEFAULTS[:path]})") { |v| options[:path] = v }
24
+ parser.on("-c", "--config PATH", "path to a litemetric config file (default: #{DEFAULTS[:config_path]}) ") { |v| options[:config_path] = v }
25
+ parser.on("-s", "--server SERVER", "use SERVER (e.g. puma/falcon/iodine)") { |v| options[:port] = v }
26
+ parser.on("-H", "--host HOST", "listen on HOST (default: #{options[:Host]})") { |v| options[:Host] = v }
27
+ parser.on("-p", "--port PORT", "use PORT (default: #{options[:Port]})") { |v| options[:Port] = v.to_i rescue options[:Port] }
28
+ parser.on("-D", "--deamonize", "run in the background") { |v| options[:deamonize] = true }
29
+ parser.on("-E", "--env ENVIRONMENT", "which environment to use (default: #{options[:environment]})") { |v| options[:environment] = v }
30
+ parser.on("-q", "--quiet", "turn off logging") { |v| options[:quiet] = true }
31
+ parser.on("-h", "--help", "print this message") do
32
+ puts parser
33
+ exit
34
+ end
35
+ end.parse!
36
+
37
+ def check_database(path)
38
+ unless File.exist?(path)
39
+ puts "liteboard: missing database file, please ensure the db path is correct"
40
+ puts "liteboard: exiting"
41
+ exit
42
+ end
43
+ end
44
+
45
+ check_database(options[:path]) if options[:path]
46
+
47
+ # if there is a config file then we need to check it
48
+ config = nil
49
+ if options[:config_path]
50
+ begin
51
+ config = Yaml.load_file(options[:config_path])
52
+ rescue
53
+ puts "liteboard: missing or bad config file, please ensure the config file path is correct"
54
+ puts "liteboard: exiting"
55
+ exit
56
+ end
57
+ else # no config path! use the default
58
+ config = Yaml.load_file(DEFAULTS[:config_path]) rescue nil
59
+ end
60
+
61
+ if config
62
+ if options[:path].nil?
63
+ path = config['path'] || config[options[:environment]]['path']
64
+ options[:path] = path
65
+ end
66
+ end
67
+
68
+ # if still no path we assume a default db path
69
+ options[:path] = DEFAULTS[:path] if options[:path].nil?
70
+
71
+
72
+ # check the validity of the path before starting the server
73
+ check_database(options[:path])
74
+ Litemetric.options = options
75
+ litemetric = Litemetric.instance
76
+ options[:app] = Liteboard.app
77
+
78
+ require_relative '../lib/litestack'
79
+ puts "Starting Liteboard version #{Litestack::VERSION}"
80
+
81
+ Rack::Server.start(options)
@@ -10,20 +10,10 @@ module ActionCable
10
10
 
11
11
  prepend ChannelPrefix
12
12
 
13
- DEFAULT_OPTIONS = {
14
- config_path: "./config/litecable.yml",
15
- path: "./db/cable.db",
16
- sync: 0, # no need to sync at all
17
- mmap_size: 16 * 1024 * 1024, # 16MB of memory hold hot messages
18
- expire_after: 10, # remove messages older than 10 seconds
19
- listen_interval: 0.005, # check new messages every 5 milliseconds
20
- metrics: false
21
- }
22
-
23
13
  def initialize(server, logger=nil)
24
14
  @server = server
25
15
  @logger = server.logger
26
- super(DEFAULT_OPTIONS.dup)
16
+ super({config_path: "./config/litecable.yml"})
27
17
  end
28
18
 
29
19
  def shutdown
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Installs litestack gem and configures the project to use litestack
3
+ in development, test, and production environments.
4
+
5
+ Example:
6
+ bin/rails generate litestack:install
7
+
8
+ This will modify:
9
+ config/environments/production.rb
10
+ config/database.yml
11
+ config/cable.yml
@@ -0,0 +1,35 @@
1
+ class Litestack::InstallGenerator < Rails::Generators::Base
2
+ source_root File.expand_path("templates", __dir__)
3
+
4
+ # Force copy configuratioon files so Rails installs don't ask questions
5
+ # that less experienced people might not understand. The more Sr folks.
6
+ # will know to check git to look at what changed.
7
+ def modify_database_adapater
8
+ copy_file "database.yml", "config/database.yml", force: true
9
+ end
10
+
11
+ def modify_action_cable_adapter
12
+ copy_file "cable.yml", "config/cable.yml", force: true
13
+ end
14
+
15
+ def modify_cache_store_adapter
16
+ gsub_file "config/environments/production.rb",
17
+ "# config.cache_store = :mem_cache_store",
18
+ "config.cache_store = :litecache"
19
+ end
20
+
21
+ def modify_active_job_adapter
22
+ gsub_file "config/environments/production.rb",
23
+ "# config.active_job.queue_adapter = :resque",
24
+ "config.active_job.queue_adapter = :litejob"
25
+ end
26
+
27
+ def modify_gitignore
28
+ append_file ".gitignore", <<~TEXT
29
+
30
+ # Ignore default Litestack SQLite databases.
31
+ /db/**/*.sqlite3
32
+ /db/**/*.sqlite3-*
33
+ TEXT
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ development:
2
+ adapter: litecable
3
+
4
+ test:
5
+ adapter: test
6
+
7
+ staging:
8
+ adapter: litecable
9
+
10
+ production:
11
+ adapter: litecable
@@ -0,0 +1,34 @@
1
+ # SQLite. Versions 3.8.0 and up are supported.
2
+ # gem install sqlite3
3
+ #
4
+ # Ensure the SQLite 3 gem is defined in your Gemfile
5
+ # gem "sqlite3"
6
+ #
7
+ # `Litesupport.root.join("data.sqlite3")` stores
8
+ # application data in the path `./db/#{Rails.env}/data.sqlite3`
9
+ #
10
+ # idle_timeout should be set to zero, to avoid recycling sqlite connections
11
+ # and losing the page cache
12
+ #
13
+ default: &default
14
+ adapter: litedb
15
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
16
+ idle_timeout: 0
17
+ database: <%= Litesupport.root.join("data.sqlite3") %>
18
+
19
+ development:
20
+ <<: *default
21
+
22
+ # Warning: The database defined as "test" will be erased and
23
+ # re-generated from your development database when you run "rake".
24
+ # Do not set this db to the same as development or production.
25
+ test:
26
+ <<: *default
27
+
28
+ # Warning: Make sure your production database path is on a persistent
29
+ # volume, otherwise your application data could be deleted between deploys.
30
+ #
31
+ # You may also set the Litesupport.root in production via the
32
+ # `LITESTACK_DATA_PATH` environment variable.
33
+ production:
34
+ <<: *default
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+ require 'hanami/router'
3
+ require 'tilt'
4
+ require 'erubi'
5
+
6
+ # require relative so we pick the gem version that corresponds to the liteboard binary
7
+ require_relative '../../litestack/litemetric'
8
+
9
+ class Liteboard
10
+
11
+ @@resolutions = {'minute' => [300, 12], 'hour' => [3600, 24], 'day' => [3600*24, 7], 'week' => [3600*24*7, 53], 'year' => [3600*24*365, 100] }
12
+ @@res_mapping = {'hour' => 'minute', 'day' => 'hour', 'week' => 'day', 'year' => 'week'}
13
+ @@templates = {}
14
+ @@app = Hanami::Router.new do
15
+
16
+ get "/", to: ->(env) do
17
+ Liteboard.new(env).call(:index)
18
+ end
19
+
20
+ get "/topics/Litejob", to: ->(env) do
21
+ Liteboard.new(env).call(:litejob)
22
+ end
23
+
24
+ get "/topics/Litecache", to: ->(env) do
25
+ Liteboard.new(env).call(:litecache)
26
+ end
27
+
28
+ get "/topics/Litedb", to: ->(env) do
29
+ Liteboard.new(env).call(:litedb)
30
+ end
31
+
32
+ get "/topics/Litecable", to: ->(env) do
33
+ Liteboard.new(env).call(:litecable)
34
+ end
35
+
36
+ end
37
+
38
+
39
+ def initialize(env)
40
+ @env = env
41
+ @params = @env["router.params"]
42
+ @running = true
43
+ @lm = Litemetric.instance
44
+ end
45
+
46
+ def params(key)
47
+ URI.decode_uri_component("#{@params[key]}")
48
+ end
49
+
50
+ def call(method)
51
+ before
52
+ res = send(method)
53
+ after(res)
54
+ end
55
+
56
+ def after(body=nil)
57
+ [200, {'Cache-Control' => 'no-cache'}, [body]]
58
+ end
59
+
60
+ def before
61
+ @res = params(:res) || 'day'
62
+ @resolution = @@res_mapping[@res]
63
+ if not @resolution
64
+ @res = 'day'
65
+ @resolution = @@res_mapping[@res]
66
+ end
67
+ @step = @@resolutions[@resolution][0]
68
+ @count = @@resolutions[@resolution][1]
69
+ @order = params(:order)
70
+ @order = nil if @order == ''
71
+ @dir = params(:dir)
72
+ @dir = 'desc' if @dir.nil? || @dir == ''
73
+ @dir = @dir.downcase
74
+ @idir = if @dir == "asc"
75
+ "desc"
76
+ else
77
+ "asc"
78
+ end
79
+ @search = params(:search)
80
+ @search = nil if @search == ''
81
+ @topics = @lm.topic_summaries(@resolution, @step * @count, @order, @dir, @search)
82
+ end
83
+
84
+ def index
85
+ @order = 'topic' unless @order
86
+ @topics.each do |topic|
87
+ data_points = @lm.topic_data_points(@step, @count, @resolution, topic[0])
88
+ topic << data_points.collect{|r| [r[0],r[2] || 0]}
89
+ end
90
+ render :index
91
+ end
92
+
93
+ def litecache
94
+ @order = 'rcount' unless @order
95
+ @topic = 'Litecache'
96
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
97
+ @events.each do |event|
98
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
99
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount']]}
100
+ event['values'] = data_points.collect{|r| [r['rtime'],r['ravg']]}
101
+ end
102
+ @snapshot = read_snapshot(@topic)
103
+ @size = @snapshot[0][:summary][:size] rescue 0
104
+ @max_size = @snapshot[0][:summary][:max_size] rescue 0
105
+ @full = (@size / @max_size)*100 rescue 0
106
+ @entries = @snapshot[0][:summary][:entries] rescue 0
107
+ @gets = @events.find{|t| t['name'] == 'get'}
108
+ @sets = @events.find{|t| t['name'] == 'set'}
109
+ @reads = @gets['rcount'] rescue 0
110
+ @writes = @sets['rcount'] rescue 0
111
+ @hitrate = @gets['ravg'] rescue 0
112
+ @hits = @reads * @hitrate
113
+ @misses = @reads - @hits
114
+ @reads_vs_writes = @gets['counts'].collect.with_index{|obj, i| obj.clone << @sets['counts'][i][1] } rescue []
115
+ @hits_vs_misses = @gets['values'].collect.with_index{|obj, i| [obj[0], obj[1].to_f * @gets['counts'][i][1].to_f, (1 - obj[1].to_f) * @gets['counts'][i][1].to_f] } rescue []
116
+ @top_reads = @lm.keys_summaries(@topic, 'get', @resolution, @order, @dir, nil, @step * @count).first(8)
117
+ @top_writes = @lm.keys_summaries(@topic, 'set', @resolution, @order, @dir, nil, @step * @count).first(8)
118
+ render :litecache
119
+ end
120
+
121
+ def litedb
122
+ @order = 'rcount' unless @order
123
+ @topic = 'Litedb'
124
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
125
+ @events.each do |event|
126
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
127
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount'] || 0]}
128
+ event['values'] = data_points.collect{|r| [r['rtime'],r['rtotal'] || 0]}
129
+ end
130
+ @snapshot = read_snapshot(@topic)
131
+ @size = @snapshot[0][:summary][:size] rescue 0
132
+ @tables = @snapshot[0][:summary][:tables] rescue 0
133
+ @indexes = @snapshot[0][:summary][:indexes] rescue 0
134
+ @gets = @events.find{|t| t['name'] == 'Read'}
135
+ @sets = @events.find{|t| t['name'] == 'Write'}
136
+ @reads = @gets['rcount'] rescue 0
137
+ @writes = @sets['rcount'] rescue 0
138
+ @time = @gets['ravg'] rescue 0
139
+ @reads_vs_writes = @gets['counts'].collect.with_index{|obj, i| obj.clone << @sets['counts'][i][1] } rescue []
140
+ @reads_vs_writes_times = @gets['values'].collect.with_index{|obj, i| [obj[0], obj[1], @sets['values'][i][1].to_f] } rescue []
141
+ @read_times = @gets['rtotal'] rescue 0
142
+ @write_times = @sets['rtotal'] rescue 0
143
+ @slowest = @lm.keys_summaries(@topic, 'Read', @resolution, 'ravg', 'desc', nil, @step * @count).first(8)
144
+ @slowest += @lm.keys_summaries(@topic, 'Write', @resolution, 'ravg', 'desc', nil, @step * @count).first(8)
145
+ @slowest = @slowest.sort{|a, b| a['ravg'] <=> b['ravg']}.reverse.first(8)
146
+ @popular = @lm.keys_summaries(@topic, 'Read', @resolution, 'rtotal', 'desc', nil, @step * @count).first(8)
147
+ @popular += @lm.keys_summaries(@topic, 'Write', @resolution, 'rtotal', 'desc', nil, @step * @count).first(8)
148
+ @popular = @popular.sort{|a, b| a['rtotal'] <=> b['rtotal']}.reverse.first(8)
149
+ render :litedb
150
+ end
151
+
152
+ def litejob
153
+ @order = 'rcount' unless @order
154
+ @topic = 'Litejob'
155
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
156
+ @events.each do |event|
157
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
158
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount'] || 0]}
159
+ event['values'] = data_points.collect{|r| [r['rtime'],r['rtotal'] || 0]}
160
+ end
161
+ @snapshot = read_snapshot(@topic)
162
+ @size = @snapshot[0][:summary][:size] rescue 0
163
+ @jobs = @snapshot[0][:summary][:jobs] rescue 0
164
+ @queues = @snapshot[0][:queues] rescue {}
165
+ @processed_jobs = @events.find{|e|e['name'] == 'perform'}
166
+ @processed_count = @processed_jobs['rcount'] rescue 0
167
+ @processing_time = @processed_jobs['rtotal'] rescue 0
168
+ keys_summaries = @lm.keys_summaries(@topic, 'perform', @resolution, 'rcount', 'desc', nil, @step * @count)
169
+ @processed_count_by_queue = keys_summaries.collect{|r|[r['key'], r['rcount']]}
170
+ @processing_time_by_queue = keys_summaries.collect{|r|[r['key'], r['rtotal']]} #.sort{|r1, r2| r1['rtotal'] > r2['rtotal'] }
171
+ @processed_count_over_time = @events.find{|e| e['name'] == 'perform'}['counts'] rescue []
172
+ @processing_time_over_time = @events.find{|e| e['name'] == 'perform'}['values'] rescue []
173
+ @processed_count_over_time_by_queues = []
174
+ @processing_time_over_time_by_queues = []
175
+ keys = ['Time']
176
+ keys_summaries.each_with_index do |summary,i|
177
+ key = summary['key']
178
+ keys << key
179
+ data_points = @lm.key_data_points(@step, @count, @resolution, @topic, 'perform', key)
180
+ if i == 0
181
+ data_points.each do |dp|
182
+ @processed_count_over_time_by_queues << [dp['rtime']]
183
+ @processing_time_over_time_by_queues << [dp['rtime']]
184
+ end
185
+ end
186
+ data_points.each_with_index do |dp, j|
187
+ @processed_count_over_time_by_queues[j] << (dp['rcount'] || 0)
188
+ @processing_time_over_time_by_queues[j] << (dp['rtotal'] || 0)
189
+ end
190
+ end
191
+ @processed_count_over_time_by_queues.unshift(keys)
192
+ @processing_time_over_time_by_queues.unshift(keys)
193
+ render :litejob
194
+ end
195
+
196
+ def litecable
197
+ @order = 'rcount' unless @order
198
+ @topic = 'Litecable'
199
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
200
+ @events.each do |event|
201
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
202
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount'] || 0]}
203
+ end
204
+
205
+ @subscription_count = @events.find{|t| t['name'] == 'subscribe'}['rcount'] rescue 0
206
+ @broadcast_count = @events.find{|t| t['name'] == 'broadcast'}['rcount'] rescue 0
207
+ @message_count = @events.find{|t| t['name'] == 'message'}['rcount'] rescue 0
208
+
209
+ @subscriptions_over_time = @events.find{|t| t['name'] == 'subscribe'}['counts'] rescue []
210
+ @broadcasts_over_time = @events.find{|t| t['name'] == 'broadcast'}['counts'] rescue []
211
+ @messages_over_time = @events.find{|t| t['name'] == 'message'}['counts'] rescue []
212
+ @messages_over_time = @messages_over_time.collect.with_index{|msg, i| [msg[0], @broadcasts_over_time[i][1], msg[1]]}
213
+
214
+ @top_subscribed_channels = @lm.keys_summaries(@topic, 'subscribe', @resolution, @order, @dir, @search, @step * @count).first(8)
215
+ @top_messaged_channels = @lm.keys_summaries(@topic, 'message', @resolution, @order, @dir, @search, @step * @count).first(8)
216
+ render :litecable
217
+ end
218
+
219
+ def index_url
220
+ "/?res=#{@res}&order=#{@order}&dir=#{@dir}&search=#{@search}"
221
+ end
222
+
223
+ def topic_url(topic)
224
+ "/topics/#{encode(topic)}?res=#{@res}&order=#{@order}&dir=#{@dir}&search=#{@search}"
225
+ end
226
+
227
+ def index_sort_url(field)
228
+ "/?#{compose_query(field)}"
229
+ end
230
+
231
+ def topic_sort_url(field)
232
+ "/topics/#{encode(@topic)}?#{compose_query(field)}"
233
+ end
234
+
235
+ def event_sort_url(field)
236
+ "/topics/#{encode(@topic)}/events/#{encode(@event)}?#{compose_query(field)}"
237
+ end
238
+
239
+ def compose_query(field)
240
+ field.downcase!
241
+ "res=#{@res}&order=#{field}&dir=#{@order == field ? @idir : @dir}&search=#{@search}"
242
+ end
243
+
244
+ def sorted?(field)
245
+ @order == field
246
+ end
247
+
248
+ def dir(field)
249
+ if sorted?(field)
250
+ if @dir == 'asc'
251
+ return "<span class='material-icons'>arrow_drop_up</span>"
252
+ else
253
+ return "<span class='material-icons'>arrow_drop_down</span>"
254
+ end
255
+ end
256
+ '&nbsp;&nbsp;'
257
+ end
258
+
259
+ def encode(text)
260
+ URI.encode_uri_component(text)
261
+ end
262
+
263
+ def round(float)
264
+ return 0 unless float.is_a? Numeric
265
+ ((float * 100).round).to_f / 100
266
+ end
267
+
268
+ def format(float)
269
+ string = float.to_s
270
+ whole, decimal = string.split('.')
271
+ whole = whole.split('').reverse.each_slice(3).map(&:join).join(',').reverse
272
+ whole = [whole, decimal].join('.') if decimal
273
+ whole
274
+ end
275
+
276
+ def self.app
277
+ @@app
278
+ end
279
+
280
+ private
281
+
282
+ def read_snapshot(topic)
283
+ snapshot = @lm.snapshot(topic)
284
+ if snapshot.empty?
285
+ snapshot = []
286
+ else
287
+ snapshot[0] = Oj.load(snapshot[0]) unless snapshot[0].nil?
288
+ end
289
+ snapshot
290
+ end
291
+
292
+ def render(tpl_name)
293
+ layout = Tilt.new("#{__dir__}/views/layout.erb")
294
+ tpl_path = "#{__dir__}/views/#{tpl_name.to_s}.erb"
295
+ tpl = Tilt.new(tpl_path)
296
+ res = layout.render(self){tpl.render(self)}
297
+ end
298
+
299
+
300
+ end
301
+
302
+
303
+
304
+
305
+ #Rack::Server.start({app: Litebaord.app, daemonize: false})
@@ -0,0 +1,32 @@
1
+ <h5><a href="/?res=<%=@res%>">All Topics</a> > <a href="/topics/<%=@topic%>?res=<%=@res%>"><%=@topic%></a> > <%= @event %></h5>
2
+ <div id="search"><form><input id="search-field" type="text" placeholder="Search keys" onkeydown="search_kd(this)" onkeyup="search_ku(this)" value="<%=@search%>"/></form></div>
3
+ <table class="table sortable">
4
+ <tr>
5
+ <th width="16%" class="<%='sorted' if @order == 'key'%>"><a href="<%=event_sort_url('key')%>">Key</a> <%=dir('key')%></th>
6
+ <th width="8%" class="<%='sorted' if @order == 'rcount'%>"><a href="<%=event_sort_url('rcount')%>">Event Count</a> <%=dir('rcount')%></th>
7
+ <th width="8%" class="<%='sorted' if @order == 'ravg'%>"><a href="<%=event_sort_url('ravg')%>">Avg Value</a> <%=dir('ravg')%></th>
8
+ <th width="8%" class="<%='sorted' if @order == 'rtotal'%>"><a href="<%=event_sort_url('rtotal')%>">Total Value</a> <%=dir('rtotal')%></th>
9
+ <th width="8%" class="<%='sorted' if @order == 'rmin'%>"><a href="<%=event_sort_url('rmin')%>">Min Value</a> <%=dir('rmin')%></th>
10
+ <th width="8%" class="<%='sorted' if @order == 'rmax'%>"><a href="<%=event_sort_url('rmax')%>">Max Value</a> <%=dir('rmax')%></th>
11
+ <th width="22%">Events over time</th>
12
+ <th width="22%">Average value over time</th>
13
+ </tr>
14
+ <% @keys.each do |key|%>
15
+ <tr>
16
+ <td title="<%=key[0]%>"><div class="label"><span><%=key[0]%></span></div></td>
17
+ <td><%=key[1]%></td>
18
+ <td><%="%0.2f" % [key[2]] if key[2]%></td>
19
+ <td><%="%0.2f" % [key[3]] if key[3]%></td>
20
+ <td><%="%0.2f" % [key[4]] if key[4]%></td>
21
+ <td><%="%0.2f" % [key[5]] if key[5]%></td>
22
+ <td class="chart"><span class="inlinecolumn hidden" data-label="Count"><%=Oj.dump(key[6]) if key[7]%></span></td>
23
+ <td class="chart"><span class="inlinecolumn hidden" data-label="Avg Value"><%=Oj.dump(key[7]) if key[7]%></span></td>
24
+ </tr>
25
+ <% end %>
26
+ <% if @keys.empty? %>
27
+ <tr>
28
+ <td class="empty" colspan="8">No data to display</td>
29
+ </tr>
30
+ <% end %>
31
+ </table>
32
+