metric_system 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/metric_system.rb CHANGED
@@ -1,67 +1,33 @@
1
1
  require "expectation"
2
- require_relative "metric_system/core_extensions"
3
- require_relative "metric_system/sqlite3_extensions"
4
-
5
- # -- The MetricSystem module --------------------------------------------------
6
2
 
3
+ require_relative "metric_system/core_extensions"
7
4
  require "forwardable"
8
- class MetricSystem
9
- extend Forwardable
10
- delegate [:exec, :select, :transaction, :rollback] => :@db
11
-
12
- PERIODS = [
13
- [ :year, 31536000, "strftime('%Y-01-01', starts_at, 'unixepoch')" ],
14
- [ :month, 2592000, "strftime('%Y-%m-01', starts_at, 'unixepoch')" ],
15
- [ :week, 604800, "strftime('%Y-%m-%d', starts_at, 'unixepoch', 'weekday 1', '-7 days')" ],
16
- [ :day, 86400, "strftime('%Y-%m-%d', starts_at, 'unixepoch')" ],
17
- [ :hour, 3600, "strftime('%Y-%m-%d %H:00:00', starts_at, 'unixepoch')" ],
18
- [ :minute, 60, "strftime('%Y-%m-%d %H:%M:00', starts_at, 'unixepoch')" ],
19
- [ :second, 1, "strftime('%Y-%m-%d %H:%M:%S', starts_at, 'unixepoch')" ],
20
- ]
21
5
 
22
- def initialize(path)
23
- @db = SQLite3::Database.new(path)
6
+ module MetricSystem
7
+ extend self
24
8
 
25
- [ :counters, :gauges ].each do |name|
26
- exec <<-SQL
27
- CREATE TABLE IF NOT EXISTS #{name}(
28
- id INTEGER PRIMARY KEY,
9
+ attr :target, :database
29
10
 
30
- name NOT NULL, -- the event name
31
- value NOT NULL, -- the value
32
- starts_at TIMESTAMP NOT NULL DEFAULT (strftime('%s','now')) -- the timestamp
33
- );
11
+ def target=(target)
12
+ @target = @database = nil
34
13
 
35
- CREATE INDEX IF NOT EXISTS #{name}_idx1 ON #{name}(name, starts_at);
36
-
37
- CREATE TABLE IF NOT EXISTS aggregated_#{name}(
38
- id INTEGER PRIMARY KEY,
39
-
40
- name NOT NULL, -- the event name
41
- starts_at TIMESTAMP NOT NULL, -- the start-at timestamp
42
- duration NOT NULL, -- the duration (estimate, in secs.)
43
- period, -- the name of the period (year, day, etc.)
44
- sum, -- the sum of event values
45
- count, -- the count of events
46
- value -- the aggregated value
47
- );
48
-
49
- CREATE UNIQUE INDEX IF NOT EXISTS aggregated_#{name}_uidx1 ON aggregated_#{name}(name, starts_at, duration);
50
- CREATE INDEX IF NOT EXISTS aggregated_#{name}_idx2 ON aggregated_#{name}(starts_at);
51
- CREATE INDEX IF NOT EXISTS aggregated_#{name}_idx3 ON aggregated_#{name}(duration);
52
- SQL
14
+ case target
15
+ when nil
16
+ when String
17
+ require_relative "metric_system/database"
18
+ @target = @database = MetricSystem::Database.new(target)
19
+ else
20
+ require_relative "metric_system/io"
21
+ @target = MetricSystem::IO.new(target)
53
22
  end
54
-
55
- exec <<-SQL
56
- PRAGMA synchronous = NORMAL;
57
-
58
- CREATE VIEW IF NOT EXISTS aggregates AS
59
- SELECT * FROM aggregated_gauges
60
- UNION
61
- SELECT * FROM aggregated_counters
62
- SQL
63
23
  end
64
24
 
25
+ extend Forwardable
26
+ delegate [:aggregate, :select, :print, :run, :ask, :register] => :"@target"
27
+ delegate [:transaction] => :"@target"
28
+ delegate [:add_event] => :"@target"
29
+ delegate [:quit_server!] => :"@target"
30
+
65
31
  def gauge(name, value, starts_at = nil)
66
32
  add_event :gauges, name, value, starts_at
67
33
  end
