appsignal 0.11.18 → 0.12.beta.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/.gitignore +6 -0
  2. data/CHANGELOG.md +4 -38
  3. data/Rakefile +14 -6
  4. data/appsignal.gemspec +3 -1
  5. data/benchmark.rake +12 -16
  6. data/ext/appsignal_extension.c +183 -0
  7. data/ext/extconf.rb +39 -0
  8. data/gemfiles/capistrano2.gemfile +0 -1
  9. data/gemfiles/capistrano3.gemfile +0 -1
  10. data/gemfiles/rails-4.2.gemfile +1 -1
  11. data/lib/appsignal.rb +23 -61
  12. data/lib/appsignal/capistrano.rb +1 -2
  13. data/lib/appsignal/config.rb +13 -1
  14. data/lib/appsignal/event_formatter.rb +67 -0
  15. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +23 -0
  16. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +74 -0
  17. data/lib/appsignal/event_formatter/moped/query_formatter.rb +80 -0
  18. data/lib/appsignal/event_formatter/net_http/request_formatter.rb +13 -0
  19. data/lib/appsignal/instrumentations/net_http.rb +6 -4
  20. data/lib/appsignal/integrations/resque.rb +2 -10
  21. data/lib/appsignal/integrations/sidekiq.rb +2 -2
  22. data/lib/appsignal/integrations/sinatra.rb +1 -0
  23. data/lib/appsignal/js_exception_transaction.rb +44 -28
  24. data/lib/appsignal/marker.rb +11 -13
  25. data/lib/appsignal/params_sanitizer.rb +5 -8
  26. data/lib/appsignal/rack/instrumentation.rb +2 -0
  27. data/lib/appsignal/rack/js_exception_catcher.rb +1 -0
  28. data/lib/appsignal/rack/listener.rb +1 -1
  29. data/lib/appsignal/rack/sinatra_instrumentation.rb +2 -12
  30. data/lib/appsignal/subscriber.rb +59 -0
  31. data/lib/appsignal/transaction.rb +117 -174
  32. data/lib/appsignal/transmitter.rb +8 -37
  33. data/lib/appsignal/version.rb +2 -1
  34. data/spec/lib/appsignal/config_spec.rb +25 -4
  35. data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +42 -0
  36. data/spec/lib/appsignal/{aggregator/middleware/active_record_sanitizer_spec.rb → event_formatter/active_record/sql_formatter_spec.rb} +61 -61
  37. data/spec/lib/appsignal/{event/moped_event_spec.rb → event_formatter/moped/query_formatter_spec.rb} +32 -78
  38. data/spec/lib/appsignal/event_formatter/net_http/request_formatter_spec.rb +26 -0
  39. data/spec/lib/appsignal/event_formatter_spec.rb +102 -0
  40. data/spec/lib/appsignal/extension_spec.rb +75 -0
  41. data/spec/lib/appsignal/instrumentations/net_http_spec.rb +20 -4
  42. data/spec/lib/appsignal/integrations/delayed_job_spec.rb +3 -2
  43. data/spec/lib/appsignal/integrations/rails_spec.rb +0 -7
  44. data/spec/lib/appsignal/integrations/resque_spec.rb +51 -55
  45. data/spec/lib/appsignal/integrations/sequel_spec.rb +8 -3
  46. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +4 -21
  47. data/spec/lib/appsignal/integrations/sinatra_spec.rb +0 -6
  48. data/spec/lib/appsignal/js_exception_transaction_spec.rb +57 -60
  49. data/spec/lib/appsignal/params_sanitizer_spec.rb +11 -27
  50. data/spec/lib/appsignal/rack/listener_spec.rb +6 -6
  51. data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +2 -43
  52. data/spec/lib/appsignal/subscriber_spec.rb +162 -0
  53. data/spec/lib/appsignal/transaction_spec.rb +283 -615
  54. data/spec/lib/appsignal/transmitter_spec.rb +3 -32
  55. data/spec/lib/appsignal_spec.rb +41 -90
  56. data/spec/lib/generators/appsignal/appsignal_generator_spec.rb +0 -17
  57. data/spec/spec_helper.rb +18 -22
  58. data/spec/support/helpers/notification_helpers.rb +1 -1
  59. data/spec/support/helpers/time_helpers.rb +11 -0
  60. data/spec/support/helpers/transaction_helpers.rb +6 -18
  61. data/spec/support/project_fixture/config/appsignal.yml +1 -2
  62. metadata +68 -78
  63. checksums.yaml +0 -7
  64. data/gemfiles/padrino-0.13.gemfile +0 -7
  65. data/gemfiles/resque.gemfile +0 -5
  66. data/lib/appsignal/agent.rb +0 -217
  67. data/lib/appsignal/aggregator.rb +0 -67
  68. data/lib/appsignal/aggregator/middleware.rb +0 -4
  69. data/lib/appsignal/aggregator/middleware/action_view_sanitizer.rb +0 -23
  70. data/lib/appsignal/aggregator/middleware/active_record_sanitizer.rb +0 -65
  71. data/lib/appsignal/aggregator/middleware/chain.rb +0 -101
  72. data/lib/appsignal/aggregator/middleware/delete_blanks.rb +0 -16
  73. data/lib/appsignal/aggregator/post_processor.rb +0 -32
  74. data/lib/appsignal/event.rb +0 -20
  75. data/lib/appsignal/event/moped_event.rb +0 -90
  76. data/lib/appsignal/integrations/padrino.rb +0 -64
  77. data/lib/appsignal/integrations/passenger.rb +0 -13
  78. data/lib/appsignal/integrations/rake.rb +0 -29
  79. data/lib/appsignal/integrations/unicorn.rb +0 -25
  80. data/lib/appsignal/ipc.rb +0 -68
  81. data/lib/appsignal/transaction/formatter.rb +0 -85
  82. data/lib/appsignal/transaction/params_sanitizer.rb +0 -4
  83. data/lib/appsignal/zipped_payload.rb +0 -37
  84. data/spec/lib/appsignal/agent_spec.rb +0 -592
  85. data/spec/lib/appsignal/aggregator/middleware/action_view_sanitizer_spec.rb +0 -44
  86. data/spec/lib/appsignal/aggregator/middleware/chain_spec.rb +0 -168
  87. data/spec/lib/appsignal/aggregator/middleware/delete_blanks_spec.rb +0 -37
  88. data/spec/lib/appsignal/aggregator/post_processor_spec.rb +0 -99
  89. data/spec/lib/appsignal/aggregator_spec.rb +0 -186
  90. data/spec/lib/appsignal/event_spec.rb +0 -48
  91. data/spec/lib/appsignal/integrations/padrino_spec.rb +0 -171
  92. data/spec/lib/appsignal/integrations/passenger_spec.rb +0 -22
  93. data/spec/lib/appsignal/integrations/rake_spec.rb +0 -92
  94. data/spec/lib/appsignal/integrations/unicorn_spec.rb +0 -48
  95. data/spec/lib/appsignal/ipc_spec.rb +0 -128
  96. data/spec/lib/appsignal/transaction/formatter_spec.rb +0 -247
  97. data/spec/lib/appsignal/zipped_payload_spec.rb +0 -42
