newrelic_plugin 1.0.3 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.md +44 -1
  3. data/README.md +11 -5
  4. data/lib/newrelic_platform_binding.rb +11 -0
  5. data/lib/newrelic_platform_binding/component.rb +27 -0
  6. data/lib/newrelic_platform_binding/config.rb +39 -0
  7. data/lib/newrelic_platform_binding/connection.rb +86 -0
  8. data/lib/newrelic_platform_binding/context.rb +49 -0
  9. data/lib/newrelic_platform_binding/logger.rb +40 -0
  10. data/lib/newrelic_platform_binding/metric.rb +49 -0
  11. data/lib/newrelic_platform_binding/request.rb +111 -0
  12. data/lib/newrelic_plugin.rb +1 -4
  13. data/lib/newrelic_plugin/agent.rb +14 -21
  14. data/lib/newrelic_plugin/config.rb +2 -1
  15. data/lib/newrelic_plugin/processors/rate_processor.rb +3 -3
  16. data/lib/newrelic_plugin/run.rb +97 -54
  17. data/lib/newrelic_plugin/setup.rb +10 -19
  18. data/lib/newrelic_plugin/version.rb +1 -1
  19. data/newrelic_plugin.gemspec +4 -4
  20. data/test/newrelic_platform_binding/component_test.rb +16 -0
  21. data/test/newrelic_platform_binding/config_test.rb +89 -0
  22. data/test/newrelic_platform_binding/connection_test.rb +67 -0
  23. data/test/newrelic_platform_binding/context_test.rb +35 -0
  24. data/test/newrelic_platform_binding/logger_test.rb +33 -0
  25. data/test/newrelic_platform_binding/metric_test.rb +97 -0
  26. data/test/newrelic_platform_binding/request_test.rb +127 -0
  27. data/test/newrelic_plugin/agent_test.rb +127 -0
  28. data/test/newrelic_plugin/run_test.rb +77 -0
  29. data/test/newrelic_plugin/setup_test.rb +17 -0
  30. data/test/test_helper.rb +11 -4
  31. metadata +59 -17
  32. data/lib/newrelic_plugin/data_collector.rb +0 -67
  33. data/lib/newrelic_plugin/logger.rb +0 -19
  34. data/lib/newrelic_plugin/new_relic_connection.rb +0 -67
  35. data/lib/newrelic_plugin/new_relic_message.rb +0 -173
  36. data/test/agent_test.rb +0 -153
  37. data/test/logger_test.rb +0 -21
  38. data/test/manual_test.rb +0 -20
  39. data/test/new_relic_message_test.rb +0 -76
@@ -1,13 +1,10 @@
1
+ require "newrelic_platform_binding"
1
2
  require "newrelic_plugin/version"
2
3
  require "newrelic_plugin/error"
3
4
  require "newrelic_plugin/config"
4
- require "newrelic_plugin/new_relic_connection"
5
- require "newrelic_plugin/new_relic_message"
6
5
  require "newrelic_plugin/agent"
7
- require "newrelic_plugin/data_collector"
8
6
  require "newrelic_plugin/setup" # cleanup needed
9
7
  require "newrelic_plugin/run" # cleanup needed
10
8
  require "newrelic_plugin/processor"
11
9
  require "newrelic_plugin/processors/epoch_counter_processor" # port needed
12
10
  require "newrelic_plugin/processors/rate_processor" # port needed
13
- require "newrelic_plugin/logger"
@@ -69,19 +69,15 @@ module NewRelic
69
69
  #
70
70
  # Instance Info
71
71
  #
72
- attr_reader :name,:agent_info
72
+ attr_reader :name
73
73
  def guid
74
74
  return @guid if @guid
75
- @guid=self.class.guid
75
+ @guid = self.class.guid
76
76
  #
77
77
  # Verify GUID is set correctly...
78
78
  #
