litestack 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,9 +1,15 @@
1
1
  # all components should require the support module
2
2
  require_relative 'litesupport'
3
3
 
4
+ # all measurable components should require the litemetric class
5
+ require_relative 'litemetric'
6
+
4
7
  # Litedb inherits from the SQLite3::Database class and adds a few initialization options
5
8
  class Litedb < ::SQLite3::Database
6
9
 
10
+ # add litemetric support
11
+ include Litemetric::Measurable
12
+
7
13
  # overrride the original initilaizer to allow for connection configuration
8
14
  def initialize(file, options = {}, zfs = nil )
9
15
  if block_given?
@@ -15,6 +21,13 @@ class Litedb < ::SQLite3::Database
15
21
  super(file, options, zfs)
16
22
  init unless options[:noinit] == true
17
23
  end
24
+ @running = true
25
+ @collecting_metrics = options[:metrics]
26
+ collect_metrics if @collecting_metrics
27
+ end
28
+
29
+ def collecting_metrics?
30
+ @collecting_metrics
18
31
  end
19
32
 
20
33
  # enforce immediate mode to avoid deadlocks for a small performance penalty
@@ -22,6 +35,67 @@ class Litedb < ::SQLite3::Database
22
35
  super(mode)
23
36
  end
24
37
 
38
+ # return the size of the database file
39
+ def size
40
+ execute("SELECT s.page_size * c.page_count FROM pragma_page_size() AS s, pragma_page_count() AS c")[0][0]
41
+ end
42
+
43
+ def schema_object_count(type = nil)
44
+ execute("SELECT count(*) FROM SQLITE_MASTER WHERE iif(?1 IS NOT NULL, type = ?1, TRUE)", type)[0][0]
45
+ end
46
+
47
+ # collect snapshot information
48
+ def snapshot
49
+ {
50
+ summary: {
51
+ path: filename,
52
+ journal_mode: journal_mode,
53
+ synchronous: synchronous,
54
+ size: size.to_f / (1024 * 1024),
55
+ tables: schema_object_count('table'),
56
+ indexes: schema_object_count('index')
57
+ }
58
+ }
59
+ end
60
+
61
+ # override prepare to return Litedb::Statement (and pass the sql to it)
62
+ def prepare(sql)
63
+ stmt = Litedb::Statement.new(self, sql)
64
+ stmt.sql = sql.strip.upcase
65
+ return stmt unless block_given?
66
+ begin
67
+ yield stmt
68
+ ensure
69
+ stmt.close unless stmt.closed?
70
+ end
71
+ end
72
+
73
+ # override execute to capture metrics
74
+ def execute(sql, bind_vars = [], *args, &block)
75
+ if bind_vars.nil? || !args.empty?
76
+ if args.empty?
77
+ bind_vars = []
78
+ else
79
+ bind_vars = [bind_vars] + args
80
+ end
81
+ end
82
+
83
+ prepare(sql) do |stmt|
84
+ measure(stmt.stmt_type, stmt.sql) do
85
+ stmt.bind_params(bind_vars)
86
+ stmt = SQLite3::ResultSet.new self, stmt
87
+ end
88
+ if block_given?
89
+ stmt.each do |row|
90
+ yield row
91
+ end
92
+ else
93
+ stmt.to_a
94
+ end
95
+ end
96
+
97
+ end
98
+
25
99
  private
26
100
 
27
101
  # default connection configuration values
@@ -41,7 +115,56 @@ class Litedb < ::SQLite3::Database
41
115
  # increase the local connection cache to 2000 pages
42
116
  self.cache_size = 2000
43
117
  end
44
-
118
+
45
119
  end
46
120
 