@@ -1,22 +1,19 @@
1
1
  require 'logger'
2
- require 'rack'
3
- require 'thread_safe'
4
2
  require 'securerandom'
5
3
 
6
4
  begin
7
5
  require 'active_support/notifications'
8
- rescue LoadError
6
+ ActiveSupport::Notifications::Fanout::Subscribers::Timed # See it it's recent enough
7
+ rescue LoadError, NameError
9
8
  require 'vendor/active_support/notifications'
10
9
  end
11
10
 
12
11
  module Appsignal
13
12
  class << self
14
- attr_accessor :config, :logger, :agent, :in_memory_log
13
+ attr_accessor :config, :subscriber, :logger, :agent, :in_memory_log
15
14
 
16
15
  def load_integrations
17
16
  require 'appsignal/integrations/delayed_job'
18
- require 'appsignal/integrations/passenger'
19
- require 'appsignal/integrations/unicorn'
20
17
  require 'appsignal/integrations/sidekiq'
21
18
  require 'appsignal/integrations/resque'
22
19
  require 'appsignal/integrations/sequel'
@@ -47,14 +44,13 @@ module Appsignal
47
44
  end
48
45
  if config.active?
49
46
  logger.info("Starting AppSignal #{Appsignal::VERSION} on #{RUBY_VERSION}/#{RUBY_PLATFORM}")