79
- if @guid=="guid" or @guid=="_TYPE_YOUR_GUID_HERE_"
80
- #this feels tightly coupled to the config class, testing this is difficult, thinking about refactoring (NH)
81
- @guid = NewRelic::Plugin::Config.config.newrelic['guids'][agent_info[:ident].to_s] if NewRelic::Plugin::Config.config.newrelic['guids']
82
- Logger.write "NOTE: GUID updated for #{instance_label} at run-time to '#{@guid}'"
83
- end
84
- raise "Did not set GUID" if @guid.nil? or @guid=="" or @guid=="guid" or @guid=="_TYPE_YOUR_GUID_HERE_"
79
+ invalid_guids = ["", "guid", "_TYPE_YOUR_GUID_HERE_"]
80
+ raise "Did not set GUID" if @guid.nil? or invalid_guids.include?(@guid)
85
81
  @guid
86
82
  end
87
83
  def version
@@ -92,22 +88,22 @@ module NewRelic
92
88
  end
93
89
  #
94
90
  # Instantiate a newrelic_plugin instance
95
- def initialize name,agent_info,options={}
96
- @name=name
97
- @agent_info=agent_info
98
- @ident=agent_info[:ident]
99
- @options=options
91
+ def initialize context, options = {}
92
+ @context = context
93
+ @options = options
100
94
  if self.class.config_options_list
101
95
  self.class.config_options_list.each do |config|
102
96
  self.send("#{config}=",options[config])
103
97
  end
104
98
  end
105
- @last_time=nil
99
+ @last_time = nil
106
100
 
107
101
  #
108
102
  # Run agent-specific metric setup, if necessary
109
103
  setup_metrics if respond_to? :setup_metrics
104
+ @component = @context.create_component(instance_label, guid)
110
105
  end
106
+
111
107
  def instance_label
112
108
  if !respond_to? :instance_label_proc_method
113
109
  mod=Module.new
@@ -122,9 +118,9 @@ module NewRelic
122
118
  # Setup & Report Metrics
123
119
  #
124
120
  #
125
- def report_metric metric_name,units,value,opts={}
121
+ def report_metric(metric_name, units, value, opts = {} )
126
122
  return if value.nil?
127
- @data_collector.add_data metric_name,units,value.to_f,opts
123
+ @request.add_metric(@component, "Component/#{metric_name}[#{units}]", value, opts)
128
124
  end
129
125
 
130
126
  #
@@ -132,22 +128,19 @@ module NewRelic
132
128
  # Execute a poll cycle
133
129
  #
134
130
  #
135
- def run poll_interval
131
+ def run(request)
132
+ @request = request
136
133
  #
137
134
  # Start of cycle work, if any
138
135
  cycle_start if respond_to? :cycle_start
139
136
 
140
137
  #
141
138
  # Collect Data
142
- @data_collector=DataCollector.new self,poll_interval
143
139
  poll_cycle
144
- cnt=@data_collector.process
145
- @data_collector=nil
146
140
 
147
141
  #
148
142
  # End of cycle work, if any
149
143
  cycle_end if respond_to? :cycle_end
150
- cnt
151
144
  end
152
145
  end
153
146
  end
@@ -1,4 +1,5 @@
1
1
  require 'yaml'
2
+ require 'erb'
2
3
  module NewRelic
3
4
  module Plugin
4
5
  #
@@ -24,7 +25,7 @@ module NewRelic
24
25
  # directly, rather the global method +config+ should be referenced instead.
25
26
  def initialize
26
27
  @options = YAML::load(Config.config_yaml) if Config.config_yaml
27
- @options = YAML.load_file(Config.config_file) unless @options
28
+ @options = YAML::load(ERB.new(File.read(Config.config_file), 0, '<>').result) unless @options
28
29
  end
29
30
 
30
31
  #
@@ -13,10 +13,10 @@ module NewRelic::Processor
13
13
  # This processor will be removed from the code base shortly...
14
14
  #
15
15
  #
16
- class Rate<NewRelic::Plugin::Processor::Base
16
+ class Rate < NewRelic::Plugin::Processor::Base
17
17
  def initialize
18
- Logger.write "OBSOLESCENCE WARNING: The 'Rate' processor is obsolete and should not be used."
19
- Logger.write "OBSOLESCENCE WARNING: It will be completely removed in the near future."
18
+ Logger.warn("OBSOLESCENCE WARNING: The 'Rate' processor is obsolete and should not be used.")
19
+ Logger.warn("OBSOLESCENCE WARNING: It will be completely removed in the near future.")
20
20
  super :rate,"Rate"
21
21
  end
22
22
  def process val
@@ -2,50 +2,83 @@ module NewRelic
2
2
  module Plugin
