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
@@ -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