@@ -70,99 +36,10 @@ class MetricSystem
70
36
  add_event :counters, name, value, starts_at
71
37
  end
72
38
 
73
- private
74
-
75
- def add_event(table, name, value, starts_at)
76
- # get names of all related events. An event "a.b.c" is actually
77
- # 3 events: "a", "a.b", "a.b.c"
78
- names = begin
79
- parts = name.split(".")
80
- parts.length.downto(1).map do |cnt|
81
- parts[0,cnt].join(".")
82
- end
83
- end
84
-
85
- if starts_at
86
- starts_at = Time.parse(starts_at) if starts_at.is_a?(String)
87
-
88
- names.each do |name|
89
- @db.run "INSERT INTO #{table}(name, value, starts_at) VALUES(?, ?, ?)", name, value, starts_at.to_i
90
- end
91
- else
92
- names.each do |name|
93
- @db.run "INSERT INTO #{table}(name, value) VALUES(?, ?)", name, value
94
- end
95
- end
96
- end
97
-
98
- public
99
-
100
- PERIODS_BY_KEY = PERIODS.by(&:first)
101
-
102
- def aggregate(*keys)
103
- if keys.empty?
104
- keys = PERIODS.map(&:first)
105
- end
106
-
107
- transaction do
108
- keys.each do |period|
109
- aggregate_for_period :period => period, :source => :counters, :dest => :aggregated_counters, :aggregate => "sum"
110
- end
111
-
112
- @db.exec "DELETE FROM counters"
113
- end
114
-
115
- transaction do
116
- keys.each do |period|
117
- aggregate_for_period :period => period, :source => :gauges, :dest => :aggregated_gauges, :aggregate => "CAST(sum AS FLOAT) / count"
118
- end
119
-
120
- @db.exec "DELETE FROM gauges"
39
+ def measure(name, starts_at = nil, &block)
40
+ start = Time.now
41
+ yield.tap do
42
+ gauge name, Time.now - start, starts_at
121
43
  end
122
44
  end
123
-
124
- def aggregate_for_period(options)
125
- expect! options => {
126
- :period => PERIODS.map(&:first)
127
- }
128
- period, source, dest, aggregate = options.values_at :period, :source, :dest, :aggregate
129
-
130
- _, duration, starts_at = PERIODS_BY_KEY[period]
131
-
132
- # sql expression to calculate value from sum and count of event values
133
- aggregate = source == :gauges ? "CAST(sum AS FLOAT) / count" : "sum"
134
-
135
- @db.exec <<-SQL
136
- CREATE TEMPORARY TABLE batch AS
137
- SELECT name, starts_at, SUM(sum) AS sum, SUM(count) AS count FROM
138
- (
139
- SELECT name AS name,
140
- #{starts_at} AS starts_at,
141
- SUM(value) AS sum,
142
- COUNT(value) AS count
143
- FROM #{source}
144
- GROUP BY name, starts_at
145
-
146
- UNION
147
-
148
- SELECT #{dest}.name AS name,
149
- #{dest}.starts_at AS starts_at,
150
- sum AS sum,
151
- count AS count
152
- FROM #{dest}
153
- INNER JOIN #{source} ON #{dest}.name=#{source}.name
154
- WHERE duration=#{duration}
155
- AND #{dest}.starts_at >= (SELECT MIN(starts_at) FROM #{source})
156
- )
157
- GROUP BY name, starts_at;
158
-
159
- INSERT OR REPLACE INTO #{dest}(name, starts_at, period, duration, sum, count, value)
160
- SELECT name, starts_at, '#{period}', #{duration}, sum, count, #{aggregate}
161
- FROM batch;
162
- SQL
163
-
164
- @db.exec <<-SQL
165
- DROP TABLE batch;
166
- SQL
167
- end
168
45
  end