3
3
  #
4
4
  # Run class. Provides entry points and polling initiation support.
5
- #
6
- # Author:: Lee Atchison <lee@newrelic.com>
7
- # Copyright:: Copyright (c) 2012 New Relic, Inc.
8
5
  #
9
6
  class Run
10
7
  #
11
8
  # Primary Driver entry point
12
9
  #
13
- def self.setup_and_run component_type_filter=nil
14
- run=new
10
+ def self.setup_and_run component_type_filter = nil
11
+ run = new
15
12
  run.setup_from_config component_type_filter
16
13
  run.setup_no_config_agents
14
+ run.agent_startup
17
15
  run.loop_forever
16
+ run.agent_shutdown
18
17
  end
19
18
  def initialize
20
- @poll_cycle = (NewRelic::Plugin::Config.config.newrelic["poll"] || 60).to_i
21
- @poll_cycle = 60 if (@poll_cycle <= 0) or (@poll_cycle >= 600)
22
- Logger.write "WARNING: Poll cycle differs from 60 seconds (current is #{@poll_cycle})" if @poll_cycle!=60
19
+ @config = NewRelic::Plugin::Config.config
20
+ @context = NewRelic::Binding::Context.new(
21
+ @config.newrelic['license_key']
22
+ )
23
+ configuration_and_logging
24
+ end
25
+
26
+ def configuration_and_logging
27
+ if @config.newrelic["verbose"].to_i > 0
28
+ NewRelic::Logger.log_level = ::Logger::DEBUG
29
+ end
30
+ Logger.info("Using Ruby SDK version: #{NewRelic::Plugin::VERSION}")
31
+
32
+ if @config.newrelic['endpoint']
33
+ NewRelic::Binding::Config.endpoint = @config.newrelic['endpoint']
34
+ Logger.info("Using alternate endpoint: #{NewRelic::Binding::Config.endpoint}")
35
+ end
36
+
37
+ unless @config.newrelic['ssl_host_verification'].nil?
38
+ NewRelic::Binding::Config.ssl_host_verification = !!@config.newrelic['ssl_host_verification']
39
+ Logger.info("Disabling ssl host verification") unless NewRelic::Binding::Config.ssl_host_verification
40
+ end
41
+
42
+ if @config.newrelic['proxy']
43
+ NewRelic::Binding::Config.proxy = @config.newrelic['proxy']
44
+ Logger.info("Using a proxy: #{NewRelic::Binding::Config.proxy.inspect}")
45
+ end
46
+
47
+ @poll_cycle_period = (@config.newrelic["poll"] || 60).to_i
48
+ NewRelic::Binding::Config.poll_cycle_period = @poll_cycle_period
49
+ if @poll_cycle_period <= 0
50
+ message = "A poll cycle period less than or equal to 0 is invalid"
51
+ Logger.fatal(message)
52
+ abort message
53
+ end
54
+ if @poll_cycle_period != 60
55
+ Logger.warn("WARNING: Poll cycle period differs from 60 seconds (current is #{@poll_cycle_period})")
56
+ end
23
57
  end
58
+
24
59
  def installed_agents
25
- if Setup.installed_agents.size==0
26
- Logger.write "No agents installed!"
60
+ if Setup.installed_agents.size == 0
61
+ Logger.error("No agents installed!")
27
62
  raise NoAgents, "No agents installed"
28
63
  end
29
64
  Setup.installed_agents
30
65
  end
31
- #def installed_processors
32
- # Setup.installed_processors
33
- #end
66
+
34
67
  def configured_agents
35
68
  agent_setup.agents
36
69
  end
37
- def setup_from_config component_type_filter=nil
70
+
71
+ def setup_from_config(component_type_filter = nil)
38
72
  return unless NewRelic::Plugin::Config.config.agents
39
- installed_agents.each do |agent_id,installed_agent|
40
- next if component_type_filter and agent_id!=component_type_filter
41
- config_list=NewRelic::Plugin::Config.config.agents[agent_id.to_s]
73
+ installed_agents.each do |agent_id, installed_agent|
74
+ next if component_type_filter and agent_id != component_type_filter
75
+ config_list = NewRelic::Plugin::Config.config.agents[agent_id.to_s]
42
76
  next unless config_list
