simple_metrics 0.2.3 → 0.3.2

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 (61) hide show
  1. data/.travis.yml +3 -0
  2. data/README.markdown +16 -53
  3. data/Rakefile +17 -0
  4. data/bin/populate +26 -13
  5. data/bin/simple_metrics_server +4 -3
  6. data/bin/simple_metrics_web +11 -0
  7. data/config.ru +6 -0
  8. data/default_config.yml +34 -0
  9. data/lib/simple_metrics/app.rb +136 -0
  10. data/lib/simple_metrics/array_aggregation.rb +84 -0
  11. data/lib/simple_metrics/bucket.rb +74 -63
  12. data/lib/simple_metrics/configuration.rb +79 -0
  13. data/lib/simple_metrics/data_point/base.rb +59 -0
  14. data/lib/simple_metrics/data_point/counter.rb +13 -0
  15. data/lib/simple_metrics/data_point/event.rb +12 -0
  16. data/lib/simple_metrics/data_point/gauge.rb +13 -0
  17. data/lib/simple_metrics/data_point/timing.rb +12 -0
  18. data/lib/simple_metrics/data_point.rb +32 -136
  19. data/lib/simple_metrics/data_point_repository.rb +131 -0
  20. data/lib/simple_metrics/functions.rb +5 -5
  21. data/lib/simple_metrics/graph.rb +10 -14
  22. data/lib/simple_metrics/metric.rb +28 -0
  23. data/lib/simple_metrics/metric_repository.rb +54 -0
  24. data/lib/simple_metrics/public/css/bootstrap-responsive.min.css +12 -0
  25. data/lib/simple_metrics/public/css/bootstrap.min.css +689 -0
  26. data/lib/simple_metrics/public/css/graph.css +45 -0
  27. data/lib/simple_metrics/public/css/rickshaw.min.css +1 -0
  28. data/lib/simple_metrics/public/img/glyphicons-halflings-white.png +0 -0
  29. data/lib/simple_metrics/public/img/glyphicons-halflings.png +0 -0
  30. data/lib/simple_metrics/public/js/application.js +278 -0
  31. data/lib/simple_metrics/public/js/backbone-0.9.2.min.js +38 -0
  32. data/lib/simple_metrics/public/js/bootstrap.min.js +6 -0
  33. data/lib/simple_metrics/public/js/d3.v2.min.js +4 -0
  34. data/lib/simple_metrics/public/js/handlebars-1.0.0.beta.6.js +1550 -0
  35. data/lib/simple_metrics/public/js/jquery-1.7.1.min.js +4 -0
  36. data/lib/simple_metrics/public/js/rickshaw.min.js +1 -0
  37. data/lib/simple_metrics/public/js/underscore-1.3.1.min.js +31 -0
  38. data/lib/simple_metrics/repository.rb +34 -0
  39. data/lib/simple_metrics/udp_server.rb +81 -0
  40. data/lib/simple_metrics/update_aggregation.rb +62 -0
  41. data/lib/simple_metrics/value_aggregation.rb +63 -0
  42. data/lib/simple_metrics/version.rb +1 -1
  43. data/lib/simple_metrics/views/graph.erb +93 -0
  44. data/lib/simple_metrics/views/index.erb +0 -0
  45. data/lib/simple_metrics/views/layout.erb +119 -0
  46. data/lib/simple_metrics/views/show.erb +31 -0
  47. data/lib/simple_metrics.rb +19 -76
  48. data/simple_metrics.gemspec +6 -0
  49. data/spec/array_aggregation_spec.rb +51 -0
  50. data/spec/bucket_spec.rb +24 -62
  51. data/spec/data_point_repository_spec.rb +114 -0
  52. data/spec/data_point_spec.rb +1 -70
  53. data/spec/graph_spec.rb +2 -20
  54. data/spec/metric_repository_spec.rb +53 -0
  55. data/spec/spec_helper.rb +3 -3
  56. data/spec/value_aggregation_spec.rb +52 -0
  57. metadata +131 -24
  58. data/bin/simple_metrics_client +0 -64
  59. data/lib/simple_metrics/client.rb +0 -83
  60. data/lib/simple_metrics/mongo.rb +0 -48
  61. data/lib/simple_metrics/server.rb +0 -66