@@ -0,0 +1,85 @@
1
+ class SQLite3::Database
2
+ # execute multiple SQL statements at once.
3
+ def exec(sql, *args)
4
+ args = prepare_arguments(args)
5
+
6
+ while sql =~ /\S/ do
7
+ statement = prepare(sql)
8
+
9
+ sql = statement.remainder
10
+ if statement.active?
11
+ statement.execute!(*args)
12
+ end
13
+ end
14
+
15
+ rescue
16
+ STDERR.puts "#{sql}: #{$!}"
17
+ raise
18
+ end
19
+
20
+ # -- cached queries ---------------------------------------------------------
21
+
22
+ private
23
+
24
+ def query(sql)
25
+ expect! sql => [ String, Symbol ]
26
+
27
+ if sql.is_a?(Symbol)
28
+ expect! sql => registry.keys
29
+ sql = registry.fetch(sql)
30
+ end
31
+
32
+ @queries ||= {}
33
+ @queries[sql] ||= SQLite3::Query.new sql, prepare(sql)
34
+ end
35
+
36
+ def prepare_arguments(args)
37
+ args.map do |arg|
38
+ case arg
39
+ when Time then arg.to_i
40
+ when Date then arg.to_time.to_i
41
+ else arg
42
+ end
43
+ end
44
+ end
45
+
46
+ public
47
+
48
+ def run(sql, *args)
49
+ query(sql).run *prepare_arguments(args)
50
+ end
51
+
52
+ def ask(sql, *args)
53
+ query(sql).ask *prepare_arguments(args)
54
+ end
55
+
56
+ # run a select like query. Returns an array of records.
57
+ def select(sql, *args)
58
+ query(sql).select *prepare_arguments(args)
59
+ end
60
+
61
+ def print(sql, *args)
62
+ require "pp"
63
+
64
+ results = select sql, *args
65
+ log_sql = sql.gsub(/\n/, " ").gsub(/\s+/, " ")
66
+ puts "=" * log_sql.length
67
+ puts log_sql
68
+ puts "-" * log_sql.length
69
+
70
+ results.each do |result|
71
+ pp result.to_a
72
+ end
73
+ puts "=" * log_sql.length
74
+ end
75
+
76
+ # -- query registry
77
+
78
+ def registry
79
+ @registry ||= {}
80
+ end
81
+
82
+ def register(name, query)
83
+ registry[name] = query
84
+ end
85
+ end
@@ -0,0 +1,77 @@
1
+ class SQLite3::Query
2
+ def initialize(sql, statement)
3
+ expect! statement => SQLite3::Statement
4
+
5
+ @sql, @statement = sql, statement
6
+ end
7
+
8
+ def run(*args)
9
+ # STDERR.puts "Q: #{@sql} #{args.map(&:inspect).join(", ")}"
10
+ @statement.execute *args
11
+ end
12
+
13
+ def select(*args)
14
+ @klass ||= SQLite3::Record.for_columns(@statement.columns)
15
+
16
+ ary = run(*args).map do |rec|
17
+ @klass.build *rec
18
+ end
19
+
20
+ ary.extend Description
21
+ ary.columns = @statement.columns
22
+ ary
23
+ end
24
+
25
+ module Description
26
+ attr :columns, true
27
+
28
+ # A Google Chart compatible data table; see
29
+ # https://developers.google.com/chart/interactive/docs/reference#dataparam
30
+ def data_table
31
+ cols = columns.map do |column|
32
+ type = case column
33
+ when /_at$/ then :datetime
34
+ when /_on$/ then :date
35
+ when /value/ then :number
36
+ else :string
37
+ end
38
+
39
+ { id: column, type: type, label: column }
40
+ end
41
+
42
+ rows = map { |record| convert_record record, cols }
43
+
44
+ { cols: cols, rows: rows }
45
+ end
46
+
47
+ def convert_record(record, cols)
48
+ values = cols.map do |col|
49
+ id, type = col.values_at(:id, :type)
50
+ v = record.send(id)
51
+
52
+ case type
53
+ when :date then f = v.strftime("%a %b, %Y")
54
+ when :datetime then f = v.inspect
55
+ when :number then f = v
56
+ else f = v
57
+ end
58
+
59
+
60
+ { v: v, f: f }
61
+ end
62
+
63
+ { c: values }
64
+ end
65
+ end
66
+
67
+ def ask(*args)
68
+ results = run(*args)
69
+ row = results.first
70
+ results.reset
71
+
72
+ if !row then nil
73
+ elsif row.length == 1 then row.first
74
+ else row
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ # manage SQLite3::Records
2
+ #
3
+ # The SQLite3::Record module is able to generate classes that are optimized
4
+ # for a specific set of columns. It is build on top of Struct, which is way
5
+ # faster than Hashes, for example.
6
+ module SQLite3::Record
7
+ module ClassMethods
8
+ attr :columns, true
9
+
10
+ private
11
+
12
+ def to_time(s)
13
+ case s
14
+ when String then Time.parse(s)
15
+ when Fixnum then Time.at(s)
16
+ else s
17
+ end
18
+ end
19
+
20
+ def to_date(s)
21
+ return unless time = to_time(s)
22
+ time.to_date
23
+ end
24
+
25
+ public
26
+
27
+ def build(*attrs)
28
+ attrs = columns.zip(attrs).map do |key, value|
29
+ case key
30
+ when /_at$/ then to_time(value)
31
+ when /_on$/ then to_date(value)
32
+ else value
33
+ end
34
+ end
35
+
36
+ new *attrs
37
+ end
38
+ end
39
+
40
+ def to_a
41
+ self.class.columns.map do |column| send(column) end
42
+ end
43
+
44
+ def to_hash
45
+ kvs = self.class.columns.inject([]) do |ary, column|
46
+ ary << column << send(column)
47
+ end
48
+
49
+ Hash[*kvs]
50
+ end
51
+
52
+ def self.for_columns(columns)
53
+ columns = columns.map(&:to_sym)
54
+
55
+ @@classes ||= {}
56
+ @@classes[columns] ||= begin
57
+ struct = Struct.new(*columns)
58
+ struct.extend SQLite3::Record::ClassMethods
59
+ struct.include SQLite3::Record
60
+
61
+ struct.columns = columns
62
+ struct
63
+ end
64
+ end
65
+ end
data/lib/to_js.rb ADDED
@@ -0,0 +1,55 @@
1
+ require "json"
2
+
3
+ class Object
4
+ def to_js
5
+ convert_to_js
6
+ end
7
+
8
+ alias :convert_to_js :to_json
9
+ end
10
+
11
+ class Array
12
+ def convert_to_js
13
+ "[" + map(&:convert_to_js).join(", ") + "]"
14
+ end
15
+ end
16
+
17
+ class Hash
18
+ def convert_to_js
19
+ "{" + map { |k,v| "#{k.convert_to_js}: #{v.convert_to_js}" }.join(", ") + "}"
20
+ end
21
+ end
22
+
23
+ class Date
24
+ def convert_to_js
25
+ "new Date(#{year}, #{month-1}, #{day})"
26
+ end
27
+ end
28
+
29
+ class Time
30
+ def convert_to_js
31
+ "new Date(#{year}, #{month-1}, #{day}, #{hour}, #{min}, #{sec}, #{usec / 1000})"
32
+ end
33
+ end
34
+
35
+ class Numeric
36
+ def convert_to_js
37
+ "%f" % self
38
+ end
39
+ end
40
+
41
+ class OpenStruct
42
+ def convert_to_js
43
+ @table.to_js
44
+ end
45
+ end
46
+
47
+ if defined?(SQLite3::Record)
48
+
49
+ module SQLite3::Record::ClassMethods
50
+ def convert_to_js
51
+ to_hash.convert_to_js
52
+ end
53
+ end
54
+
55
+ end
data/test/benchmark.rb ADDED
@@ -0,0 +1,48 @@
1
+ $: << "#{File.dirname(__FILE__)}/../lib"
2
+ require "metric_system"
3
+
4
+ def benchmark(msg, &block)
5
+ starts = Time.now
6
+
7
+ yield
8
+
9
+ ensure
10
+ runtime = Time.now - starts
11
+ if runtime > 0.1
12
+ STDERR.puts "%s: %.3f secs" % [ msg, runtime = Time.now - starts ]
13
+ end
14
+ end
15
+
16
+ TICKS_PER_DAY = 86400
17
+ TICKS_PER_DAY = 10
18
+ DBPATH = "samples.sqlite"
19
+
20
+ File.unlink(DBPATH) rescue nil
21
+ MetricSystem.target = DBPATH
22
+
23
+ benchmark "Building metric_system" do
24
+ distance = 24 * 3600.0 / TICKS_PER_DAY
25
+
26
+ 0.upto(365) do |day|
27
+ midnight = Time.parse("2013-01-01").to_i + day * 24 * 3600
28
+
29
+ benchmark "Day ##{day}: add metrics" do
30
+ MetricSystem.transaction do
31
+ 1.upto(TICKS_PER_DAY) do |step|
32
+ time = midnight + distance * step
33
+ STDERR.print "."
34
+ MetricSystem.count "clicks", 1, time
35
+ end
36
+ end
37
+ end
38
+
39
+ benchmark "Day ##{day}: aggregate" do
40
+ MetricSystem.aggregate
41
+ end
42
+ end
43
+ end
44
+
45
+ MetricSystem.print "SELECT COUNT(*) FROM aggregates"
46
+
47
+ __END__
48
+ db.print "SELECT * FROM aggregates"
@@ -3,8 +3,16 @@ require "metric_system"
3
3
  require "test/unit"