43
77
  [config_list].flatten.each do |config|
44
78
  next unless config
45
79
  # Convert keys to symbols...
46
- config.keys.each {|key|config[(key.to_sym rescue key) || key] = config.delete(key)}
47
- name=config.delete(:name) # Pull out name and remove from hash
48
- agent_setup.create_agent agent_id,name,config
80
+ config.keys.each { |key|config[(key.to_sym rescue key) || key] = config.delete(key) }
81
+ agent_setup.create_agent @context, agent_id, config
49
82
  end
50
83
  end
51
84
  end
@@ -62,58 +95,68 @@ module NewRelic
62
95
  def setup &block
63
96
  block.call(agent_setup)
64
97
  end
65
- #
66
- # Call this method to loop forever. This will delay an appropriate amount until
67
- # the next metric pull is needed, then it will loop thru all configured agents
68
- # and call each one in turn so it can perform it's appropriate metric pull.
69
- #
70
- def loop_forever
71
- if configured_agents.size==0
98
+
99
+ def agent_startup
100
+ if configured_agents.size == 0
72
101
  err_msg = "No agents configured!"
73
- err_msg+= " Check the agents portion of your yml file." unless NewRelic::Plugin::Config.config.options.empty?
74
- Logger.write err_msg
102
+ err_msg += " Check the agents portion of your yml file." unless NewRelic::Plugin::Config.config.options.empty?
103
+ Logger.error(err_msg)
75
104
  raise NoAgents, err_msg
76
105
  end
77
106
  installed_agents.each do |agent_id,installed_agent|
78
107
  version = installed_agent[:agent_class].version
79
- Logger.write "Agent #{installed_agent[:label]} is at version #{version}" if version
108
+ Logger.info("Agent #{installed_agent[:label]} is at version #{version}") if version
80
109
  end
81
110
  configured_agents.each do |agent|
82
111
  agent.startup if agent.respond_to? :startup
83
112
  end
84
- @done=false
113
+ end
114
+
115
+ #
116
+ # Call this method to loop forever. This will delay an appropriate amount until
117
+ # the next metric pull is needed, then it will loop thru all configured agents
118
+ # and call each one in turn so it can perform it's appropriate metric pull.
119
+ #
120
+
121
+ def loop_forever
122
+ @done = false
85
123
  begin
86
124
  while !@done
87
- #
88
- # Set last run time
89
- @last_run_time=Time.now
90
- #
91
- # Call each agent
92
- cnt=0
93
- configured_agents.each do |agent|
94
- begin
95
- cnt+=agent.run @poll_cycle
96
- rescue => err
97
- Logger.write "Error occurred in poll cycle: #{err}"
98
- end
99
- end
100
- Logger.write "Gathered #{cnt} statistics"
101
- #
102
- # Delay until next run
103
- secs_to_delay=@poll_cycle-(Time.now-@last_run_time)
104
- sleep secs_to_delay if secs_to_delay>0
125
+ @last_run_time = Time.now
126
+ request = @context.get_request()
127
+ run_agent_data_collection(request)
128
+ request.deliver
129
+ Logger.info("Gathered #{request.metric_count} statistics from #{request.component_count} components")
130
+
131
+ seconds_to_delay = @poll_cycle_period - (Time.now - @last_run_time)
132
+ sleep(seconds_to_delay) if seconds_to_delay > 0
133
+ end
134
+ rescue Interrupt => err
135
+ Logger.info "Shutting down..."
136
+ end
137
+ end
138
+
139
+ def run_agent_data_collection(request)
140
+ Logger.debug('Start collecting agent data for poll cycle')
141
+ configured_agents.each do |agent|
142
+ begin
143
+ agent.run(request)
144
+ rescue => err
145
+ Logger.error("Error occurred in poll cycle: #{err}")
105
146
  end
106
- rescue Interrupt =>err
107
- Logger.write "Shutting down..."
108
147
  end
148
+ Logger.debug('Finished collecting agent data for poll cycle')
149
+ end
150
+
151
+ def agent_shutdown
109
152
  configured_agents.each do |agent|
110
153
  agent.shutdown if agent.respond_to? :shutdown
111
154
  end
112
- Logger.write "Shutdown complete"
155
+ Logger.info("Shutdown complete")
113
156
  end