@@ -5,7 +5,7 @@ module SimpleMetrics
5
5
  class << self
6
6
 
7
7
  def all
8
- @@all ||= SimpleMetrics.buckets_config.map { |r| Bucket.new(r) }
8
+ @@all ||= SimpleMetrics.config.buckets.map { |r| Bucket.new(r) }
9
9
  end
10
10
 
11
11
  def first
@@ -17,44 +17,62 @@ module SimpleMetrics
17
17
  all[index]
18
18
  end
19
19
 
20
- def coarse_buckets
21
- Bucket.all.sort_by! { |r| r.seconds }[1..-1]
20
+ def flush_raw_data(data)
21
+ data_points = []
22
+ data.each do |str|
23
+ begin
24
+ data_points << DataPoint.parse(str)
25
+ rescue DataPoint::ParserError => e
26
+ SimpleMetrics.logger.debug "Invalid Data skipped: #{str}, #{e}"
27
+ end
28
+ end
29
+ flush_data_points(data_points, Time.now.utc.to_i)
22
30
  end
23
31
 
24
- def flush_data_points(data_points)
32
+ def flush_data_points(data_points, ts = nil)
25
33
  return if data_points.empty?
26
34
  SimpleMetrics.logger.info "#{Time.now} Flushing #{data_points.count} counters to MongoDB"
27
35
 
28
- ts = Time.now.utc.to_i
36
+ ts ||= Time.now.utc.to_i
29
37
  bucket = Bucket.first
30
- data_points.group_by { |data| data.name }.each_pair do |name,dps|
31
- data = DataPoint.aggregate(dps)
32
- bucket.save(data, ts)
33
- end
34
38
 
35
- self.aggregate_all(ts)
39
+ data_points.group_by { |dp| dp.name }.each_pair do |name,dps|
40
+ dp = ValueAggregation.aggregate(dps)
41
+ bucket.save(dp, ts)
42
+ update_metric(dp)
43
+ aggregate(dp)
44
+ end
36
45
  end
37
46
 
38
- def aggregate_all(ts)
39
- ts_bucket = self.first.ts_bucket(ts)
40
-
47
+ def aggregate(dp)
41
48
  coarse_buckets.each do |bucket|
42
- current_ts = bucket.ts_bucket(ts_bucket)
43
- previous_ts = bucket.previous_ts_bucket(ts_bucket)
44
- SimpleMetrics.logger.debug "Aggregating #{bucket.name} #{previous_ts}....#{current_ts} (#{humanized_timestamp(previous_ts)}..#{humanized_timestamp(current_ts)})"
45
-
46
- unless bucket.stats_exist_in_previous_ts?(previous_ts)
47
- data_points = self.first.find_all_in_ts_range(previous_ts, current_ts)
48
- data_points.group_by { |data| data.name }.each_pair do |name,dps|
49
- data = DataPoint.aggregate(dps)
50
- bucket.save(data, previous_ts)
51
- end
49
+ existing_dp = bucket.find_data_point_at_ts(dp.ts, dp.name)
50
+ if existing_dp
51
+ UpdateAggregation.aggregate(existing_dp, dp)
52
+ bucket.update(existing_dp, existing_dp.ts)
53
+ else
54
+ dp.sum = dp.value
55
+ dp.total = 1
56
+ bucket.save(dp, dp.ts)
52
57
  end
53
58
  end
54
59
  end
55
60
 
56
61
  private
57
62
 
63
+ def update_metric(dp)
64
+ metric = MetricRepository.find_one_by_name(dp.name)
65
+ if metric
66
+ MetricRepository.update(Metric.new(:name => dp.name, :total => metric.total + 1))
67
+ else
68
+ MetricRepository.save(Metric.new(:name => dp.name, :total => 1))
69
+ end
70
+ end
71
+
72
+ def coarse_buckets
73
+ Bucket.all.sort_by! { |r| r.seconds }[1..-1]
74
+ end
75
+
58
76
  def humanized_timestamp(ts)
59
77
  Time.at(ts).utc
60
78
  end
@@ -63,10 +81,10 @@ module SimpleMetrics
63
81
  attr_reader :name, :capped
