librato-rails 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/README.md +41 -9
  2. data/lib/librato-rails.rb +1 -1
  3. data/lib/librato/rails.rb +117 -25
  4. data/lib/librato/rails/aggregator.rb +4 -4
  5. data/lib/librato/rails/collector.rb +7 -7
  6. data/lib/librato/rails/counter_cache.rb +19 -19
  7. data/lib/librato/rails/group.rb +5 -5
  8. data/lib/librato/rails/subscribers.rb +18 -18
  9. data/lib/librato/rails/version.rb +1 -1
  10. data/lib/librato/rails/worker.rb +8 -12
  11. data/test/dummy/log/development.log +2764 -2100
  12. data/test/dummy/log/test.log +1788 -8099
  13. data/test/dummy/tmp/cache/assets/{D66/890/sprockets%2F789354d3ec91e1ba6c8e92878d8f6ff8 → CBF/9D0/sprockets%2F541895701c40c7d7fd678356b18cb59d} +0 -0
  14. data/test/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  15. data/test/dummy/tmp/cache/assets/CDF/870/sprockets%2Fb878faf942403e313a5b103e5d80488e +0 -0
  16. data/test/dummy/tmp/cache/assets/CE8/7E0/sprockets%2F178e2a1f9aa891d473009c7f3095df28 +0 -0
  17. data/test/dummy/tmp/cache/assets/CF9/7C0/sprockets%2F40fc2f3d2a468a00e463f1d313cb1683 +0 -0
  18. data/test/dummy/tmp/cache/assets/{D11/D90/sprockets%2Ff688bee5b15ad322749fd06432065df2 → D02/220/sprockets%2F1410fa948b990522bdfeca7841d31b13} +0 -0
  19. data/test/dummy/tmp/cache/assets/D04/890/sprockets%2F587335c079eef8d5a63784fc8f99905a +0 -0
  20. data/test/dummy/tmp/cache/assets/D05/D40/sprockets%2F1c9faaf28d05409b88ad3113374d613c +0 -0
  21. data/test/dummy/tmp/cache/assets/{D48/6E0/sprockets%2F3d5dd928c45756c99bb1018cdbba7485 → D19/710/sprockets%2F5b878936d488c1f601c231fda1d678ea} +0 -0
  22. data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  23. data/test/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  24. data/test/dummy/tmp/cache/assets/D4F/000/sprockets%2F25e44896aac12384727e9dab827ebef9 +0 -0
  25. data/test/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  26. data/test/dummy/tmp/cache/assets/{D84/000/sprockets%2F2ed60caa8412eb8334fe327cab12cb32 → D69/610/sprockets%2F3902e4b1ffbdb61222bf5fc45a44799a} +0 -0
  27. data/test/dummy/tmp/cache/assets/D8B/F90/sprockets%2Ffe6ce696e9141eb755d8eed79128e17c +0 -0
  28. data/test/dummy/tmp/cache/assets/D98/8B0/sprockets%2Fedbef6e0d0a4742346cf479f2c522eb0 +0 -0
  29. data/test/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  30. data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  31. data/test/fixtures/config/simple.yml +5 -0
  32. data/test/unit/configuration_test.rb +18 -3
  33. metadata +14 -14
  34. data/test/dummy/test_env.sh +0 -2