47
+ config.write_to_environment
48
+ Appsignal::Extension.start
50
49
  load_integrations
51
50
  load_instrumentations
51
+ Appsignal::EventFormatter.initialize_formatters
52
52
  initialize_extensions
53
- @agent = Appsignal::Agent.new
54
- at_exit do
55
- logger.debug('Running at_exit block')
56
- @agent.send_queue
57
- end
53
+ @subscriber = Appsignal::Subscriber.new
58
54
  else
59
55
  logger.info("Not starting, not active for #{config.env}")
60
56
  end
@@ -63,17 +59,6 @@ module Appsignal
63
59
  end
64
60
  end
65
61
 
66
- # Convenience method for adding a transaction to the queue. This queue is
67
- # managed and is periodically pushed to Appsignal.
68
- #
69
- # @return [ true ] True.
70
- #
71
- # @since 0.5.0
72
- def enqueue(transaction)
73
- return unless active?
74
- agent.enqueue(transaction)
75
- end
76
-
77
62
  def monitor_transaction(name, payload={})
78
63
  unless active?
79
64
  yield
@@ -81,12 +66,12 @@ module Appsignal
81
66
  end
82
67
 
83
68
  begin
84
- Appsignal::Transaction.create(SecureRandom.uuid, ENV)
69
+ Appsignal::Transaction.create(SecureRandom.uuid, ENV.to_hash)
85
70
  ActiveSupport::Notifications.instrument(name, payload) do
86
71
  yield
87
72
  end
88
- rescue Exception => exception
89
- Appsignal.add_exception(exception)
73
+ rescue => exception
74
+ Appsignal.set_exception(exception)
90
75
  raise exception
91
76
  ensure
92
77
  Appsignal::Transaction.complete_current!
@@ -102,23 +87,18 @@ module Appsignal
102
87
 
103
88
  def send_exception(exception, tags=nil)
104
89
  return if !active? || is_ignored_exception?(exception)
105
- unless exception.is_a?(Exception)
106
- logger.error('Can\'t send exception, given value is not an exception')
107
- return
108
- end
109
- transaction = Appsignal::Transaction.create(SecureRandom.uuid, nil)
110
- transaction.add_exception(exception)
90
+ transaction = Appsignal::Transaction.create(SecureRandom.uuid, ENV.to_hash)
111
91
  transaction.set_tags(tags) if tags
112
- transaction.complete!
113
- Appsignal.agent.send_queue
92
+ transaction.set_error(exception)
93
+ Appsignal::Transaction.complete_current!
114
94
  end
115
95
 
116
- def add_exception(exception)
96
+ def set_exception(exception)
117
97
  return if !active? ||
118
98
  Appsignal::Transaction.current.nil? ||
119
99
  exception.nil? ||
120
100
  is_ignored_exception?(exception)
121
- Appsignal::Transaction.current.add_exception(exception)
101
+ Appsignal::Transaction.current.set_error(exception)
122
102
  end
123
103
 
124
104
  def tag_request(params={})
@@ -129,10 +109,6 @@ module Appsignal
129
109
  end
130
110
  alias :tag_job :tag_request
131
111
 
132
- def transactions
133
- @transactions ||= {}
134
- end
135
-
136
112
  def logger
137
113
  @in_memory_log = StringIO.new unless @in_memory_log
138
114
  @logger ||= Logger.new(@in_memory_log).tap do |l|
@@ -157,15 +133,8 @@ module Appsignal
157
133
  @logger << @in_memory_log.string if @in_memory_log
158
134
  end
159
135
 
160
- def post_processing_middleware
161
- @post_processing_chain ||= Appsignal::Aggregator::PostProcessor.default_middleware
162
- yield @post_processing_chain if block_given?
163
- @post_processing_chain
164
- end
165
-
166
136
  def active?