64
82
 
65
83
  def initialize(attributes)
66
- @name = attributes[:name]
67
- @seconds = attributes[:seconds]
68
- @capped = attributes[:capped]
69
- @size = attributes[:size]
84
+ @name = attributes['name']
85
+ @seconds = attributes['seconds']
86
+ @capped = attributes['capped']
87
+ @size = attributes['size']
70
88
  end
71
89
 
72
90
  def seconds
@@ -78,7 +96,7 @@ module SimpleMetrics
78
96
  end
79
97
 
80
98
  def ts_bucket(ts)
81
- ts / seconds * seconds
99
+ (ts / seconds) * seconds
82
100
  end
83
101
 
84
102
  def next_ts_bucket(ts)
@@ -89,70 +107,59 @@ module SimpleMetrics
89
107
  ts_bucket(ts) - seconds
90
108
  end
91
109
 
92
- def find(id)
93
- mongo_result = mongo_coll.find_one({ :_id => id })
94
- DataPoint.create_from_db(mongo_result)
95
- end
96
-
97
- def find_all_by_name(name)
98
- mongo_result = mongo_coll.find({ :name => name }).to_a
99
- mongo_result.inject([]) { |result, a| result << DataPoint.create_from_db(a) }
100
- end
101
-
102
- def find_all_in_ts(ts)
103
- mongo_result = mongo_coll.find({ :ts => ts_bucket(ts) }).to_a
104
- mongo_result.inject([]) { |result, a| result << DataPoint.create_from_db(a) }
110
+ # TODO: only used in tests, do we need it?
111
+ def find_all_at_ts(ts)
112
+ repository.find_all_at_ts(ts_bucket(ts))
105
113
  end
106
114
 
107
- def find_all_in_ts_by_name(ts, name)
108
- mongo_result = mongo_coll.find({ :ts => ts_bucket(ts), :name => name }).to_a
109
- mongo_result.inject([]) { |result, a| result << DataPoint.create_from_db(a) }
115
+ def find_data_point_at_ts(ts, name)
116
+ repository.find_data_point_at_ts(ts_bucket(ts), name)
110
117
  end
111
118
 
119
+ # TODO: only used in tests, do we need it?
112
120
  def find_all_in_ts_range(from, to)
113
- mongo_result = mongo_coll.find({ :ts => { "$gte" => from, "$lte" => to }}).to_a
114
- mongo_result.inject([]) { |result, a| result << DataPoint.create_from_db(a) }
121
+ repository.find_all_in_ts_range(from, to)
115
122
  end
116
123
 
117
124
  def find_all_in_ts_range_by_name(from, to, name)
118
- mongo_result = mongo_coll.find({ :name => name, :ts => { "$gte" => from, "$lte" => to }}).to_a
119
- mongo_result.inject([]) { |result, a| result << DataPoint.create_from_db(a) }
125
+ repository.find_all_in_ts_range_by_name(from, to, name)
120
126
  end
121
127
 
122
128
  def find_all_in_ts_range_by_wildcard(from, to, target)