114
- #private
157
+
115
158
  def agent_setup
116
- @agent_setup||=AgentSetup.new
159
+ @agent_setup ||= AgentSetup.new
117
160
  end
118
161
  end
119
162
  end
@@ -2,34 +2,24 @@ module NewRelic
2
2
  module Plugin
3
3
  #
4
4
  # Setup support methods.
5
- #
6
- # Author:: Lee Atchison <lee@newrelic.com>
7
- # Copyright:: Copyright (c) 2012 New Relic, Inc.
8
5
  #
9
6
  #
10
7
  # Setup and Register new agent types and new processors
11
8
  #
12
9
  class Setup
13
10
  class << self
14
- def install_agent ident,klass
15
- @installed_agents||={}
11
+ def install_agent ident, klass
12
+ @installed_agents ||= {}
16
13
  @installed_agents[ident] = {
17
14
  :agent_class => klass::Agent,
18
15
  :label => klass::Agent.label,
19
16
  :ident => ident
20
17
  }
21
18
  end
22
- #def install_processor klass
23
- # @installed_processors||={}
24
- # tmp_instance=klass.new
25
- # @installed_processors[tmp_instance.ident]={ident: tmp_instance.ident,processor_class: klass,label: tmp_instance.label}
26
- #end
19
+
27
20
  def installed_agents
28
- @installed_agents||{}
21
+ @installed_agents || {}
29
22
  end
30
- #def installed_processors
31
- # @installed_processors||{}
32
- #end
33
23
  end
34
24
  end
35
25
 
@@ -39,15 +29,16 @@ module NewRelic
39
29
  class AgentSetup
40
30
  attr_reader :agents
41
31
  def initialize
42
- @agents=[]
32
+ @agents = []
43
33
  end
44
- def create_agent ident,name,options=nil,&block
45
- agent_info=Setup.installed_agents[ident]
34
+ def create_agent context, ident, options = nil, &block
35
+ agent_info = Setup.installed_agents[ident]
46
36
  raise UnknownInstalledAgent,"Unrecognized agent '#{ident}'" unless agent_info
47
- agent=agent_info[:agent_class].new name,agent_info,options
37
+ agent = agent_info[:agent_class].new context, options
48
38
  raise CouldNotInitializeAgent unless agent
49
39
  block.call(agent) if block_given?
50
- @agents<<agent
40
+ context.version = agent.version
41
+ @agents << agent
51
42
  end
52
43
  end
53
44
  end
@@ -1,5 +1,5 @@
1
1
  module NewRelic
2
2
  module Plugin
3
- VERSION = "1.0.3"
3
+ VERSION = "1.2.1"
4
4
  end
5
5
  end
@@ -25,15 +25,15 @@ Gem::Specification.new do |s|
25
25
  "--main", "README.md"]
26
26
  s.extra_rdoc_files = %w[README.md LICENSE]
27
27
 
28
- s.add_dependency 'faraday', '>= 0.8.1'
29
28
  s.add_dependency 'json'
30
29
 
31
30
  ## List your development dependencies here. Development dependencies are
32
31
  ## those that are only needed during development
33
- #s.add_development_dependency "minitest"
34
- #s.add_development_dependency "vcr"
35
- s.add_development_dependency 'shoulda-context'
36
32
  s.add_development_dependency 'mocha'
33
+ s.add_development_dependency "minitest", '>= 5.0.6'
34
+ s.add_development_dependency 'fakeweb'
35
+ s.add_development_dependency 'pry'
36
+ s.add_development_dependency 'timecop'
37
37
 
38
38
  s.files = `git ls-files`.split($\)
39
39
  s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -0,0 +1,16 @@