167
- config && config.active? &&
168
- agent && agent.active?
137
+ config && config.active?
169
138
  end
170
139
 
171
140
  def is_ignored_exception?(exception)
@@ -188,25 +157,18 @@ module Appsignal
188
157
  end
189
158
  end
190
159
 
191
- require 'appsignal/agent'
192
- require 'appsignal/event'
193
- require 'appsignal/aggregator'
194
- require 'appsignal/aggregator/post_processor'
195
- require 'appsignal/aggregator/middleware'
160
+ require 'appsignal_extension'
196
161
  require 'appsignal/auth_check'
197
162
  require 'appsignal/config'
163
+ require 'appsignal/event_formatter'
198
164
  require 'appsignal/marker'
199
- require 'appsignal/rack/listener'
200
- require 'appsignal/rack/instrumentation'
201
- require 'appsignal/rack/sinatra_instrumentation'
202
- require 'appsignal/rack/js_exception_catcher'
203
165
  require 'appsignal/params_sanitizer'
166
+ require 'appsignal/subscriber'
204
167
  require 'appsignal/transaction'
205
- require 'appsignal/transaction/formatter'
206
- require 'appsignal/transaction/params_sanitizer'
207
- require 'appsignal/transmitter'
208
- require 'appsignal/zipped_payload'
209
- require 'appsignal/ipc'
210
168
  require 'appsignal/version'
169
+ require 'appsignal/rack/js_exception_catcher'
170
+ require 'appsignal/rack/listener'
171
+ require 'appsignal/rack/instrumentation'
211
172
  require 'appsignal/integrations/rails'
212
173
  require 'appsignal/js_exception_transaction'
174
+ require 'appsignal/transmitter'
@@ -1,7 +1,6 @@
1
1
  require 'appsignal'
2
- require 'capistrano/version'
3
2
 
4
- if defined?(Capistrano::VERSION) && Gem::Version.new(Capistrano::VERSION) >= Gem::Version.new(3)
3
+ if defined?(Capistrano::VERSION)
5
4
  # Capistrano 3+
6
5
  load File.expand_path('../integrations/capistrano/appsignal.cap', __FILE__)
7
6
  else
@@ -10,7 +10,7 @@ module Appsignal
10
10
  :ignore_exceptions => [],
11
11
  :ignore_actions => [],
12
12
  :send_params => true,
13
- :endpoint => 'https://push.appsignal.com/1',
13
+ :endpoint => 'https://push.appsignal.com',
14
14
  :slow_request_threshold => 200,
15
15
  :instrument_net_http => true,
16
16
  :skip_session_data => false,
@@ -67,6 +67,18 @@ module Appsignal
67
67
  !! self[:active]
68
68
  end
69
69
 
70
+ def write_to_environment
71
+ ENV['APPSIGNAL_ACTIVE'] = active?.to_s
72
+ ENV['APPSIGNAL_APP_PATH'] = root_path.to_s
73
+ ENV['APPSIGNAL_AGENT_PATH'] = File.expand_path('../../../ext', __FILE__).to_s
74
+ ENV['APPSIGNAL_LOG_PATH'] = File.join(root_path, 'log')
75
+ ENV['APPSIGNAL_PUSH_API_ENDPOINT'] = config_hash[:endpoint]
76
+ ENV['APPSIGNAL_PUSH_API_KEY'] = config_hash[:push_api_key]
77
+ ENV['APPSIGNAL_APP_NAME'] = config_hash[:name]
78
+ ENV['APPSIGNAL_ENVIRONMENT'] = env
79
+ ENV['APPSIGNAL_AGENT_VERSION'] = Appsignal::AGENT_VERSION
80
+ end
81
+
70
82
  protected
71
83
 
72
84
  def config_file
