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
@@ -0,0 +1,31 @@
1
+
2
+ <ul class="breadcrumb">
3
+ <li>
4
+ <a href="/">Available Metrics</a> <span class="divider">/</span>
5
+ </li>
6
+ <li class="active"><%= params[:target].join(', ') %></li>
7
+ </ul>
8
+
9
+ <div class="row-fluid">
10
+ <div class="span12">
11
+ <div style="margin-bottom:2em" class="row-fluid">
12
+ <div id="graph-container-minute" class="span6"></div>
13
+ <div id="graph-container-hour" class="span6"></div>
14
+ </div>
15
+ <div class="row-fluid">
16
+ <div id="graph-container-day" class="span6"></div>
17
+ <div id="graph-container-week" class="span6"></div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <script>
23
+
24
+ $(function() {
25
+ $("#graph-container-minute").load("/graph?target[]=<%= params[:target].first %>&time=minute");
26
+ $("#graph-container-hour").load("/graph?target[]=<%= params[:target].first %>&time=hour");
27
+ $("#graph-container-day").load("/graph?target[]=<%= params[:target].first %>&time=day");
28
+ $("#graph-container-week").load("/graph?target[]=<%= params[:target].first %>&time=week");
29
+ });
30
+
31
+ </script>
@@ -2,13 +2,25 @@
2
2
  require "logger"
3
3
 
4
4
  require "simple_metrics/version"
5
- require "simple_metrics/client"
6
- require "simple_metrics/server"
5
+ require "simple_metrics/configuration"
6
+ require "simple_metrics/udp_server"
7
+ require "simple_metrics/repository"
8
+ require "simple_metrics/data_point_repository"
7
9
  require "simple_metrics/data_point"
10
+ require "simple_metrics/data_point/base"
11
+ require "simple_metrics/data_point/counter"
12
+ require "simple_metrics/data_point/event"
13
+ require "simple_metrics/data_point/gauge"
14
+ require "simple_metrics/data_point/timing"
15
+ require "simple_metrics/value_aggregation"
16
+ require "simple_metrics/array_aggregation"
17
+ require "simple_metrics/update_aggregation"
8
18
  require "simple_metrics/bucket"
9
19
  require "simple_metrics/graph"
10
20
  require "simple_metrics/functions"
11
- require "simple_metrics/mongo"
21
+ require "simple_metrics/metric"
22
+ require "simple_metrics/metric_repository"
23
+ require "simple_metrics/app"
12
24
 
13
25
  module SimpleMetrics
14
26
  extend self
@@ -21,81 +33,12 @@ module SimpleMetrics
21
33
  @@logger = logger
22
34
  end
23
35
 
24
- CONFIG_DEFAULTS = {
25
- :host => 'localhost',
26
- :port => 8125,
27
- :flush_interval => 10
28
- }.freeze
29
-
30
36
  def config
31
- @@config ||= CONFIG_DEFAULTS
32
- end
33
-
34
- def config=(options)
35
- @@config = CONFIG_DEFAULTS.merge(options)
36
- end
37
-
38
- BUCKETS_DEFAULTS = [
39
- {
40
- :name => 'stats_per_10s',
41
- :seconds => 10,
42
- :capped => true,
43
- :size => 100_100_100
44
- },
45
- {
46
- :name => 'stats_per_1min',
47
- :seconds => 60,
48
- :capped => true,
49
- :size => 1_100_100_100
50
- },
51
- {
52
- :name => 'stats_per_10min',
53
- :seconds => 600,
54
- :size => 0 ,
55
- :capped => false
56
- },
57
- {
58
- :name => 'stats_per_day',
59
- :seconds => 86400,
60
- :size => 0,
61
- :capped => false
62
- }
63
- ].freeze
64
-
65
- def buckets_config
66
- @@buckets ||= BUCKETS_DEFAULTS
67
- end
68
-
69
- def buckets_config=(buckets)
70
- @@buckets = buckets
71
- end
72
-
73
- MONGODB_DEFAULTS = {
74
- :pool_size => 5,
75
- :timeout => 5,
76
- :strict => true
77
- }.freeze
78
-
79
- DB_CONFIG_DEFAULTS = {
80
- :host => 'localhost',
81
- :port => 27017,
82
- :prefix => 'development'
83
- }.freeze
84
-
85
- def db_config=(options)
86
- @@db_config = {
87
- :host => options.delete(:host) || 'localhost',
88
- :port => options.delete(:port) || 27017,
89
- :db_name => "simple_metrics_#{options.delete(:prefix)}",
90
- :options => MONGODB_DEFAULTS.merge(options)
91
- }
37
+ @@config ||= Configuration.new
92
38
  end