4
4
 
5
5
  class MetricSystem::TestCounters < Test::Unit::TestCase
6
+ def setup
7
+ MetricSystem.target = ":memory:"
8
+ end
9
+
10
+ def teardown
11
+ MetricSystem.target = nil
12
+ end
13
+
6
14
  def db
7
- @db ||= MetricSystem.new ":memory:"
15
+ MetricSystem
8
16
  end
9
17
 
10
18
  def test_two_events
@@ -25,6 +33,8 @@ class MetricSystem::TestCounters < Test::Unit::TestCase
25
33
  end
26
34
 
27
35
  def test_conversion
36
+ db = MetricSystem.target
37
+ expect! db => MetricSystem::Database
28
38
  db.exec "CREATE TABLE tmp(value, starts_at, starts_on)"
29
39
  now = Time.parse("2014-03-02 11:10:11 +0100")
30
40
  day = Date.parse("2014-03-02")
@@ -42,7 +52,6 @@ class MetricSystem::TestCounters < Test::Unit::TestCase
42
52
 
43
53
  r = db.select("SELECT name, value, period, starts_at FROM aggregates ORDER BY duration, name")
44
54
  assert_equal(r.map(&:to_a), [
45
- ["foo", 1, "second", Time.parse("2014-03-02 11:10:11 +0100")],
46
55
  ["foo", 1, "minute", Time.parse("2014-03-02 11:10:00 +0100")],
47
56
  ["foo", 1, "hour", Time.parse("2014-03-02 11:00:00 +0100")],
48
57
  ["foo", 1, "day", Time.parse("2014-03-02 00:00:00 +0100")],
@@ -66,7 +75,7 @@ class MetricSystem::TestCounters < Test::Unit::TestCase
66
75
  def test_combined_name
67
76
  db.count "foo", 3, "2014-03-02 12:10:11"
68
77
  db.count "foo.bar", 2, "2014-03-02 12:10:11"
69
- db.aggregate :second, :minute, :hour
78
+ db.aggregate :minute, :hour
70
79
 
71
80
  r = db.select <<-SQL
72
81
  SELECT name, value, period, starts_at
@@ -75,8 +84,6 @@ class MetricSystem::TestCounters < Test::Unit::TestCase
75
84
  SQL
76
85
 
77
86
  assert_equal(r.map(&:to_a), [
78
- ["foo" , 5, "second", Time.parse("2014-03-02 11:10:11 +0100")],
79
- ["foo.bar", 2, "second", Time.parse("2014-03-02 11:10:11 +0100")],
80
87
  ["foo" , 5, "minute", Time.parse("2014-03-02 11:10:00 +0100")],
81
88
  ["foo.bar", 2, "minute", Time.parse("2014-03-02 11:10:00 +0100")],
82
89
  ["foo" , 5, "hour", Time.parse("2014-03-02 11:00:00 +0100")],
@@ -87,8 +94,8 @@ class MetricSystem::TestCounters < Test::Unit::TestCase
87
94
  def test_double_aggregate
88
95
  db.count "foo", 3, "2014-03-02 12:10:11"
89
96
  db.count "foo.bar", 2, "2014-03-02 12:10:11"
90
- db.aggregate :second, :minute, :hour
91
- db.aggregate :second, :minute, :hour
97
+ db.aggregate :minute, :hour
98
+ db.aggregate :minute, :hour
92
99
 
93
100
  r = db.select <<-SQL
94
101
  SELECT name, value, period, starts_at
@@ -97,8 +104,6 @@ class MetricSystem::TestCounters < Test::Unit::TestCase
97
104
  SQL
98
105
 
99
106
  assert_equal(r.map(&:to_a), [
100
- ["foo" , 5, "second", Time.parse("2014-03-02 11:10:11 +0100")],
101
- ["foo.bar", 2, "second", Time.parse("2014-03-02 11:10:11 +0100")],
102
107
  ["foo" , 5, "minute", Time.parse("2014-03-02 11:10:00 +0100")],
103
108
  ["foo.bar", 2, "minute", Time.parse("2014-03-02 11:10:00 +0100")],
104
109
  ["foo" , 5, "hour", Time.parse("2014-03-02 11:00:00 +0100")],
data/test/gauges_test.rb CHANGED
@@ -3,8 +3,16 @@ require "metric_system"
3
3
  require "test/unit"
4
4
 
5
5
  class MetricSystem::TestGauging < Test::Unit::TestCase
6
+ def setup
7
+ MetricSystem.target = ":memory:"
8
+ end
9
+
10
+ def teardown
11
+ MetricSystem.target = nil
12
+ end
13
+
6
14
  def db
7
- @db ||= MetricSystem.new ":memory:"
15
+ MetricSystem
8
16
  end
9
17
 
10
18
  def test_two_events
data/test/mixed_test.rb CHANGED
@@ -3,11 +3,19 @@ require "metric_system"
3
3
  require "test/unit"
4
4
 
5
5
  class MetricSystem::TestMixed < Test::Unit::TestCase
6
+ def setup
7
+ MetricSystem.target = ":memory:"
8
+ end
9
+
10
+ def teardown
11
+ MetricSystem.target = nil
12
+ end
13
+
6
14
  def db
7
- @db ||= MetricSystem.new ":memory:"
15
+ MetricSystem
8
16
  end
9
17
 
10
- def xtest_two_events
18
+ def test_two_events
11
19
  db.gauge "foo", 1, "2014-03-02 12:10:11"
12
20
  db.gauge "foo", 2, "2014-03-02 14:10:11"
13
21
  db.count "bar", 1, "2014-03-02 12:10:11"
data/test/parallel.rb ADDED
@@ -0,0 +1,68 @@
1
+ $: << "#{File.dirname(__FILE__)}/../lib"
2
+ require "metric_system"
3
+
4
+ SOCKET = "performance.socket"
5
+ DBPATH = "samples.sqlite"
6
+
7
+ mode, _ = *ARGV
8
+ if mode == nil then
9
+ require "metric_system/server"
10
+
11
+ Thread.new do
12
+ sleep 1
13
+ system "ruby #{__FILE__} sender"
14
+ end
15
+
16
+ MetricSystem::Server.run DBPATH, SOCKET, :quit_server => true
17
+
18
+ puts "server stopped"
19
+
20
+ MetricSystem.print "SELECT COUNT(*) FROM aggregates"
21
+ exit
22
+ end
23
+
24
+ # ---------------------------------------------------------------------
25
+
26
+ def benchmark(msg, &block)
27
+ starts = Time.now
28
+
29
+ yield
30
+
31
+ ensure
32
+ runtime = Time.now - starts
33
+ if runtime > 0.1
34
+ STDERR.puts "%s: %.3f secs" % [ msg, runtime = Time.now - starts ]
35
+ end
36
+ end
37
+
38
+ # TICKS_PER_DAY = 86400
39
+ TICKS_PER_DAY = 10
40
+
41
+ File.unlink(DBPATH) rescue nil
42
+
43
+ require "time"
44
+ require "socket"
45
+
46
+ benchmark "Sending #{365 * TICKS_PER_DAY} events to metric_system" do
47
+ distance = 24 * 3600.0 / TICKS_PER_DAY
48
+ MetricSystem.target = UNIXSocket.new(SOCKET)
49
+ 0.upto(364) do |day|
50
+ midnight = Time.parse("2013-01-01").to_i + day * 24 * 3600
51
+
52
+ benchmark "Day ##{day}: add metrics" do
53
+ 1.upto(TICKS_PER_DAY) do |step|
54
+ time = midnight + distance * step
55
+ MetricSystem.count "clicks", 1, time
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ STDERR.puts "Quitting server"
62
+ MetricSystem.quit_server!
63
+
64
+ __END__
65
+
66
+ db.print "SELECT COUNT(*) FROM aggregates"
67
+
68
+ db.print "SELECT * FROM aggregates"