data/README.md CHANGED
@@ -7,9 +7,7 @@ Report key statistics for your Rails app to [Librato Metrics](https://metrics.li
7
7
 
8
8
  **NOTE: This is currently in alpha development and is not yet officially supported**
9
9
 
10
- **NOTES FOR ALPHA TESTERS:**
11
- * If you are upgrading from a version prior to the rename to librato-rails, note that *the env variable names for configuration and the name of the config files have changed*. See the new names in configuration, below.
12
- * Starting with 0.4.0 *all metrics are now submitted as gauges*. If you were using a prior version you will need to manually remove any librato-rails generated metrics which are counters.
10
+ You may want to read the [notes on upgrading](https://github.com/librato/librato-rails/wiki/Alpha-Tester-Upgrade-Notes) if you are an alpha tester.
13
11
 
14
12
  ## Installation
15
13
 
@@ -23,25 +21,45 @@ Then run `bundle install`.
23
21
 
24
22
  If you don't have a Metrics account already, [sign up](https://metrics.librato.com/). In order to send measurements to Metrics you need to provide your account credentials to `librato-rails`. You can provide these one of two ways:
25
23
 
24
+ ##### Use a config file
25
+
26
26
  Create a `config/librato.yml` like the following:
27
27
 
28
28
  production:
29
29
  user: <your-email>
30
30
  token: <your-api-key>
31
31
 
32
- (the file is parsed via ERB in case you need to add some magic in there - useful in some cloud environments)
32
+ The `librato.yml` file is parsed via ERB in case you need to add some host or environment-specific magic.
33
+
34
+ Note that using a configuration file allows you to specify different configurations per-environment. Submission will be disabled in any environment without credentials.
35
+
36
+ ##### Use environment variables
37
+
38
+ Alternately you can provide `LIBRATO_METRICS_USER` and `LIBRATO_METRICS_TOKEN` environment variables. Unlike config file settings, environment variables will be used in all non-test environments (development, production, etc).
39
+
40
+ Note that if a config file is present, _all environment variables will be ignored_.
33
41
 
34
- OR provide `LIBRATO_METRICS_USER` and `LIBRATO_METRICS_TOKEN` environment variables. If both env variables and a config file are present, environment variables will take precendence.
42
+ For more information on combining config files and environment variables, see the [full configuration docs](https://github.com/librato/librato-rails/wiki/Configuration).
35
43
 
36
- Note that using a configuration file allows you to specify configurations per-environment. Submission will be disabled in any environment without credentials. However, if environment variables are set they will be used in all environments.
44
+ ##### Running on Heroku
45
+
46
+ If you are using the Librato Metrics Heroku addon, your user and token environment variables will already be set in your Heroku environment. If you are running without the addon you will need to provide them yourself.
47
+
48
+ In either case you will need to specify a custom source for your app to track properly. If `librato-rails` does not detect an explicit source it will not start. You can set the source in your environment:
49
+
50
+ heroku config:add LIBRATO_METRICS_SOURCE=myappname
51
+
52
+ If you are using a config file, add your source entry to that instead.
37
53
 
38
54
  Full information on configuration options is available on the [configuration wiki page](https://github.com/librato/librato-rails/wiki/Configuration).
39
55
 
56
+ Note that if Heroku idles your application measurements will not be sent until it receives another request and is restarted. If you see intermittent gaps in your measurements during periods of low traffic this is the most likely cause.
57
+
40
58
  ## Automatic Measurements
41
59
 
42
- After installing `librato-rails` and restarting your app and you will see a number of new metrics appear in your Metrics account. These track request performance, sql queries, mail handling, and other key stats. All built-in performance metrics start with the prefix `rails` by convention &mdash; for example: `rails.request.total` is the total number of requests received during an interval.
60
+ After installing `librato-rails` and restarting your app and you will see a number of new metrics appear in your Metrics account. These track request performance, sql queries, mail handling, and other key stats.
43
61
 
44
- If you have multiple apps reporting to the same Metrics account you can change this prefix in your [configuration](https://github.com/librato/librato-rails/wiki/Configuration).
62
+ Built-in performance metrics will start with either `rack` or `rails`, depending on the level they are being sampled from. For example: `rails.request.total` is the total number of requests rails has received each minute.
45
63
 
46
64
  ## Custom Measurements
47
65
 
@@ -111,7 +129,11 @@ Can also be written as:
111
129
 
112
130
  Symbols can be used interchangably with strings for metric names.
113
131
 
114
- ## Cross-process Aggregation
132
+ ## Custom Prefix
133
+
134
+ You can set an optional prefix to all metrics reported by `librato-rails` in your [configuration](https://github.com/librato/librato-rails/wiki/Configuration). This can be helpful for isolating test data or forcing different apps to use different metric names.
135
+
136
+ ## Cross-Process Aggregation
115
137
 
116
138
  `librato-rails` submits measurements back to the Librato platform on a _per-process_ basis. By default these measurements are then combined into a single measurement per source (default is your hostname) before persisting the data.
117
139
 
@@ -121,6 +143,16 @@ Current pricing applies after aggregation, so in this case you will be charged f
121
143
  If you want to report per-process instead, you can set `source_pids` to `true` in
122
144
  your config, which will append the process id to the source name used by each thread.
123
145
 
146
+ ## Troubleshooting
147
+
148
+ Note that it may take 2-3 minutes for the first results to show up in your Metrics account after you have started your servers with `librato-rails` enabled and the first request has been received.
149
+
150
+ If you want to get more information about `librato-rails` submissions to the Metrics service you can set your `log_level` to `debug` (see [configuration](https://github.com/librato/librato-rails/wiki/Configuration)) to get detailed information added to your logs about the settings `librato-rails` is seeing at startup and when it is submitting.
151
+
152
+ If you are having an issue with a specific metric, using a `log_level` of `trace` will add the exact measurements being sent to your logs along with lots of other information about `librato-rails` as it executes. Neither of these modes are recommended long-term in production as they will add quite a bit of volume to your log file and will slow operation somewhat. Note that submission I/O is non-blocking, submission times are total time - your process will continue to handle requests during submissions.
153
+
154
+ If you are debugging setting up `librato-rails` locally you can set `flush_interval` to something shorter (say 10s) to force submission more frequently. Don't change your `flush_interval` in production as it will not result in measurements showing up more quickly, but may affect performance.
155
+
124
156
  ## Contribution
125
157
 
126
158
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
@@ -1 +1 @@
1
- require 'librato/rails'
1
+ require 'librato/rails'
@@ -19,8 +19,9 @@ module Librato
19
19
 
20
20
  module Rails
21
21
  extend SingleForwardable
22
- CONFIG_SETTABLE = %w{user token flush_interval prefix source source_pids}
22
+ CONFIG_SETTABLE = %w{user token flush_interval log_level prefix source source_pids}
23
23
  FORKING_SERVERS = [:unicorn, :passenger]
24
+ LOG_LEVELS = [:off, :error, :warn, :info, :debug, :trace]
24
25
 
25
26
  mattr_accessor :config_file
26
27
  self.config_file = 'config/librato.yml'
@@ -34,9 +35,14 @@ module Librato
34
35
  # config defaults
35
36
  self.flush_interval = 60 # seconds
36
37
  self.source_pids = false # append process id to the source?
38
+ # log_level (default :info)
39
+ # source (default: your machine's hostname)
40
+
41
+ # handy introspection
42
+ mattr_accessor :explicit_source
37
43
 
38
44
  # a collector instance handles all measurement addition/storage
39
- def_delegators :collector, :aggregate, :counters, :delete_all, :group, :increment,
45
+ def_delegators :collector, :aggregate, :counters, :delete_all, :group, :increment,
40
46
  :measure, :prefix, :prefix=, :timing
41
47
 
42
48
  class << self
@@ -49,15 +55,16 @@ module Librato
49
55
  # detect / update configuration
50
56
  def check_config
51
57
  if self.config_file && File.exists?(self.config_file)
52
- logger.debug "[librato-rails] configuration file present, ignoring ENV variables"
58
+ log :debug, "configuration file present, ignoring ENV variables"
53
59
  env_specific = YAML.load(ERB.new(File.read(config_file)).result)[::Rails.env]
54
60
  settable = CONFIG_SETTABLE & env_specific.keys
55
61
  settable.each { |key| self.send("#{key}=", env_specific[key]) }
56
62
  else
57
- logger.debug "[librato-rails] no configuration file present, using ENV variables"
58
- self.token = ENV['LIBRATO_METRICS_TOKEN'] if ENV['LIBRATO_METRICS_TOKEN']
59
- self.user = ENV['LIBRATO_METRICS_USER'] if ENV['LIBRATO_METRICS_USER']
60
- self.source = ENV['LIBRATO_METRICS_SOURCE'] if ENV['LIBRATO_METRICS_SOURCE']
63
+ log :debug, "no configuration file present, using ENV variables"
64
+ %w{user token source log_level}.each do |settable|
65
+ env_var = "LIBRATO_METRICS_#{settable.upcase}"
66
+ send("#{settable}=", ENV[env_var]) if ENV[env_var]
67
+ end
61
68
  end
62
69
  end
63
70
 
@@ -75,7 +82,7 @@ module Librato
75
82
  def client
76
83
  @client ||= prepare_client
77
84
  end
78
-
85
+
79
86
  # collector instance which is tracking all measurement additions
80
87
  def collector
81
88
  @collector ||= Collector.new
@@ -83,19 +90,45 @@ module Librato
83
90
 
84
91
  # send all current data to Metrics
85
92
  def flush
86
- logger.debug "[librato-rails] flushing #{Process.pid} (#{Time.now}):"
87
- queue = client.new_queue(:source => qualified_source, :prefix => self.prefix)
93
+ log :debug, "flushing pid #{@pid} (#{Time.now}).."
94
+ start = Time.now
95
+ queue = client.new_queue(:source => qualified_source,
96
+ :prefix => self.prefix, :skip_measurement_times => true)
88
97
  # thread safety is handled internally for both stores
89
98
  counters.flush_to(queue)
90
99
  aggregate.flush_to(queue)
91
- logger.debug queue.queued
100
+ trace_queued(queue.queued) if should_log?(:trace)
92
101
  queue.submit unless queue.empty?
102
+ log :trace, "flushed pid #{@pid} in #{(Time.now - start)*1000.to_f}ms"
93
103
  rescue Exception => error
94
- logger.error "[librato-rails] submission failed permanently: #{error}"
104
+ log :error, "submission failed permanently: #{error}"
95
105
  end
96
106
 
97
- def logger
98
- @logger ||= ::Rails.logger
107
+ def log(level, message)
108
+ return unless should_log?(level)
109
+ case level
110
+ when :error, :warn
111
+ method = level
112
+ else
113
+ method = :info
114
+ end
115
+ message = '[librato-rails] ' << message
116
+ logger.send(method, message)
117
+ end
118
+
119
+ # set log level to any of LOG_LEVELS
120
+ def log_level=(level)
121
+ level = level.to_sym
122
+ if LOG_LEVELS.index(level)
123
+ @log_level = level
124
+ require 'pp' if should_log?(:debug)
125
+ else
126
+ raise "Invalid log level '#{level}'"
127
+ end
128
+ end
129
+
130
+ def log_level
131
+ @log_level ||= :info
99
132
  end
100
133
 
101
134
  # source including process pid
@@ -106,19 +139,27 @@ module Librato
106
139
  # run once during Rails startup sequence
107
140
  def setup(app)
108
141
  check_config
109
- return unless credentials_present?
110
- logger.info "[librato-rails] starting up with #{app_server}..."
142
+ trace_settings if should_log?(:debug)
143
+ return unless should_start?
144
+ if app_server == :other
145
+ log :info, "starting up..."
146
+ else
147
+ log :info, "starting up with #{app_server}..."
148
+ end
111
149
  @pid = $$
112
150
  app.middleware.use Librato::Rack::Middleware
113
151
  start_worker unless forking_server?
114
152
  end
115
153
 
116
154
  def source
117
- @source ||= Socket.gethostname
155
+ return @source if @source
156
+ self.explicit_source = false
157
+ @source = Socket.gethostname
118
158
  end
119
159
 
120
160
  # set a custom source
121
161
  def source=(src)
162
+ self.explicit_source = true
122
163
  @source = src
123
164
  end
124
165
 
@@ -129,7 +170,7 @@ module Librato
129
170
  def start_worker
130
171
  return if @worker # already running
131
172
  @pid = $$
132
- logger.debug "[librato-rails] >> starting up worker for pid #{@pid}..."
173
+ log :debug, ">> starting up worker for pid #{@pid}..."
133
174
  @worker = Thread.new do
134
175
  worker = Worker.new
135
176
  worker.run_periodically(self.flush_interval) do
@@ -151,21 +192,34 @@ module Librato
151
192
  :other
152
193
  end
153
194
  end
154
-
155
- def credentials_present?
156
- self.user && self.token
157
- end
158
195
 
159
196
  def forking_server?
160
197
  FORKING_SERVERS.include?(app_server)
161
198
  end
162
199
 
163
- def install_worker_check
164
- ::ApplicationController.prepend_before_filter do |c|
165
- Librato::Rails.check_worker
200
+ # there isn't anything in the environment before the
201
+ # first request to know if we're running on heroku, but
202
+ # they set all hostnames to UUIDs.
203
+ def implicit_source_on_heroku?
204
+ !explicit_source && on_heroku
205
+ end
206
+
207
+ def logger
208
+ @logger ||= if on_heroku
209
+ logger = Logger.new(STDOUT)
210
+ logger.level = Logger::INFO
211
+ logger
212
+ else
213
+ ::Rails.logger
166
214
  end
167
215
  end
168
216
 
217
+ def on_heroku
218
+ # would be nice to have something more specific here,
219
+ # but nothing characteristic in ENV, etc.
220
+ @on_heroku ||= source_is_uuid?(Socket.gethostname)
221
+ end
222
+
169
223
  def prepare_client
170
224
  check_config
171
225
  client = Librato::Metrics::Client.new
@@ -180,6 +234,44 @@ module Librato
180
234
  RUBY_DESCRIPTION.split[0]
181
235
  end
182
236
 
237
+ def should_log?(level)
238
+ LOG_LEVELS.index(self.log_level) >= LOG_LEVELS.index(level)
239
+ end
240
+
241
+ def should_start?
242
+ return false if implicit_source_on_heroku?
243
+ self.user && self.token # are credentials present?
244
+ end
245
+
246
+ def source_is_uuid?(source)
247
+ source =~ /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i
248
+ end
249
+
250
+ # trace current environment
251
+ def trace_environment
252
+ log :info, "Environment: " + ENV.pretty_inspect
253
+ end
254
+
255
+ # trace metrics being sent
256
+ def trace_queued(queued)
257
+ log :trace, "Queued: " + queued.pretty_inspect
258
+ end
259
+
260
+ def trace_settings
261
+ settings = {
262
+ :user => self.user,
263
+ :token => self.token,
264
+ :source => source,
265
+ :explicit_source => self.explicit_source ? 'true' : 'false',
266
+ :source_pids => self.source_pids ? 'true' : 'false',
267
+ :qualified_source => qualified_source,
268
+ :log_level => log_level,
269
+ :prefix => prefix,
270
+ :flush_interval => self.flush_interval
271
+ }
272
+ log :info, 'Settings: ' + settings.pretty_inspect
273
+ end
274
+
183
275
  def user_agent
184
276
  ua_chunks = []
185
277
  ua_chunks << "librato-rails/#{Librato::Rails::VERSION}"
@@ -13,7 +13,7 @@ module Librato
13
13
  def [](key)
14
14
  fetch(key)
15
15
  end
16
-
16
+
17
17
  def fetch(key, options={})
18
18
  return nil if @cache.empty?
19
19
  gauges = nil
@@ -61,7 +61,7 @@ module Librato
61
61
  options = {}
62
62
  event = args[0].to_s
63
63
  returned = nil
64
-
64
+
65
65
  # handle block or specified argument
66
66
  if block_given?
67
67
  start = Time.now
@@ -72,13 +72,13 @@ module Librato
72
72
  else
73
73
  raise "no value provided"
74
74
  end
75
-
75
+
76
76
  # detect options hash if present
77
77
  if args.length > 1 and args[-1].respond_to?(:each)
78
78
  options = args[-1]
79
79
  end
80
80
  source = options[:source]
81
-
81
+
82
82
  @lock.synchronize do
83
83
  if source
84
84
  @cache.add event => {:source => source, :value => value}
@@ -6,37 +6,37 @@ module Librato
6
6
  module Rails
7
7
  class Collector
8
8
  extend Forwardable
9
-
9
+
10
10
  def_delegators :counters, :increment
11
11
  def_delegators :aggregate, :measure, :timing
12
-
12
+
13
13
  # access to internal aggregator object
14
14
  def aggregate
15
15
  @aggregator_cache ||= Aggregator.new(:prefix => @prefix)
16
16
  end
17
-
17
+
18
18
  # access to internal counters object
19
19
  def counters
20
20
  @counter_cache ||= CounterCache.new
21
21
  end
22
-
22
+
23
23
  # remove any accumulated but unsent metrics
24
24
  def delete_all
25
25
  aggregate.delete_all
26
26
  counters.delete_all
27
27
  end
28
-
28
+
29
29
  def group(prefix)
30
30
  group = Group.new(prefix)
31
31
  yield group
32
32
  end
33
-
33
+
34
34
  # update prefix
35
35
  def prefix=(new_prefix)
36
36
  @prefix = new_prefix
37
37
  aggregate.prefix = @prefix
38
38
  end
39
-
39
+
40
40
  def prefix
41
41
  @prefix
42
42
  end
@@ -1,19 +1,19 @@
1
1
  module Librato
2
2
  module Rails
3
-
3
+
4
4
  class CounterCache
5
5
  DEFAULT_SOURCE = '%%'
6
-
6
+
7
7
  extend Forwardable
8
-
8
+
9
9
  def_delegators :@cache, :empty?
10
-
10
+
11
11
  def initialize
12
12
  @cache = {}
13
13
  @lock = Mutex.new
14
14
  @sporadics = {}
15
15
  end
16
-
16
+
17
17
  # Retrieve the current value for a given metric. This is a short
18
18
  # form for convenience which only retrieves metrics with no custom
19
19
  # source specified. For more options see #fetch.
@@ -21,30 +21,30 @@ module Librato
21
21
  # @param [String|Symbol] key metric name
22
22
  # @return [Integer|Float] current value
23
23
  def [](key)
24
- @lock.synchronize do
25
- @cache[key.to_s][DEFAULT_SOURCE]
24
+ @lock.synchronize do
25
+ @cache[key.to_s][DEFAULT_SOURCE]
26
26
  end
27
27
  end
28
-
28
+
29
29
  # removes all tracked metrics. note this removes all measurement
30
30
  # data AND metric names any continuously tracked metrics will not
31
31
  # report until they get another measurement
32
32
  def delete_all
33
33
  @lock.synchronize { @cache.clear }
34
34
  end
35
-
36
-
35
+
36
+
37
37
  def fetch(key, options={})
38
38
  source = DEFAULT_SOURCE
39
39
  if options[:source]
40
40
  source = options[:source].to_s
41
41
  end
42
- @lock.synchronize do
42
+ @lock.synchronize do
43
43
  return nil unless @cache[key.to_s]
44
44
  @cache[key.to_s][source]
45
45
  end
46
46
  end
47
-
47
+
48
48
  # transfer all measurements to queue and reset internal status
49
49
  def flush_to(queue)
50
50
  counts = nil
@@ -64,7 +64,7 @@ module Librato
64
64
  end
65
65
  end
66
66
  end
67
-
67
+
68
68
  # Increment a given metric
69
69
  #
70
70
  # @example Increment metric 'foo' by 1
@@ -78,7 +78,7 @@ module Librato
78
78
  #
79
79
  def increment(counter, options={})
80
80
  counter = counter.to_s
81
- if options.is_a?(Fixnum)
81
+ if options.is_a?(Fixnum)
82
82
  # suppport legacy style
83
83
  options = {:by => options}
84
84
  end
@@ -96,14 +96,14 @@ module Librato
96
96
  @cache[counter][source] += by
97
97
  end
98
98
  end
99
-
99
+
100
100
  private
101
-
101
+
102
102
  def make_sporadic(metric, source)
103
103
  @sporadics[metric] ||= Set.new
104
104
  @sporadics[metric] << source
105
105
  end
106
-
106
+
107
107
  def reset_cache
108
108
  # remove any source/metric pairs that aren't continuous
109
109
  @sporadics.each do |key, sources|
@@ -115,8 +115,8 @@ module Librato
115
115
  @cache[key].each_key { |source| @cache[key][source] = 0 }
116
116
  end
117
117
  end
118
-
118
+
119
119
  end
120
-
120
+
121
121
  end
122
122
  end