121
+ # the Litedb::Statement also inherits from SQLite3::Statement
122
+ class Litedb::Statement < SQLite3::Statement
123
+
124
+ include Litemetric::Measurable
125
+
126
+ attr_accessor :sql
127
+
128
+ def initialize(db, sql)
129
+ super(db, sql)
130
+ collect_metrics if db.collecting_metrics?
131
+ end
132
+
133
+ def metrics_identifier
134
+ "Litedb" # overridden to match the parent class
135
+ end
136
+
137
+ # return the type of the statement
138
+ def stmt_type
139
+ @stmt_type ||= detect_stmt_type
140
+ end
141
+
142
+ def detect_stmt_type
143
+ if @sql.start_with?("SEL") || @sql.start_with?("WITH")
144
+ "Read"
145
+ elsif @sql.start_with?("CRE") || @sql.start_with?("ALT") || @sql.start_with?("DRO")
146
+ "Schema change"
147
+ elsif @sql.start_with?("PRA")
148
+ "Pragma"
149
+ else
150
+ "Write"
151
+ end
152
+ end
153
+
154
+ # overriding each to measure the query time (plus the processing time as well, sadly)
155
+ def each
156
+ measure(stmt_type, @sql) do
157
+ super
158
+ end
159
+ end
160
+
161
+ # overriding execute to measure the query time
162
+ def execute(*bind_vars)
163
+ res = nil
164
+ measure(stmt_type, @sql) do
165
+ res = super(*bind_vars)
166
+ end
167
+ res
168
+ end
47
169
 
170
+ end
@@ -74,11 +74,11 @@ module Litejob
74
74
  end
75
75
 
76
76
  def queue
77
- @@queue ||= "default"
77
+ @queue_name ||= "default"
78
78
  end
79
79
 
80
80
  def queue=(queue_name)
81
- @@queue = queue_name.to_s
81
+ @queue_name = queue_name.to_s
82
82
  end
83
83
 
84
84
  def options
@@ -19,7 +19,7 @@ class Litejobqueue < Litequeue
19
19
  # can be overriden by passing new options in a hash
20
20
  # to Litejobqueue.new, it will also be then passed to the underlying Litequeue object
21
21
  # config_path: "./litejob.yml" -> were to find the configuration file (if any)
22
- # path: "./queue.db"
22
+ # path: "./db/queue.db"
23
23
  # mmap_size: 128 * 1024 * 1024 -> 128MB to be held in memory
24
24
  # sync: 1 -> sync only when checkpointing
25
25
  # queues: [["default", 1, "spawn"]] -> an array of queues to process
@@ -32,7 +32,7 @@ class Litejobqueue < Litequeue
32
32
  # This can be particularly useful for long running, IO bound jobs. It is not recommended though for threaded environments, as it can result in creating many threads that may consudme a lot of memory.