1
+ require 'test_helper'
2
+
3
+ class ComponentTest < Minitest::Test
4
+
5
+ def setup
6
+ @component = NewRelic::Binding::Component.new('name', 'com.test')
7
+ end
8
+
9
+ def test_that_component_responds_to_name
10
+ assert_equal 'name', @component.name
11
+ end
12
+
13
+ def test_that_component_responds_to_guid
14
+ assert_equal 'com.test', @component.guid
15
+ end
16
+ end
@@ -0,0 +1,89 @@
1
+ require 'test_helper'
2
+
3
+ class ConfigTest < Minitest::Test
4
+
5
+ def setup
6
+ @endpoint = NewRelic::Binding::Config.endpoint
7
+ @ssl_host_verification = NewRelic::Binding::Config.ssl_host_verification
8
+ @uri = NewRelic::Binding::Config.uri
9
+ @poll_cycle_period = NewRelic::Binding::Config.poll_cycle_period
10
+ end
11
+
12
+ def teardown
13
+ NewRelic::Binding::Config.endpoint = @endpoint
14
+ NewRelic::Binding::Config.ssl_host_verification = @ssl_host_verification
15
+ NewRelic::Binding::Config.uri = @uri
16
+ NewRelic::Binding::Config.poll_cycle_period = @poll_cycle_period
17
+ end
18
+
19
+ def test_use_ssl_with_https_url
20
+ NewRelic::Binding::Config.endpoint = 'https://example.com'
21
+ assert_equal true, NewRelic::Binding::Config.use_ssl?
22
+ end
23
+
24
+ def test_use_ssl_with_http_url
25
+ NewRelic::Binding::Config.endpoint = 'http://example.com'
26
+ assert_equal false, NewRelic::Binding::Config.use_ssl?
27
+ end
28
+
29
+ def test_ssl_supported
30
+ assert_equal NewRelic::Binding::Config.ssl_supported?, !(!defined?(RUBY_ENGINE) || (RUBY_ENGINE == 'ruby' && RUBY_VERSION < '1.9.0'))
31
+ end
32
+
33
+ def test_endpoint_default
34
+ if NewRelic::Binding::Config.ssl_supported?
35
+ assert_equal 'https://platform-api.newrelic.com', NewRelic::Binding::Config.endpoint
36
+ else
37
+ assert_equal 'http://platform-api.newrelic.com', NewRelic::Binding::Config.endpoint
38
+ end
39
+ end
40
+
41
+ def test_set_endpoint
42
+ new_endpoint = 'http://example.com'
43
+ NewRelic::Binding::Config.endpoint = new_endpoint
44
+ assert_equal new_endpoint, NewRelic::Binding::Config.endpoint
45
+ end
46
+
47
+ def test_set_ssl_endpoint
48
+ new_endpoint = 'https://example.com'
49
+ if NewRelic::Binding::Config.ssl_supported?
50
+ NewRelic::Logger.expects(:warn).with('Using SSL is not recommended when using Ruby versions below 1.9').never
51
+ else
52
+ NewRelic::Logger.expects(:warn).with('Using SSL is not recommended when using Ruby versions below 1.9')
53
+ end
54
+ NewRelic::Binding::Config.endpoint = new_endpoint
55
+ assert_equal new_endpoint, NewRelic::Binding::Config.endpoint
56
+ end
57
+
58
+ def test_uri_default
59
+ assert_equal '/platform/v1/metrics', NewRelic::Binding::Config.uri
60
+ end
61
+
62
+ def test_set_uri
63
+ new_uri = '/foo'
64
+ NewRelic::Binding::Config.uri = new_uri
65
+ assert_equal new_uri, NewRelic::Binding::Config.uri
66
+ end
67
+
68
+ def test_ssl_host_verification_default
69
+ assert_equal true, NewRelic::Binding::Config.ssl_host_verification
70
+ assert_equal false, NewRelic::Binding::Config.skip_ssl_host_verification?
71
+ end
72
+
73
+ def test_set_ssl_host_verification
74
+ new_ssl_host_verification = false
75
+ NewRelic::Binding::Config.ssl_host_verification = new_ssl_host_verification
76
+ assert_equal new_ssl_host_verification, NewRelic::Binding::Config.ssl_host_verification
77
+ assert_equal true, NewRelic::Binding::Config.skip_ssl_host_verification?
78
+ end
79
+
80
+ def test_poll_cycle_period
81
+ assert_equal 60, NewRelic::Binding::Config.poll_cycle_period
82
+ end
83
+
84
+ def test_set_poll_cycle_period
85
+ new_poll_cycle_period = 120
86
+ NewRelic::Binding::Config.poll_cycle_period = new_poll_cycle_period
87
+ assert_equal new_poll_cycle_period, NewRelic::Binding::Config.poll_cycle_period
88
+ end
89
+ end