appsignal 0.11.18 → 0.12.beta.0

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 (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