33
33
  DEFAULT_OPTIONS = {
34
34
  config_path: "./litejob.yml",
35
- path: "./queue.db",
35
+ path: Litesupport.root.join("queue.sqlite3"),
36
36
  queues: [["default", 1]],
37
37
  workers: 5,
38
38
  retries: 5,
@@ -79,6 +79,11 @@ class Litejobqueue < Litequeue
79
79
  pgroups[q[1]] << [q[0], q[2] == "spawn"]
80
80
  end
81
81
  @queues = pgroups.keys.sort.reverse.collect{|p| [p, pgroups[p]]}
82
+ collect_metrics if @options[:metrics]
83
+ end
84
+
85
+ def metrics_identifier
86
+ "Litejob" # overrides default identifier
82
87
  end
83
88
 
84
89
  # push a job to the queue
@@ -164,12 +169,7 @@ class Litejobqueue < Litequeue
164
169
  def job_finished
165
170
  Litesupport.synchronize(@mutex){@jobs_in_flight -= 1}
166
171
  end
167
-
168
- # return a hash encapsulating the info about the current jobqueue
169
- def snapshot
170
- info
171
- end
172
-
172
+
173
173
  # optionally run a job in its own context
174
174
  def schedule(spawn = false, &block)
175
175
  if spawn
@@ -13,11 +13,12 @@ class Litemetric
13
13
 
14
14
  DEFAULT_OPTIONS = {
15
15
  config_path: "./litemetric.yml",
16
- path: "./metrics.db",
16
+ path: Litesupport.root.join("metrics.sqlite3"),
17
17
  sync: 1,
18
- mmap_size: 16 * 1024 * 1024, # 16MB of memory to easily process 1 year worth of data
19
- flush_interval: 10, # flush data every 1 minute
20
- summarize_interval: 10 # summarize data every 1 minute
18
+ mmap_size: 128 * 1024 * 1024, # 16MB of memory to easily process 1 year worth of data
19
+ flush_interval: 10, # flush data every 10 seconds
20
+ summarize_interval: 30, # summarize data every 1/2 minute
21
+ snapshot_interval: 10*60 # snapshot every 10 minutes
21
22
  }
22
23
 
23
24
  RESOLUTIONS = {
@@ -27,8 +28,20 @@ class Litemetric
27
28
  week: 7*24*3600 # 1 week (lowest resolution)
28
29
  }
29
30
 
31
+ # :nodoc:
32
+ def self.options=(options)
33
+ # an ugly hack to pass options to a singleton
34
+ # need to rethink the whole singleton thing
35
+ @options = options
36
+ end
37
+
38
+ def self.options
39
+ @options
40
+ end
41
+
30
42
  # :nodoc:
31
43
  def initialize(options = {})
44
+ options = options.merge(Litemetric.options) if Litemetric.options
32
45
  init(options)
33
46
  end
34
47
 
@@ -41,36 +54,18 @@ class Litemetric
41
54
 
42
55
  ## event capturing
43
56
  ##################
44
-
45
- def capture(topic, event, key=event, value=nil)
46
- if key.is_a? Array
47
- key.each{|k| capture_single_key(topic, event, k, value)}
48
- else
49
- capture_single_key(topic, event, key, value)
50
- end
51
- end
52
57
 
53
- def capture_single_key(topic, event, key=event, value=nil)
54
- @mutex.synchronize do
55
- time_slot = current_time_slot # should that be 5 minutes?
56
- topic_slot = @metrics[topic]
57
- if event_slot = topic_slot[event]
58
- if key_slot = event_slot[key]
59
- if key_slot[time_slot]
60
- key_slot[time_slot][:count] += 1
61
- key_slot[time_slot][:value] += value unless value.nil?
62
- else # new time slot
63
- key_slot[time_slot] = {count: 1, value: value}
64
- end
65
- else
66
- event_slot[key] = {time_slot => {count: 1, value: value}}
67
- end
68
- else # new event
69
- topic_slot[event] = {key => {time_slot => {count: 1, value: value}}}
70
- end
71
- end
58
+ def current_time_slot
59
+ (Time.now.to_i / 300) * 300
72
60
  end
73
61
 
62
+ def capture(topic, event, key=event, value=nil)
63
+ @collector.capture(topic, event, key, value, current_time_slot)
64
+ end
65
+
66
+ def capture_snapshot(topic, state)
67
+ run_stmt(:capture_state, topic, Oj.dump(state))
68
+ end
74
69
 
75
70
  ## event reporting
76
71
  ##################
@@ -78,17 +73,48 @@ class Litemetric
78
73
  def topics
79
74
  run_stmt(:list_topics).to_a
80
75
  end
81
-
82
- def event_names(resolution, topic)
83
- run_stmt(:list_event_names, resolution, topic).to_a
76
+
77
+ def topic_summaries(resolution, count, order, dir, search)
78
+ search = "%#{search}%" if search
79
+ if dir.downcase == "desc"
80
+ run_stmt(:topics_summaries, resolution, count, order, search).to_a
81
+ else
82
+ run_stmt(:topics_summaries_asc, resolution, count, order, search).to_a
83
+ end
84
84
  end
85
-
86
- def keys(resolution, topic, event_name)
87
- run_stmt(:list_event_keys, resolution, topic, event_name).to_a
85
+
86
+ def events_summaries(topic, resolution, order, dir, search, count)
87
+ search = "%#{search}%" if search
88
+ if dir.downcase == "desc"
89
+ run_stmt_hash(:events_summaries, topic, resolution, order, search, count)
90
+ else
91
+ run_stmt_hash(:events_summaries_asc, topic, resolution, order, search, count)
92
+ end
93
+ end
94
+
95
+ def keys_summaries(topic, event, resolution, order, dir, search, count)
96
+ search = "%#{search}%" if search
97
+ if dir.downcase == "desc"
98
+ run_stmt_hash(:keys_summaries, topic, event, resolution, order, search, count).to_a
99
+ else
100
+ run_stmt_hash(:keys_summaries_asc, topic, event, resolution, order, search, count).to_a
101
+ end
88
102
  end
89
103
 
90
- def event_data(resolution, topic, event_name, key)
91
- run_stmt(:list_events_by_key, resolution, topic, event_name, key).to_a
104
+ def topic_data_points(step, count, resolution, topic)
105
+ run_stmt(:topic_data_points, step, count, resolution, topic).to_a
106
+ end
107
+
108
+ def event_data_points(step, count, resolution, topic, event)
109
+ run_stmt_hash(:event_data_points, step, count, resolution, topic, event).to_a
110
+ end
111
+
112
+ def key_data_points(step, count, resolution, topic, event, key)
113
+ run_stmt_hash(:key_data_points, step, count, resolution, topic, event, key).to_a
114
+ end
115
+
116
+ def snapshot(topic)
117
+ run_stmt(:snapshot, topic)[0].to_a
92
118
  end
93
119
 
94
120
  ## summarize data
@@ -107,9 +133,23 @@ class Litemetric
107
133
  ###################
108
134
 
109
135
  private
110
-
136
+
137
+ def run_stmt_hash(stmt, *args)
138
+ res = run_stmt(stmt, *args)
139
+ cols = run_stmt_method(stmt, :columns)
140
+ hashes = []
141
+ res.each do | row |
142
+ hash = {}
143
+ row.each_with_index do |field, i|
144
+ hash[cols[i]] = field
145
+ end
146
+ hashes << hash
147
+ end
148
+ hashes
149
+ end
150
+
111
151
  def exit_callback
112
- puts "--- Litemetric detected an exit, flushing metrics"
152
+ STDERR.puts "--- Litemetric detected an exit, flushing metrics"
113
153
  @running = false
114
154
  flush
115
155
  end
@@ -118,9 +158,10 @@ class Litemetric
118
158
  super
119
159
  @metrics = {}
120
160
  @registered = {}
121
- @flusher = create_flusher
122
- @summarizer = create_summarizer
123
161
  @mutex = Litesupport::Mutex.new
162
+ @collector = Litemetric::Collector.new({dbpath: @options[:path]})
163
+ @summarizer = create_summarizer
164
+ @flusher = create_flusher
124
165
  end
125
166
 
126
167
  def current_time_slot
@@ -128,53 +169,20 @@ class Litemetric
128
169
  end
129
170
 
130
171
  def flush
131
- to_delete = []
132
- @conn.acquire do |conn|
133
- conn.transaction(:immediate) do
134
- @metrics.each_pair do |topic, event_hash|
135
- event_hash.each_pair do |event, key_hash|
136
- key_hash.each_pair do |key, time_hash|
137
- time_hash.each_pair do |time, data|
138
- conn.stmts[:capture_event].execute!(topic, event.to_s, key, time, data[:count], data[:value]) if data
139
- time_hash[time] = nil
140
- to_delete << [topic, event, key, time]
141
- end
142
- end
143
- end
144
- end
145
- end
146
- end
147
- to_delete.each do |r|
148
- @metrics[r[0]][r[1]][r[2]].delete(r[3])
149
- @metrics[r[0]][r[1]].delete(r[2]) if @metrics[r[0]][r[1]][r[2]].empty?
150
- @metrics[r[0]].delete(r[1]) if @metrics[r[0]][r[1]].empty?
151
- end
152
- end
172
+ @collector.flush
173
+ end
153
174
 
154
175
  def create_connection
155
- conn = super
156
- conn.wal_autocheckpoint = 10000
157
- sql = YAML.load_file("#{__dir__}/litemetric.sql.yml")
158
- version = conn.get_first_value("PRAGMA user_version")
159
- sql["schema"].each_pair do |v, obj|
160
- if v > version
161
- conn.transaction do
162
- obj.each{|k, s| conn.execute(s)}
163
- conn.user_version = v
164
- end
165
- end
166
- end
167
- sql["stmts"].each { |k, v| conn.stmts[k.to_sym] = conn.prepare(v) }
168
- conn
176
+ super("#{__dir__}/litemetric.sql.yml") do |conn|
177
+ conn.wal_autocheckpoint = 10000 # checkpoint after 10000 pages are written
178
+ end
169
179
  end
170
180
 
171
181
  def create_flusher
172
182
  Litesupport.spawn do
173
183
  while @running do
174
184
  sleep @options[:flush_interval]
175
- @mutex.synchronize do
176
- flush
177
- end
185
+ flush
178
186
  end
179
187
  end
180
188
  end
@@ -199,6 +207,16 @@ class Litemetric
199
207
  def collect_metrics
200
208
  @litemetric = Litemetric.instance
201
209
  @litemetric.register(metrics_identifier)
210
+ @snapshotter = create_snapshotter
211
+ end
212
+
213
+ def create_snapshotter
214
+ Litesupport.spawn do
215
+ while @running do
216
+ sleep @litemetric.options[:snapshot_interval]
217
+ capture_snapshot
218
+ end
219
+ end
202
220
  end
203
221
 
204
222
  def metrics_identifier
@@ -211,18 +229,89 @@ class Litemetric
211
229
  end
212
230
 
213
231
  def measure(event, key=event)
214
- return yield unless @litemetric
232
+ unless @litemetric
233
+ yield
234
+ return 0
235
+ end
215
236
  t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
216
- res = yield
237
+ yield
217
238
  t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
218
- value = (( t2 - t1 ) * 1000).round # capture time in milliseconds
239
+ value = t2 - t1
219
240
  capture(event, key, value)
220
- res
241
+ value # return value so other events can reuse it
221
242
  end
222
243
 
223
- def snapshot
224
- raise Litestack::NotImplementedError
244
+ def capture_snapshot
245
+ return unless @litemetric
246
+ state = snapshot if defined? snapshot
247
+ if state
248
+ @litemetric.capture_snapshot(metrics_identifier, state)
249
+ end
250
+ end
251
+
252
+ end
253
+ end
254
+
255
+ class Litemetric
256
+
257
+ class Collector
258
+
259
+ include Litesupport::Liteconnection
260
+
261
+ DEFAULT_OPTIONS = {
262
+ path: ":memory:",
263
+ sync: 1,
264
+ flush_interval: 3, # flush data every 1 minute
265
+ summarize_interval: 10, # summarize data every 1 minute
266
+ snapshot_interval: 1 # snapshot every 10 minutes
267
+ }
268
+
269
+ RESOLUTIONS = {
270
+ minute: 300, # 5 minutes (highest resolution)
271
+ hour: 3600, # 1 hour
272
+ day: 24*3600, # 1 day
273
+ week: 7*24*3600 # 1 week (lowest resolution)
274
+ }
275
+
276
+ def initialize(options = {})
277
+ init(options)
278
+ end
279
+
280
+ def capture(topic, event, key, value=nil, time=nil)
281
+ if key.is_a? Array
282
+ key.each{|k| capture_single_key(topic, event, k, value, time)}
283
+ else
284
+ capture_single_key(topic, event, key, value, time)
285
+ end
286
+ end
287
+
288
+ def capture_single_key(topic, event, key, value, time=nil)
289
+ run_stmt(:capture_event, topic.to_s, event.to_s, key.to_s, time ,1, value)
290
+ end
291
+
292
+ def flush
293
+ t = Time.now
294
+ limit = 1000 # migrate 1000 records at a time
295
+ count = run_stmt(:event_count)[0][0]
296
+ while count > 0
297
+ @conn.acquire do |conn|
298
+ conn.transaction(:immediate) do
299
+ conn.stmts[:migrate_events].execute!(limit)
300
+ conn.stmts[:delete_migrated_events].execute!(limit)
301
+ count = conn.stmts[:event_count].execute![0][0]
302
+ end
303
+ end
304
+ sleep 0.005 #give other threads a chance to run
305
+ end
306
+ end
307
+
308
+ def create_connection
309
+ super("#{__dir__}/litemetric_collector.sql.yml") do |conn|
310
+ conn.execute("ATTACH ? as m", @options[:dbpath].to_s)
311
+ conn.wal_autocheckpoint = 10000
312
+ end
225
313
  end
226
314
 
227
315
  end
316
+
228
317
  end