93
39
 
94
- def db_config
95
- @@db_config ||= DB_CONFIG_DEFAULTS.merge(
96
- :db_name => "simple_metrics_#{DB_CONFIG_DEFAULTS[:prefix]}",
97
- :options => MONGODB_DEFAULTS
98
- )
40
+ def configure(hash = {}, &block)
41
+ config.configure(hash, &block)
99
42
  end
100
43
 
101
- end
44
+ end
@@ -19,9 +19,15 @@ Gem::Specification.new do |s|
19
19
  s.add_development_dependency "rake"
20
20
  s.add_development_dependency "rspec"
21
21
  s.add_development_dependency "rr"
22
+ s.add_development_dependency "shotgun"
22
23
 
23
24
  s.add_dependency "eventmachine"
24
25
  s.add_dependency "daemons"
25
26
  s.add_dependency "mongo", '~> 1.6'
26
27
  s.add_dependency "bson", '~> 1.6'
28
+ s.add_dependency "bson_ext", '~> 1.6'
29
+ s.add_dependency "sinatra"
30
+ s.add_dependency "erubis"
31
+ s.add_dependency "vegas", '~> 0.1.2'
32
+ s.add_dependency "json"
27
33
  end
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ module SimpleMetrics
5
+
6
+ describe ArrayAggregation do
7
+
8
+ before do
9
+ DataPointRepository.truncate_collections
10
+ DataPointRepository.ensure_collections_exist
11
+ end
12
+
13
+ let(:ts) do
14
+ Time.now.utc.to_i
15
+ end
16
+
17
+ describe "#aggregate" do
18
+ it "aggregates counter data points" do
19
+ stats1 = DataPoint.create_counter(:name => "com.test.key1", :value => 5, :ts => ts)
20
+ stats2 = DataPoint.create_counter(:name => "com.test.key1", :value => 7, :ts => ts)
21
+ stats3 = DataPoint.create_counter(:name => "com.test.key1", :value => 9, :ts => (ts + 60) )
22
+
23
+ results = ArrayAggregation.aggregate([stats1, stats2, stats3], "com.test.*")
24
+ results.should have(2).data_points
25
+ results.first.name.should == "com.test.*"
26
+ results.last.name.should == "com.test.*"
27
+ results.first.value.should == 12
28
+ results.last.value.should == 9
29
+ end
30
+
31
+ it "aggregates gauge data points" do
32
+ stats1 = DataPoint.create_gauge(:name => "com.test.key1", :value => 5, :ts => ts)
33
+ stats2 = DataPoint.create_gauge(:name => "com.test.key1", :value => 7, :ts => ts)
34
+ stats3 = DataPoint.create_gauge(:name => "com.test.key1", :value => 9, :ts => (ts + 60) )
35
+
36
+ results = ArrayAggregation.aggregate([stats1, stats2, stats3], "com.test.*")
37
+ results.should have(2).data_points
38
+ results.first.name.should == "com.test.*"
39
+ results.last.name.should == "com.test.*"
40
+ results.first.value.should == 6
41
+ results.last.value.should == 9
42
+ end
43
+
44
+ it "raises NonMatchingTypesError if types are different" do
45
+ stats1 = DataPoint.create_counter(:name => "com.test.key1", :value => 5, :ts => ts)
46
+ stats2 = DataPoint.create_gauge(:name => "com.test.key1", :value => 5, :ts => ts)
47
+ expect { ArrayAggregation.aggregate([stats1, stats2], "com.test.*") }.to raise_error(SimpleMetrics::DataPoint::NonMatchingTypesError)
48
+ end
49
+ end
50
+ end
51
+ end
data/spec/bucket_spec.rb CHANGED
@@ -37,8 +37,8 @@ module SimpleMetrics
37
37
 
38
38
  describe "#save" do