@@ -0,0 +1,67 @@
1
+ module Appsignal
2
+ # Keeps track of formatters for types event that we can use to get
3
+ # the title and body of an event. Formatters should inherit from this class
4
+ # and implement a format(payload) method which returns an array with the title
5
+ # and body.
6
+ #
7
+ # When implementing a formatter remember that it cannot keep separate state per
8
+ # event, the same object will be called intermittently in a threaded environment.
9
+ # So only keep global configuration as state and pass the payload around as an
10
+ # argument if you need to use helper methods.
11
+ class EventFormatter
12
+ class << self
13
+ def formatters
14
+ @@formatters ||= {}
15
+ end
16
+
17
+ def formatter_classes
18
+ @@formatter_classes ||= {}
19
+ end
20
+
21
+ def register(name, formatter=self)
22
+ formatter_classes[name] = formatter
23
+ end
24
+
25
+ def unregister(name, formatter=self)
26
+ if formatter_classes[name] == formatter
27
+ formatter_classes.delete(name)
28
+ formatters.delete(name)
29
+ end
30
+ end
31
+
32
+ def registered?(name, klass=nil)
33
+ if klass
34
+ formatter_classes[name] == klass
35
+ else
36
+ formatter_classes.include?(name)
37
+ end
38
+ end
39
+
40
+ def initialize_formatters
41
+ formatter_classes.each do |name, formatter|
42
+ begin
43
+ format_method = formatter.instance_method(:format)
44
+ if format_method && format_method.arity == 1
45
+ formatters[name] = formatter.new
46
+ else
47
+ raise "#{f} does not have a format(payload) method"
48
+ end
49
+ rescue Exception => ex
50
+ formatter_classes.delete(name)
51
+ formatters.delete(name)
52
+ Appsignal.logger.debug("'#{ex.message}' when initializing #{name} event formatter")
53
+ end
54
+ end
55
+ end
56
+
57
+ def format(name, payload)
58
+ formatter = formatters[name]
59
+ formatter.format(payload) unless formatter.nil?
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ Dir.glob(File.expand_path('../event_formatter/**/*.rb', __FILE__)).each do |file|
66
+ require file
67
+ end
@@ -0,0 +1,23 @@
1
+ module Appsignal
2
+ class EventFormatter
3
+ module ActionView
4
+ class RenderFormatter < Appsignal::EventFormatter
5
+ register 'render_partial.action_view'
6
+ register 'render_template.action_view'
7
+
8
+ BLANK = ''.freeze
9
+
10
+ attr_reader :root_path
11
+
12
+ def initialize
13
+ @root_path = "#{Rails.root.to_s}/".freeze
14
+ end
15
+
16
+ def format(payload)
17
+ return nil unless payload[:identifier]
18
+ [payload[:identifier].sub(root_path, BLANK), nil]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,74 @@
1
+ module Appsignal
2
+ class EventFormatter
3
+ module ActiveRecord
4
+ class SqlFormatter < Appsignal::EventFormatter
5
+ register 'sql.active_record'
6
+
7
+ SINGLE_QUOTE = /\\'/.freeze
8
+ DOUBLE_QUOTE = /\\"/.freeze
9
+ QUOTED_DATA = /(?:"[^"]+"|'[^']+')/.freeze
10
+ SINGLE_QUOTED_DATA = /(?:'[^']+')/.freeze
11
+ IN_ARRAY = /(IN \()[^\)]+(\))/.freeze
12
+ NUMERIC_DATA = /\b\d+\b/.freeze
13
+ SANITIZED_VALUE = '\1?\2'.freeze
14
+
15
+ attr_reader :adapter_uses_double_quoted_table_names, :adapter_uses_prepared_statements
16
+
17
+ def initialize
18
+ @connection_config = connection_config
19
+ @adapter_uses_prepared_statements = adapter_uses_prepared_statements?
20
+ @adapter_uses_double_quoted_table_names = adapter_uses_double_quoted_table_names?
21
+ rescue ::ActiveRecord::ConnectionNotEstablished
22
+ Appsignal::EventFormatter.unregister('sql.active_record', self.class)
23
+ Appsignal.logger.error('Error while getting ActiveRecord connection info, unregistering sql.active_record event formatter')
24
+ end
25
+
26
+ def format(payload)
27
+ return nil if schema_query?(payload) || !payload[:sql]
28
+ if adapter_uses_prepared_statements
29
+ [payload[:name], payload[:sql]]
30
+ else
31
+ sql_string = payload[:sql].dup
32
+ if adapter_uses_double_quoted_table_names
33
+ sql_string.gsub!(SINGLE_QUOTE, SANITIZED_VALUE)
34
+ sql_string.gsub!(SINGLE_QUOTED_DATA, SANITIZED_VALUE)
35
+ else
36
+ sql_string.gsub!(SINGLE_QUOTE, SANITIZED_VALUE)
37
+ sql_string.gsub!(DOUBLE_QUOTE, SANITIZED_VALUE)
38
+ sql_string.gsub!(QUOTED_DATA, SANITIZED_VALUE)
39
+ end
40
+ sql_string.gsub!(IN_ARRAY, SANITIZED_VALUE)
41
+ sql_string.gsub!(NUMERIC_DATA, SANITIZED_VALUE)
42
+ [payload[:name], sql_string]
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ def schema_query?(payload)
49
+ payload[:name] == 'SCHEMA'
50
+ end
51
+
52
+ def connection_config
53
+ # TODO handle ActiveRecord::ConnectionNotEstablished
54
+ if ::ActiveRecord::Base.respond_to?(:connection_config)
55
+ ::ActiveRecord::Base.connection_config
56
+ else
57
+ ::ActiveRecord::Base.connection_pool.spec.config
58
+ end
59
+ end
60
+
61
+ def adapter_uses_double_quoted_table_names?
62
+ adapter = @connection_config[:adapter]
63
+ adapter =~ /postgres/ || adapter =~ /sqlite/
64
+ end
65
+
66
+ def adapter_uses_prepared_statements?
67
+ return false unless adapter_uses_double_quoted_table_names?
68
+ return true if @connection_config[:prepared_statements].nil?
69
+ @connection_config[:prepared_statements]
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,80 @@
1
+ module Appsignal
2
+ class EventFormatter
3
+ module Moped
4
+ class QueryFormatter < Appsignal::EventFormatter
5
+ register 'query.moped'
6
+
7
+ def format(payload)
8
+ if payload[:ops] && payload[:ops].length > 0
9
+ op = payload[:ops].first
10
+ case op.class.to_s
11
+ when 'Moped::Protocol::Command'
12
+ return ['Command', {
13
+ :database => op.full_collection_name,
14
+ :selector => sanitize(op.selector)
15
+ }.inspect]
16
+ when 'Moped::Protocol::Query'
17
+ return ['Query', {
18
+ :database => op.full_collection_name,
19
+ :selector => sanitize(op.selector),
20
+ :flags => op.flags,
21
+ :limit => op.limit,
22
+ :skip => op.skip,
23
+ :fields => op.fields
24
+ }.inspect]
25
+ when 'Moped::Protocol::Delete'
26
+ return ['Delete', {
27
+ :database => op.full_collection_name,
28
+ :selector => sanitize(op.selector),
29
+ :flags => op.flags,
30
+ }.inspect]
31
+ when 'Moped::Protocol::Insert'
32
+ return ['Insert', {
33
+ :database => op.full_collection_name,
34
+ :documents => sanitize(op.documents),
35
+ :flags => op.flags,
36
+ }.inspect]
37
+ when 'Moped::Protocol::Update'
38
+ return ['Update', {
39
+ :database => op.full_collection_name,
40
+ :selector => sanitize(op.selector),
41
+ :update => sanitize(op.update),
42
+ :flags => op.flags,
43
+ }.inspect]
44
+ when 'Moped::Protocol::KillCursors'
45
+ return ['KillCursors', {
46
+ :number_of_cursor_ids => op.number_of_cursor_ids
47
+ }.inspect]
48
+ else
49
+ return [op.class.to_s.sub('Moped::Protocol::', ''), {
50
+ :database => op.full_collection_name
51
+ }.inspect]
52
+ end
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def sanitize(params)
59
+ if params.is_a?(Hash)
60
+ {}.tap do |hsh|
61
+ params.each do |key, val|
62
+ hsh[key] = sanitize(val)
63
+ end
64
+ end
65
+ elsif params.is_a?(Array)
66
+ if params.first.is_a?(String)
67
+ ['?']
68
+ else
69
+ params.map do |item|
70
+ sanitize(item)
71
+ end
72
+ end
73
+ else
74
+ '?'
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end