123
- target = target.gsub('.', '\.')
124
- target = target.gsub('*', '.*')
125
- mongo_result = mongo_coll.find({ :name => /#{target}/, :ts => { "$gte" => from, "$lte" => to } }).to_a
126
- mongo_result.inject([]) { |result, a| result << DataPoint.create_from_db(a) }
129
+ repository.find_all_in_ts_range_by_wildcard(from, to, target)
127
130
  end
128
131
 
129
- def find_all_in_ts_range_by_regexp(from, to, target)
130
- mongo_result = mongo_coll.find({ :name => /#{target}/, :ts => { "$gte" => from, "$lte" => to } }).to_a
131
- mongo_result.inject([]) { |result, a| result << DataPoint.create_from_db(a) }
132
+ # TODO: only used in tests, do we need it?
133
+ def data_points_exist_at_ts?(ts, name)
134
+ repository.count_for_name_at(ts, name) > 0
132
135
  end
133
136
 
137
+ # TODO: only used in tests, do we need it?
134
138
  def stats_exist_in_previous_ts?(ts)
135
- mongo_coll.find({ :ts => ts }).count > 0
139
+ repository.count_at(ts) > 0
136
140
  end
137
141
 
138
142
  def find_all_distinct_names
139
- mongo_coll.distinct(:name).to_a
143
+ repository.find_all_distinct_names
140
144
  end
141
145
 
142
- def save(stats, ts)
143
- stats.ts = ts_bucket(ts)
144
- result = mongo_coll.insert(stats.attributes)
145
- SimpleMetrics.logger.debug "SERVER: MongoDB - insert in #{name}: #{stats.inspect}, result: #{result}"
146
+ def save(dp, ts)
147
+ dp.ts = ts_bucket(ts)
148
+ repository.save(dp)
149
+ SimpleMetrics.logger.debug "SERVER: MongoDB - insert in #{name}: #{dp.inspect}"
146
150
  end
147
151
 
148
- def mongo_coll
149
- Mongo.collection(name)
152
+ def update(dp, ts)
153
+ dp.ts = ts_bucket(ts)
154
+ repository.update(dp, ts)
155
+ SimpleMetrics.logger.debug "SERVER: MongoDB - update in #{name}: #{dp.inspect}"
150
156
  end
151
157
 
152
158
  def capped?
153
159
  @capped == true
154
160
  end
155
161
 
162
+ # TODO refactor, move to graph.rb
156
163
  def fill_gaps(from, to, query_result)
157
164
  return query_result if query_result.nil? || query_result.size == 0
158
165
 
@@ -176,6 +183,10 @@ module SimpleMetrics
176
183
 
177
184
  private
178
185
 
186
+ def repository
187
+ DataPointRepository.for_retention(name)
188
+ end
189
+
179
190
  def each_ts(from, to)
180
191
  current_bucket_ts = ts_bucket(from)
181
192
  while (current_bucket_ts <= ts_bucket(to))
@@ -0,0 +1,79 @@
1
+ require 'yaml'
2
+
3
+ module SimpleMetrics
4
+ class Configuration
5
+
6
+ attr_reader :config
7
+
8
+ def initialize(hash = {}, &block)
9
+ @config = load_defaults.merge(hash)
10
+ end
11
+
12
+ def configure(hash = {}, &block)
13
+ yield self if block_given?
14
+ self
15
+ end
16
+
17
+ def db
18
+ @db ||= config['db']
19
+ end
20
+
21
+ def db=(db)
22
+ @db = db
23
+ end
24
+
25
+ def buckets
26
+ @buckets ||= config['buckets']
27
+ end
28
+
29
+ def buckets=(buckets)
30
+ @buckets = buckets
31
+ end
32
+
33
+ def server
34
+ @server ||= config['server']
35
+ end
36
+
37
+ def server=(server)
38
+ @server = server
39
+ end
40
+
41
+ def web
42
+ @web ||= config['web']
43
+ end
44
+
45
+ def web=(web)
46
+ @web = web
47
+ end
48
+
49
+ private
50
+
51
+ def load_defaults
52
+ @config = load_config
53
+ rescue Errno::ENOENT # not found error
54
+ logger.info "Creating initial config file: #{config_file}"
55
+ FileUtils.cp(default_config_file, config_file)
56
+ @config = load_config
57
+ end
58
+
59
+ def config_file
60
+ File.expand_path('~/.simple_metrics.conf')
61
+ end
62
+
63
+ def default_config_file
64
+ File.expand_path('../../../default_config.yml', __FILE__)
65
+ end
66
+
67
+ def load_config
68
+ YAML.load_file(config_file)
69
+ rescue ArgumentError => e
70
+ logger.error "Error parsing config file: #{e}"
71
+ rescue IOError => e
72
+ logger.error "Error reading config file: #{e}"
73
+ end
74
+
75
+ def logger
76
+ SimpleMetrics.logger
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+ module SimpleMetrics
3
+ module DataPoint
4
+ class Base
5
+
6
+ attr_accessor :name, :ts, :type, :value, :total, :sum
7
+ attr_reader :id
8
+
9
+ def initialize(attributes)
10
+ @id = attributes[:id]
11
+ @name = attributes[:name]
12
+ @value = attributes[:value]
13
+ @ts = attributes[:ts]
14
+ @sample_rate = attributes[:sample_rate]
15
+ @sum = attributes[:sum]
16
+ @total = attributes[:total]
17
+ end
18
+
19
+ def counter?
20
+ @type == 'c'
21
+ end
22
+
23
+ def gauge?
24
+ @type == 'g'
25
+ end
26
+
27
+ def timing?
28
+ @type == 'ms'
29
+ end
30
+
31
+ def event?
32
+ @type == 'ev'
33
+ end
34
+
35
+ def timestamp
36
+ ts
37
+ end
38
+
39
+ def value
40
+ @value.to_i if @value
41
+ end
42
+
43
+ def attributes
44
+ {
45
+ :name => @name,
46
+ :value => @value,
47
+ :ts => @ts,
48
+ :type => @type,
49
+ :total => @total,
50
+ :sum => @sum
51
+ }
52
+ end
53
+
54
+ def to_s
55
+ attributes.to_s
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ module SimpleMetrics
2
+ module DataPoint
3
+ class Counter < Base
4
+
5
+ def initialize(attributes)
6
+ super(attributes)
7
+ @type = 'c'
8
+ @value = (@value.to_i || 1) * (1.0 / (@sample_rate || 1).to_f)
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleMetrics
2
+ module DataPoint
3
+ class Event < Base
4
+
5
+ def initialize(attributes)
6
+ super(attributes)
7
+ @type = 'ev'
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module SimpleMetrics
2
+ module DataPoint
3
+ class Gauge < Base
4
+
5
+ def initialize(attributes)
6
+ super(attributes)
7
+ @type = 'g'
8
+ @value = (@value.to_i || 1) * (1.0 / (@sample_rate || 1).to_f)
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleMetrics
2
+ module DataPoint
3
+ class Timing < Base
4
+
5
+ def initialize(attributes)
6
+ super(attributes)
7
+ @type = 'ms'
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -1,10 +1,10 @@
1
- # encoding: utf-8
2
1
  module SimpleMetrics
3
-
4
- class DataPoint
2
+ module DataPoint
3
+ extend self
5
4
 
6
5
  class NonMatchingTypesError < Exception; end
7
6
  class ParserError < Exception; end
7
+ class UnknownTypeError < Exception; end
8
8
 
9
9
  # examples:
10
10
  # com.example.test1:1|c
@@ -14,149 +14,45 @@ module SimpleMetrics
14
14
  # com.example.test4:44|ms
15
15
  REGEXP = /^([\d\w_.]*):(-?[\d]*)\|(c|g|ms){1}(\|@([.\d]+))?$/i
16
16
 
17
- class << self
18
-
19
- def parse(str)
20
- if str =~ REGEXP
21
- name, value, type, sample_rate = $1, $2, $3, $5
22
- if type == "ms"
23
- # TODO: implement sample_rate handling
24
- create_timing(:name => name, :value => value)
25
- elsif type == "g"
26
- create_gauge(:name => name, :value => (value.to_i || 1) * (1.0 / (sample_rate || 1).to_f) )
27
- elsif type == "c"
28
- create_counter(:name => name, :value => (value.to_i || 1) * (1.0 / (sample_rate || 1).to_f) )
29
- end
30
- else
31
- raise ParserError, "Parser Error - Invalid Stat: #{str}"
32
- end
33
- end
34
-
35
- def create_counter(attributes)
36
- self.new(attributes.merge(:type => 'c'))
37
- end
38
-
39
- def create_gauge(attributes)
40
- self.new(attributes.merge(:type => 'g'))
41
- end
42
-
43
- def create_timing(attributes)
44
- self.new(attributes.merge(:type => 'ms'))
45
- end
46
-
47
- def aggregate(stats_array, name = nil)
48
- raise NonMatchingTypesError unless stats_array.group_by { |stats| stats.type }.size == 1
49
-
50
- result_stat = stats_array.first.dup
51
- result_stat.name = name if name
52
- if stats_array.first.counter?
53
- result_stat.value = stats_array.map { |stats| stats.value }.inject(0) { |result, value| result += value }
54
- result_stat
55
- elsif stats_array.first.gauge?
56
- total_value = stats_array.map { |stats| stats.value }.inject(0) { |result, value| result += value }
57
- result_stat.value = total_value / stats_array.size
58
- result_stat
59
- elsif stats_array.first.timing?
60
- # TODO implement timing aggregation
61
- elsif stats_array.first.event?
62
- # TODO implement event aggregation
63
- else
64
- raise ArgumentError, "Unknown data point type"
65
- end
66
- end
67
-
68
- def aggregate_array(stats_array, name = nil)
69
- raise NonMatchingTypesError unless stats_array.group_by { |stats| stats.type }.size == 1
70
-
71
- if stats_array.first.counter?
72
- tmp_hash = ts_hash_aggregated(stats_array) do |value1, value2|
73
- value1 + value2
74
- end
75
-
76
- result = []
77
- tmp_hash.each_pair do |key, value|
78
- result << self.new(:name => name, :ts => key, :value => value, :type => 'c')
79
- end
80
- result
81
- elsif stats_array.first.gauge?
82
- tmp_hash = ts_hash_aggregated(stats_array) do |value1, value2|
83
- (value1 + value2)/2
84
- end
85
-
86
- result = []
87
- tmp_hash.each_pair do |key, value|
88
- result << self.new(:name => name, :ts => key, :value => value, :type => 'g')
89
- end
90
- result
91
- elsif stats_array.first.timing?
92
- # TODO implement timing aggregation
93
- elsif stats_array.first.event?
94
- # TODO implement event aggregation
95
- else
96
- raise ArgumentError, "Unknown data point type"
97
- end
98
- end
99
-
100
- def create_from_db(attributes)
101
- self.new(:name => attributes["name"], :value => attributes["value"], :ts => attributes["ts"], :type => attributes["type"])
102
- end
103
-
104
- def ts_hash(query_result)
105
- query_result.inject({}) { |result, dp| result[dp.ts] = dp; result }
106
- end
107
-
108
- private
109
-
110
- def ts_hash_aggregated(data_points, &block)
111
- tmp = {}
112
- data_points.each do |dp|
113
- if tmp.key?(dp.ts)
114
- tmp[dp.ts] = block.call(tmp[dp.ts], dp.value)
115
- else
116
- tmp[dp.ts] = dp.value
117
- end
118
- end
119
- tmp
17
+ def parse(str)
18
+ if str =~ REGEXP
19
+ name, value, type, sample_rate = $1, $2, $3, $5
20
+ build(:name => name, :value => value, :type => type, :sample_rate => sample_rate)
21
+ else
22
+ raise ParserError, "Parser Error - Invalid data point: #{str}"
120
23
  end
121
24
  end
122
25
 
123
- attr_accessor :name, :ts, :type, :value
124
-
125
- def initialize(attributes)
126
- @name = attributes[:name]
127
- @value = attributes[:value]
128
- @ts = attributes[:ts]
129
- @type = attributes[:type]
130
- end
131
-
132
- def counter?
133
- type == 'c'
134
- end
135
-
136
- def gauge?
137
- type == 'g'
26
+ def build(attributes)
27
+ case attributes[:type]
28
+ when 'c'
29
+ DataPoint.create_counter(attributes)
30
+ when 'g'
31
+ Gauge.new(attributes)
32
+ when 'ms'
33
+ Timing.new(attributes)
34
+ when 'ev'
35
+ Event.new(attributes)
36
+ else
37
+ raise UnknownTypeError, "Unknown Type Error: #{attributes[:type]}"
38
+ end
138
39
  end
139
40
 
140
- def timing?
141
- type == 'ms'
41
+ def create_counter(attributes)
42
+ Counter.new(attributes)
142
43
  end
143
44
 
144
- def timestamp
145
- ts
45
+ def create_gauge(attributes)
46
+ Gauge.new(attributes)
146
47
  end
147
48
 
148
- def value
149
- @value.to_i if @value
49
+ def create_timing(attributes)
50
+ Timing.new(attributes)
150
51
  end
151
52
 
152
- def attributes
153
- {
154
- :name => name,
155
- :value => value,
156
- :ts => ts,
157
- :type => type
158
- }
53
+ def ts_hash(query_result)
54
+ query_result.inject({}) { |result, dp| result[dp.ts] = dp; result }
159
55
  end
160
-
56
+
161
57
  end
162
- end
58
+ end