39
39
  before do
40
- Mongo.truncate_collections
41
- Mongo.ensure_collections_exist
40
+ DataPointRepository.truncate_collections
41
+ DataPointRepository.ensure_collections_exist
42
42
  bucket.save(stats, ts)
43
43
  end
44
44
 
@@ -47,7 +47,7 @@ module SimpleMetrics
47
47
  end
48
48
 
49
49
  it "saves given data in bucket" do
50
- results = bucket.find_all_by_name("key1")
50
+ results = bucket.find_all_at_ts(ts)
51
51
  results.should have(1).item
52
52
  result = results.first
53
53
  result.name.should == stats.name
@@ -56,7 +56,7 @@ module SimpleMetrics
56
56
  end
57
57
 
58
58
  it "saves data in correct timestamp" do
59
- result = bucket.find_all_by_name("key1").first
59
+ result = bucket.find_all_at_ts(ts).first
60
60
  result.ts.should == ts/sec*sec
61
61
  end
62
62
 
@@ -65,27 +65,11 @@ module SimpleMetrics
65
65
  describe "finder methods" do
66
66
 
67
67
  before do
68
- Mongo.truncate_collections
69
- Mongo.ensure_collections_exist
68
+ DataPointRepository.truncate_collections
69
+ DataPointRepository.ensure_collections_exist
70
70
  end
71
71
 
72
- describe "#find_all_by_name" do
73
- it "returns all stats for given name" do
74
- stats_same1 = DataPoint.create_counter(:name => "key1", :value => 5)
75
- stats_same2 = DataPoint.create_counter(:name => "key1", :value => 3)
76
- stats_different = DataPoint.create_counter(:name => "key2", :value => 3)
77
-
78
- bucket.save(stats_same1, ts)
79
- bucket.save(stats_same2, ts)
80
- bucket.save(stats_different, ts)
81
-
82
- results = bucket.find_all_by_name("key1")
83
- results.should have(2).items
84
- results.first.name.should == stats_same1.name
85
- end
86
- end
87
-
88
- describe "#find_all_in_ts" do
72
+ describe "#find_all_at_ts" do
89
73
  it "returns all stats in given timestamp" do
90
74
  stats1 = DataPoint.create_counter(:name => "key1", :value => 5)
91
75
  stats2 = DataPoint.create_counter(:name => "key2", :value => 3)
@@ -93,36 +77,17 @@ module SimpleMetrics
93
77
  bucket.save(stats1, ts)
94
78
  bucket.save(stats2, bucket.next_ts_bucket(ts))
95
79
 
96
- result1 = bucket.find_all_in_ts(ts).first
80
+ result1 = bucket.find_all_at_ts(ts).first
97
81
  result1.name.should == stats1.name
98
82
  result1.value.should == stats1.value
99
83
 
100
- result2 = bucket.find_all_in_ts(bucket.next_ts_bucket(ts)).first
84
+ result2 = bucket.find_all_at_ts(bucket.next_ts_bucket(ts)).first
101
85
  result2.name.should == stats2.name
102
86
  result2.value.should == stats2.value
103
87
  end
104
88
  end
105
89
 
106
- describe "#find_all_in_ts_by_name" do
107
- it "returns all stats for given name and timestamp" do
108
- stats1a = DataPoint.create_counter(:name => "key1", :value => 5)
109
- stats1b = DataPoint.create_counter(:name => "key1", :value => 7)
110
- stats2 = DataPoint.create_counter(:name => "key2", :value => 7)
111
- stats1_different_ts = DataPoint.create_counter(:name => "key1", :value => 3)
112
-
113
- bucket.save(stats1a, ts)
114
- bucket.save(stats1b, ts)
115
- bucket.save(stats2, ts)
116
- bucket.save(stats1_different_ts, bucket.next_ts_bucket(ts))
117
-
118
- results = bucket.find_all_in_ts_by_name(ts, "key1")
119
- results.should have(2).items
120
- results.first.name.should == "key1"
121
- results.last.name.should == "key1"
122
- end
123
- end
124
-
125
- describe "#find_all_in_ts_by_wildcard" do
90
+ describe "#find_all_in_ts_range_by_wildcard" do
126
91
  it "returns all stats for given name and timestamp" do
127
92
  stats1 = DataPoint.create_counter(:name => "com.test.key1", :value => 5)
128
93
  stats2 = DataPoint.create_counter(:name => "com.test.key2", :value => 7)
@@ -171,8 +136,8 @@ module SimpleMetrics
171
136
 
172
137
  describe "#aggregate_all" do
173
138
  before do
174
- Mongo.truncate_collections
175
- Mongo.ensure_collections_exist
139
+ DataPointRepository.truncate_collections
140
+ DataPointRepository.ensure_collections_exist
176
141
  end
177
142
 
178
143
  it "aggregates all counter data points" do
@@ -183,12 +148,13 @@ module SimpleMetrics
183
148
  bucket2 = Bucket[1]
184
149
  ts_at_insert = bucket2.previous_ts_bucket(ts)
185
150
  bucket.save(stats1a, ts_at_insert)
151
+ Bucket.aggregate(stats1a)
186
152
  bucket.save(stats1b, ts_at_insert)
153
+ Bucket.aggregate(stats1b)
187
154
  bucket.save(stats2, ts_at_insert)
155
+ Bucket.aggregate(stats2)
188
156
 
189
- Bucket.aggregate_all(ts)
190
-
191
- results = bucket2.find_all_in_ts(ts_at_insert)
157
+ results = bucket2.find_all_at_ts(ts_at_insert)
192
158
  results.should have(2).items
193
159
 
194
160
  key1_result = results.find {|stat| stat.name == "key1"}
@@ -208,12 +174,13 @@ module SimpleMetrics
208
174
  bucket2 = Bucket[1]
209
175
  ts_at_insert = bucket2.previous_ts_bucket(ts)
210
176
  bucket.save(stats1a, ts_at_insert)
177
+ Bucket.aggregate(stats1a)
211
178
  bucket.save(stats1b, ts_at_insert)
179
+ Bucket.aggregate(stats1b)
212
180
  bucket.save(stats2, ts_at_insert)
181
+ Bucket.aggregate(stats2)
213
182
 
214
- Bucket.aggregate_all(ts)
215
-
216
- results = bucket2.find_all_in_ts(ts_at_insert)
183
+ results = bucket2.find_all_at_ts(ts_at_insert)
217
184
  results.should have(2).items
218
185
 
219
186
  key1_result = results.find {|stat| stat.name == "key1"}
@@ -229,8 +196,8 @@ module SimpleMetrics
229
196
 
230
197
  describe "#flush_data_points" do
231
198
  before do
232
- Mongo.truncate_collections
233
- Mongo.ensure_collections_exist
199
+ DataPointRepository.truncate_collections
200
+ DataPointRepository.ensure_collections_exist
234
201
 
235
202
  stats1 = DataPoint.create_counter(:name => "key1", :value => 5)
236
203
  stats2 = DataPoint.create_counter(:name => "key1", :value => 7)
@@ -241,19 +208,14 @@ module SimpleMetrics
241
208
  it "saves all stats in finest/first bucket" do
242
209
  Bucket.flush_data_points(@stats)
243
210
 
244
- results = bucket.find_all_in_ts(ts)
211
+ results = bucket.find_all_at_ts(ts)
245
212
  results.should have(2).items
246
213
  end
247
214
 
248
- it "calls aggregate_all afterwards" do
249
- mock(Bucket).aggregate_all(ts)
250
- Bucket.flush_data_points(@stats)
251
- end
252
-
253
215
  it "saves all stats and aggregate if duplicates found" do
254
216
  Bucket.flush_data_points(@stats)
255
217
 
256
- results = bucket.find_all_in_ts(ts)
218
+ results = bucket.find_all_at_ts(ts)
257
219
  results.should have(2).items
258
220
  results.first.name.should == "key1"
259
221
  results.last.name.should == "key2"
@@ -0,0 +1,114 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ module SimpleMetrics
5
+
6
+ describe DataPointRepository do
7
+
8
+ before do
9
+ DataPointRepository.truncate_collections
10
+ DataPointRepository.ensure_collections_exist
11
+ end
12
+
13
+ let(:ts) do
14
+ Time.now.utc.to_i
15
+ end
16
+
17
+ let(:repository) do
18
+ DataPointRepository.for_retention('stats_per_10s')
19
+ end
20
+
21
+ describe "#save" do
22
+ it "saves data point correctly" do
23
+ dp = DataPoint.create_counter(:name => "key", :ts => ts)
24
+ repository.save(dp)
25
+
26
+ results = repository.find_all_at_ts(ts)
27
+ results.should have(1).data_point
28
+ end
29
+ end
30
+
31
+ describe "#find_all_at_ts" do
32
+ it "returns all data points at given time stamp" do
33
+ dp1 = DataPoint.create_counter(:name => "key1", :ts => ts)
34
+ dp2 = DataPoint.create_counter(:name => "key1", :ts => ts + 10)
35
+ repository.save(dp1)
36
+ repository.save(dp2)
37
+
38
+ results = repository.find_all_at_ts(ts)
39
+ results.should have(1).data_point
40
+ results.first.name.should == "key1"
41
+ end
42
+ end
43
+
44
+ describe "#find_all_in_ts_range" do
45
+ it "returns all data points in given time stamp range" do
46
+ dp1 = DataPoint.create_counter(:name => "key1", :ts => ts)
47
+ dp2 = DataPoint.create_counter(:name => "key2", :ts => ts + 10)
48
+ dp3 = DataPoint.create_counter(:name => "key3", :ts => ts + 20)
49
+ repository.save(dp1)
50
+ repository.save(dp2)
51
+ repository.save(dp3)
52
+
53
+ results = repository.find_all_in_ts_range(ts, ts+10)
54
+ results.should have(2).data_points
55
+ results.first.name.should == "key1"
56
+ results.last.name.should == "key2"
57
+ end
58
+ end
59
+
60
+ describe "#find_all_in_ts_range_by_name" do
61
+ it "returns all data points in given time stamp range by name" do
62
+ dp1 = DataPoint.create_counter(:name => "key1", :ts => ts)
63
+ dp2 = DataPoint.create_counter(:name => "key2", :ts => ts + 10)
64
+ dp3 = DataPoint.create_counter(:name => "key3", :ts => ts + 20)
65
+ repository.save(dp1)
66
+ repository.save(dp2)
67
+ repository.save(dp3)
68
+
69
+ results = repository.find_all_in_ts_range_by_name(ts, ts+10, "key1")
70
+ results.should have(1).data_point
71
+ results.first.name.should == "key1"
72
+ end
73
+ end
74
+
75
+ describe "#find_all_in_ts_range_by_wildcard" do
76
+ it "returns all data points in given time stamp range by wildcard" do
77
+ dp1 = DataPoint.create_counter(:name => "test.key1", :ts => ts)
78
+ dp2 = DataPoint.create_counter(:name => "test.key2", :ts => ts + 10)
79
+ dp3 = DataPoint.create_counter(:name => "test.key3", :ts => ts + 20)
80
+ repository.save(dp1)
81
+ repository.save(dp2)
82
+ repository.save(dp3)
83
+
84
+ results = repository.find_all_in_ts_range_by_wildcard(ts, ts+10, "test.*")
85
+ results.should have(2).data_point2
86
+ results.first.name.should == "test.key1"
87
+ results.last.name.should == "test.key2"
88
+ end
89
+ end
90
+
91
+ describe "#count_at" do
92
+ it "returns the total count of entries for given time stamp" do
93
+ dp1 = DataPoint.create_counter(:name => "test.key1", :ts => ts)
94
+ dp2 = DataPoint.create_counter(:name => "test.key2", :ts => ts + 10)
95
+ repository.save(dp1)
96
+ repository.save(dp2)
97
+ repository.count_at(ts).should == 1
98
+ end
99
+ end
100
+
101
+ describe "#find_all_distinct_names" do
102
+ it "returns all distinct names of data points" do
103
+ dp1 = DataPoint.create_counter(:name => "test.key1", :ts => ts)
104
+ dp2 = DataPoint.create_counter(:name => "test.key2", :ts => ts + 10)
105
+ repository.save(dp1)
106
+ repository.save(dp2)
107
+ results = repository.find_all_distinct_names
108
+ results.should include("test.key1")
109
+ results.should include("test.key2")
110
+ end
111
+ end
112
+ end
113
+
114
+ end