semantic_logger 3.2.1 → 3.3.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/semantic_logger.rb +22 -2
  4. data/lib/semantic_logger/appender/bugsnag.rb +2 -2
  5. data/lib/semantic_logger/appender/elasticsearch.rb +9 -1
  6. data/lib/semantic_logger/appender/file.rb +1 -1
  7. data/lib/semantic_logger/appender/graylog.rb +18 -21
  8. data/lib/semantic_logger/appender/honeybadger.rb +27 -12
  9. data/lib/semantic_logger/appender/http.rb +19 -11
  10. data/lib/semantic_logger/appender/mongodb.rb +23 -30
  11. data/lib/semantic_logger/appender/new_relic.rb +2 -2
  12. data/lib/semantic_logger/appender/splunk.rb +32 -21
  13. data/lib/semantic_logger/appender/splunk_http.rb +1 -3
  14. data/lib/semantic_logger/appender/syslog.rb +25 -10
  15. data/lib/semantic_logger/appender/tcp.rb +231 -0
  16. data/lib/semantic_logger/appender/udp.rb +106 -0
  17. data/lib/semantic_logger/appender/wrapper.rb +1 -1
  18. data/lib/semantic_logger/base.rb +9 -3
  19. data/lib/semantic_logger/formatters/base.rb +36 -0
  20. data/lib/semantic_logger/formatters/color.rb +13 -10
  21. data/lib/semantic_logger/formatters/default.rb +8 -5
  22. data/lib/semantic_logger/formatters/json.rb +10 -3
  23. data/lib/semantic_logger/formatters/raw.rb +13 -0
  24. data/lib/semantic_logger/formatters/syslog.rb +119 -0
  25. data/lib/semantic_logger/log.rb +7 -5
  26. data/lib/semantic_logger/loggable.rb +5 -0
  27. data/lib/semantic_logger/logger.rb +45 -10
  28. data/lib/semantic_logger/metrics/new_relic.rb +1 -1
  29. data/lib/semantic_logger/metrics/statsd.rb +5 -1
  30. data/lib/semantic_logger/metrics/udp.rb +80 -0
  31. data/lib/semantic_logger/semantic_logger.rb +23 -27
  32. data/lib/semantic_logger/subscriber.rb +127 -0
  33. data/lib/semantic_logger/version.rb +1 -1
  34. data/test/appender/elasticsearch_test.rb +6 -4
  35. data/test/appender/file_test.rb +12 -12
  36. data/test/appender/honeybadger_test.rb +7 -1
  37. data/test/appender/http_test.rb +4 -2
  38. data/test/appender/mongodb_test.rb +1 -2
  39. data/test/appender/splunk_http_test.rb +8 -6
  40. data/test/appender/splunk_test.rb +48 -45
  41. data/test/appender/syslog_test.rb +3 -3
  42. data/test/appender/tcp_test.rb +68 -0
  43. data/test/appender/udp_test.rb +61 -0
  44. data/test/appender/wrapper_test.rb +5 -5
  45. data/test/concerns/compatibility_test.rb +6 -6
  46. data/test/debug_as_trace_logger_test.rb +2 -2
  47. data/test/loggable_test.rb +2 -2
  48. data/test/logger_test.rb +48 -45
  49. metadata +13 -3
  50. data/lib/semantic_logger/appender/base.rb +0 -101
@@ -11,7 +11,7 @@ end
11
11
  #
12
12
  # Example:
13
13
  # SemanticLogger.add_appender(appender: :new_relic)
14
- class SemanticLogger::Appender::NewRelic < SemanticLogger::Appender::Base
14
+ class SemanticLogger::Appender::NewRelic < SemanticLogger::Subscriber
15
15
  # Create Appender
16
16
  #
17
17
  # Parameters
@@ -39,7 +39,7 @@ class SemanticLogger::Appender::NewRelic < SemanticLogger::Appender::Base
39
39
 
40
40
  # Returns [Hash] of parameters to send to New Relic.
41
41
  def call(log, logger)
42
- h = log.to_h
42
+ h = log.to_h(host, application)
43
43
  h.delete(:time)
44
44
  h.delete(:exception)
45
45
  {metric: log.metric, custom_params: h}
@@ -18,8 +18,8 @@ end
18
18
  # scheme: :https,
19
19
  # index: 'main'
20
20
  # )
21
- class SemanticLogger::Appender::Splunk < SemanticLogger::Appender::Base
22
- attr_reader :config, :index, :service, :service_index
21
+ class SemanticLogger::Appender::Splunk < SemanticLogger::Subscriber
22
+ attr_reader :config, :index, :service, :service_index, :source_type
23
23
 
24
24
  # Write to Splunk.
25
25
  #
@@ -37,7 +37,7 @@ class SemanticLogger::Appender::Splunk < SemanticLogger::Appender::Base
37
37
  # Not required if username and password are supplied.
38
38
  #
39
39
  # :host [String]
40
- # Splunk host name.
40
+ # Splunk server host name.
41
41
  # Default: 'localhost'
42
42
  #
43
43
  # :port [Integer]
@@ -61,6 +61,17 @@ class SemanticLogger::Appender::Splunk < SemanticLogger::Appender::Base
61
61
  # :ssl_client_key [OpenSSL::PKey::RSA | OpenSSL::PKey::DSA]
62
62
  # Client key.
63
63
  #
64
+ # source_type: [String]
65
+ # Optional: Source type to display in Splunk
66
+ #
67
+ # application: [String]
68
+ # The :source forwarded to Splunk
69
+ # Default: SemanticLogger.application
70
+ #
71
+ # host: [String]
72
+ # Name of this host to appear in log messages.
73
+ # Default: SemanticLogger.host
74
+ #
64
75
  # level: [:trace | :debug | :info | :warn | :error | :fatal]
65
76
  # Override the log level for this appender.
66
77
  # Default: SemanticLogger.default_level
@@ -75,21 +86,16 @@ class SemanticLogger::Appender::Splunk < SemanticLogger::Appender::Base
75
86
  # regular expression. All other messages will be ignored.
76
87
  # Proc: Only include log messages where the supplied Proc returns true
77
88
  # The Proc must return true or false.
78
- def initialize(options, _deprecated_level = nil, &block)
89
+ def initialize(options = {}, _deprecated_level = nil, &block)
79
90
  @config = options.dup
80
91
  @config[:level] = _deprecated_level if _deprecated_level
81
92
  @index = @config.delete(:index) || 'main'
93
+ @source_type = options.delete(:source_type)
82
94
 
83
- options = {
84
- level: @config.delete(:level) || :error,
85
- formatter: @config.delete(:formatter),
86
- filter: @config.delete(:filter)
87
- }
88
-
89
- reopen
90
-
95
+ options = extract_subscriber_options!(@config)
91
96
  # Pass on the level and custom formatter if supplied
92
97
  super(options, &block)
98
+ reopen
93
99
  end
94
100
 
95
101
  # After forking an active process call #reopen to re-open
@@ -105,20 +111,25 @@ class SemanticLogger::Appender::Splunk < SemanticLogger::Appender::Base
105
111
  # Log the message to Splunk
106
112
  def log(log)
107
113
  return false unless should_log?(log)
108
-
109
- service_index.submit(log.message, formatter.call(log, self))
114
+ event = formatter.call(log, self)
115
+ service_index.submit(event.delete(:message), event)
110
116
  true
111
117
  end
112
118
 
113
- # Returns [String] JSON to send to Splunk
119
+ # Returns [Hash] To send to Splunk
114
120
  # For splunk format requirements see:
115
121
  # http://dev.splunk.com/view/event-collector/SP-CAAAE6P
116
- def call(log, _logger)
117
- h = log.to_h
118
- h.delete(:message)
119
- h.delete(:application)
120
- h.delete(:host)
122
+ def call(log, logger)
123
+ h = log.to_h(nil, nil)
121
124
  h.delete(:time)
122
- h
125
+ message = {
126
+ source: logger.application,
127
+ host: logger.host,
128
+ time: log.time.utc.to_f,
129
+ message: h.delete(:message),
130
+ event: h
131
+ }
132
+ message[:source_type] = source_type if source_type
133
+ message
123
134
  end
124
135
  end
@@ -83,9 +83,7 @@ class SemanticLogger::Appender::SplunkHttp < SemanticLogger::Appender::Http
83
83
  # For splunk format requirements see:
84
84
  # http://dev.splunk.com/view/event-collector/SP-CAAAE6P
85
85
  def call(log, logger)
86
- h = log.to_h
87
- h.delete(:application)
88
- h.delete(:host)
86
+ h = log.to_h(nil, nil)
89
87
  h.delete(:time)
90
88
  message = {
91
89
  source: logger.application,
@@ -20,9 +20,7 @@ require 'socket'
20
20
  # )
21
21
  module SemanticLogger
22
22
  module Appender
23
- class Syslog < SemanticLogger::Appender::Base
24
-
25
- attr_reader :remote_syslog, :url, :server, :port, :protocol, :facility, :host, :application
23
+ class Syslog < SemanticLogger::Subscriber
26
24
 
27
25
  # Default mapping of ruby log levels to syslog log levels
28
26
  #
@@ -42,6 +40,7 @@ module SemanticLogger
42
40
  debug: ::Syslog::LOG_INFO,
43
41
  trace: ::Syslog::LOG_DEBUG
44
42
  }
43
+ attr_reader :remote_syslog, :url, :server, :port, :protocol, :facility
45
44
 
46
45
  # Create a Syslog appender instance.
47
46
  #
@@ -149,7 +148,6 @@ module SemanticLogger
149
148
  # Default: :syslog
150
149
  def initialize(options = {}, &block)
151
150
  options = options.dup
152
- @application = options.delete(:application) || options.delete(:ident) || 'ruby'
153
151
  @options = options.delete(:options) || (::Syslog::LOG_PID | ::Syslog::LOG_CONS)
154
152
  @facility = options.delete(:facility) || ::Syslog::LOG_USER
155
153
  level_map = options.delete(:level_map)
@@ -159,7 +157,6 @@ module SemanticLogger
159
157
  @protocol = (uri.scheme || :syslog).to_sym
160
158
  @port = uri.port || 514
161
159
  @server = 'localhost' if @protocol == :syslog
162
- @host = options.delete(:host) || options.delete(:local_hostname) || SemanticLogger.host
163
160
  @tcp_client_options = options.delete(:tcp_client)
164
161
 
165
162
  raise "Unknown protocol #{@protocol}!" unless [:syslog, :tcp, :udp].include?(@protocol)
@@ -187,9 +184,8 @@ module SemanticLogger
187
184
  end
188
185
  end
189
186
 
190
- reopen
191
-
192
187
  super(options, &block)
188
+ reopen
193
189
  end
194
190
 
195
191
  # After forking an active process call #reopen to re-open
@@ -197,7 +193,7 @@ module SemanticLogger
197
193
  def reopen
198
194
  case @protocol
199
195
  when :syslog
200
- ::Syslog.open(@application, @options, @facility)
196
+ ::Syslog.open(application, @options, @facility)
201
197
  when :tcp
202
198
  # Use the local logger for @remote_syslog so errors with the remote logger can be recorded locally.
203
199
  @tcp_client_options[:logger] = SemanticLogger::Logger.logger
@@ -264,13 +260,32 @@ module SemanticLogger
264
260
  message
265
261
  end
266
262
 
263
+ private
264
+
265
+ # Extract Syslog formatter options
266
+ def format_options(options, protocol, &block)
267
+ opts = options.delete(:options)
268
+ facility = options.delete(:facility)
269
+ level_map = options.delete(:level_map)
270
+ if formatter = options.delete(:formatter)
271
+ extract_formatter(formatter)
272
+ else
273
+ case protocol
274
+ when :syslog
275
+ extract_formatter(syslog: {options: opts, facility: facility, level_map: level_map})
276
+ when :tcp, :udp
277
+ extract_formatter(syslog: {options: opts, facility: facility, level_map: level_map})
278
+ end
279
+ end
280
+ end
281
+
267
282
  # Format the syslog packet so it can be sent over TCP or UDP
268
283
  def syslog_packet_formatter(log)
269
284
  packet = SyslogProtocol::Packet.new
270
- packet.hostname = @host
285
+ packet.hostname = host
271
286
  packet.facility = @facility
272
287
  packet.severity = @level_map[log.level]
273
- packet.tag = @application
288
+ packet.tag = application.gsub(' ', '')
274
289
  packet.content = formatter.call(log, self)
275
290
  packet.time = log.time
276
291
  packet.to_s
@@ -0,0 +1,231 @@
1
+ begin
2
+ require 'net/tcp_client'
3
+ rescue LoadError
4
+ raise 'Gem net_tcp_client is required for logging over TCP. Please add the gem "net_tcp_client" to your Gemfile.'
5
+ end
6
+
7
+ raise 'Net::TCPClient v2.0 or greater is required to log over TCP' unless Net::TCPClient::VERSION.to_f >= 2.0
8
+
9
+ module SemanticLogger
10
+ module Appender
11
+ # TCP log appender.
12
+ #
13
+ # Log to a server over a TCP Socket.
14
+ # By default messages are in JSON format.
15
+ #
16
+ # Features:
17
+ # * JSON Formatted messages.
18
+ # * SSL encryption.
19
+ # * Transparently reconnect when a connection is lost.
20
+ #
21
+ # Example:
22
+ # SemanticLogger.add_appender(
23
+ # appender: :tcp,
24
+ # server: 'server:3300',
25
+ # )
26
+ #
27
+ # Example, with connection retry options:
28
+ # SemanticLogger.add_appender(
29
+ # appender: :tcp,
30
+ # server: 'server:3300',
31
+ # connect_retry_interval: 0.1,
32
+ # connect_retry_count: 5
33
+ # )
34
+ #
35
+ # Example, with SSL enabled:
36
+ # SemanticLogger.add_appender(
37
+ # appender: :tcp,
38
+ # server: 'server:3300',
39
+ # ssl: true
40
+ # )
41
+ #
42
+ class Tcp < SemanticLogger::Subscriber
43
+ attr_accessor :separator
44
+ attr_reader :tcp_client
45
+
46
+ # Create TCP log appender.
47
+ #
48
+ # Net::TCPClient Parameters:
49
+ # :server [String]
50
+ # URL of the server to connect to with port number
51
+ # 'localhost:2000'
52
+ # '192.168.1.10:80'
53
+ #
54
+ # :servers [Array of String]
55
+ # Array of URL's of servers to connect to with port numbers
56
+ # ['server1:2000', 'server2:2000']
57
+ #
58
+ # The second server will only be attempted once the first server
59
+ # cannot be connected to or has timed out on connect
60
+ # A read failure or timeout will not result in switching to the second
61
+ # server, only a connection failure or during an automatic reconnect
62
+ #
63
+ # :connect_timeout [Float]
64
+ # Time in seconds to timeout when trying to connect to the server
65
+ # A value of -1 will cause the connect wait time to be infinite
66
+ # Default: Half of the :read_timeout ( 30 seconds )
67
+ #
68
+ # :read_timeout [Float]
69
+ # Time in seconds to timeout on read
70
+ # Can be overridden by supplying a timeout in the read call
71
+ # Default: 60
72
+ #
73
+ # :write_timeout [Float]
74
+ # Time in seconds to timeout on write
75
+ # Can be overridden by supplying a timeout in the write call
76
+ # Default: 60
77
+ #
78
+ # :log_level [Symbol]
79
+ # Optional: Set the logging level for the TCPClient
80
+ # Any valid SemanticLogger log level:
81
+ # :trace, :debug, :info, :warn, :error, :fatal
82
+ # Default: SemanticLogger.default_level
83
+ #
84
+ # :buffered [Boolean]
85
+ # Whether to use Nagle's Buffering algorithm (http://en.wikipedia.org/wiki/Nagle's_algorithm)
86
+ # Recommend disabling for RPC style invocations where we don't want to wait for an
87
+ # ACK from the server before sending the last partial segment
88
+ # Buffering is recommended in a browser or file transfer style environment
89
+ # where multiple sends are expected during a single response
90
+ # Default: true
91
+ #
92
+ # :connect_retry_count [Fixnum]
93
+ # Number of times to retry connecting when a connection fails
94
+ # Default: 10
95
+ #
96
+ # :connect_retry_interval [Float]
97
+ # Number of seconds between connection retry attempts after the first failed attempt
98
+ # Default: 0.5
99
+ #
100
+ # :retry_count [Fixnum]
101
+ # Number of times to retry when calling #retry_on_connection_failure
102
+ # This is independent of :connect_retry_count which still applies with
103
+ # connection failures. This retry controls upto how many times to retry the
104
+ # supplied block should a connection failure occurr during the block
105
+ # Default: 3
106
+ #
107
+ # :on_connect [Proc]
108
+ # Directly after a connection is established and before it is made available
109
+ # for use this Block is invoked.
110
+ # Typical Use Cases:
111
+ # - Initialize per connection session sequence numbers
112
+ # - Pass any authentication information to the server
113
+ # - Perform a handshake with the server
114
+ #
115
+ # :policy [Symbol|Proc]
116
+ # Specify the policy to use when connecting to servers.
117
+ # :ordered
118
+ # Select a server in the order supplied in the array, with the first
119
+ # having the highest priority. The second server will only be connected
120
+ # to if the first server is unreachable
121
+ # :random
122
+ # Randomly select a server from the list every time a connection
123
+ # is established, including during automatic connection recovery.
124
+ # :ping_time
125
+ # FUTURE - Not implemented yet - Pull request anyone?
126
+ # The server with the lowest ping time will be tried first
127
+ # Proc:
128
+ # When a Proc is supplied, it will be called passing in the list
129
+ # of servers. The Proc must return one server name
130
+ # Example:
131
+ # :policy => Proc.new do |servers|
132
+ # servers.last
133
+ # end
134
+ # Default: :ordered
135
+ #
136
+ # :close_on_error [True|False]
137
+ # To prevent the connection from going into an inconsistent state
138
+ # automatically close the connection if an error occurs
139
+ # This includes a Read Timeout
140
+ # Default: true
141
+ #
142
+ # Appender Parameters:
143
+ # separator: [String]
144
+ # Separator between every message
145
+ # Default: "\n"
146
+ # Note: The separator should not be something that could be output in the formatted log message.
147
+ #
148
+ # Common Appender Parameters:
149
+ # application: [String]
150
+ # Name of this application to appear in log messages.
151
+ # Default: SemanticLogger.application
152
+ #
153
+ # host: [String]
154
+ # Name of this host to appear in log messages.
155
+ # Default: SemanticLogger.host
156
+ #
157
+ # level: [:trace | :debug | :info | :warn | :error | :fatal]
158
+ # Override the log level for this appender.
159
+ # Default: SemanticLogger.default_level
160
+ #
161
+ # formatter: [Object|Proc]
162
+ # An instance of a class that implements #call, or a Proc to be used to format
163
+ # the output from this appender
164
+ # Default: Use the built-in formatter (See: #call)
165
+ #
166
+ # filter: [Regexp|Proc]
167
+ # RegExp: Only include log messages where the class name matches the supplied.
168
+ # regular expression. All other messages will be ignored.
169
+ # Proc: Only include log messages where the supplied Proc returns true
170
+ # The Proc must return true or false.
171
+ # Example:
172
+ # SemanticLogger.add_appender(
173
+ # appender: :tcp,
174
+ # server: 'server:3300'
175
+ # )
176
+ #
177
+ # Example, with connection retry options:
178
+ # SemanticLogger.add_appender(
179
+ # appender: :tcp,
180
+ # server: 'server:3300',
181
+ # connect_retry_interval: 0.1,
182
+ # connect_retry_count: 5
183
+ # )
184
+ def initialize(options = {}, &block)
185
+ @options = options.dup
186
+ @separator = @options.delete(:separator) || "\n"
187
+
188
+ # Use the internal logger so that errors with remote logging are only written locally.
189
+ Net::TCPClient.logger = SemanticLogger::Logger.logger.dup
190
+ Net::TCPClient.logger.name = 'Net::TCPClient'
191
+
192
+ options = extract_subscriber_options!(@options)
193
+ super(options, &block)
194
+ reopen
195
+ end
196
+
197
+ # After forking an active process call #reopen to re-open
198
+ # open the handles to resources
199
+ def reopen
200
+ close
201
+ @tcp_client = Net::TCPClient.new(@options)
202
+ end
203
+
204
+ # Write the log using the specified protocol and server.
205
+ def log(log)
206
+ return false unless should_log?(log)
207
+
208
+ @tcp_client.retry_on_connection_failure { @tcp_client.write("#{formatter.call(log, self)}#{separator}") }
209
+ true
210
+ end
211
+
212
+ # Flush is called by the semantic_logger during shutdown.
213
+ def flush
214
+ @tcp_client.flush if @tcp_client && @tcp_client.respond_to?(:flush)
215
+ end
216
+
217
+ # Close is called during shutdown, or with reopen
218
+ def close
219
+ @tcp_client.close if @tcp_client
220
+ end
221
+
222
+ private
223
+
224
+ # Returns [SemanticLogger::Formatters::Default] formatter default for this Appender
225
+ def default_formatter
226
+ SemanticLogger::Formatters::Json.new
227
+ end
228
+
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,106 @@
1
+ require 'socket'
2
+ module SemanticLogger
3
+ module Appender
4
+ # UDP log appender.
5
+ #
6
+ # Write log messages to UDP.
7
+ # By default messages are in JSON format.
8
+ #
9
+ # Example:
10
+ # SemanticLogger.add_appender(
11
+ # appender: :udp,
12
+ # server: 'server:3300',
13
+ # )
14
+ class Udp < SemanticLogger::Subscriber
15
+ attr_accessor :server, :udp_flags
16
+ attr_reader :socket
17
+
18
+ # Create UDP log appender.
19
+ #
20
+ # server: [String]
21
+ # URL of the server to write UDP messages to.
22
+ #
23
+ # udp_flags: [Integer]
24
+ # Should be a bitwise OR of Socket::MSG_* constants.
25
+ # Default: 0
26
+ #
27
+ # Common Appender Parameters:
28
+ # application: [String]
29
+ # Name of this application to appear in log messages.
30
+ # Default: SemanticLogger.application
31
+ #
32
+ # host: [String]
33
+ # Name of this host to appear in log messages.
34
+ # Default: SemanticLogger.host
35
+ #
36
+ # level: [:trace | :debug | :info | :warn | :error | :fatal]
37
+ # Override the log level for this appender.
38
+ # Default: SemanticLogger.default_level
39
+ #
40
+ # formatter: [Object|Proc]
41
+ # An instance of a class that implements #call, or a Proc to be used to format
42
+ # the output from this appender
43
+ # Default: Use the built-in formatter (See: #call)
44
+ #
45
+ # filter: [Regexp|Proc]
46
+ # RegExp: Only include log messages where the class name matches the supplied.
47
+ # regular expression. All other messages will be ignored.
48
+ # Proc: Only include log messages where the supplied Proc returns true
49
+ # The Proc must return true or false.
50
+ #
51
+ # Limitations:
52
+ # * UDP packet size is limited by the connected network and any routers etc
53
+ # that the message has to traverse. See https://en.wikipedia.org/wiki/Maximum_transmission_unit
54
+ #
55
+ # Example:
56
+ # SemanticLogger.add_appender(
57
+ # appender: :udp,
58
+ # server: 'server:3300'
59
+ # )
60
+ def initialize(options = {}, &block)
61
+ options = options.dup
62
+ @server = options.delete(:server)
63
+ @udp_flags = options.delete(:udp_flags) || 0
64
+ raise(ArgumentError, 'Missing mandatory argument: :server') unless @server
65
+
66
+ super(options, &block)
67
+ reopen
68
+ end
69
+
70
+ # After forking an active process call #reopen to re-open
71
+ # open the handles to resources
72
+ def reopen
73
+ close
74
+ @socket = UDPSocket.new
75
+ host, port = server.split(':')
76
+ @socket.connect(host, port.to_i)
77
+ end
78
+
79
+ # Write the log using the specified protocol and server.
80
+ def log(log)
81
+ return false unless should_log?(log)
82
+
83
+ @socket.send(formatter.call(log, self), udp_flags)
84
+ true
85
+ end
86
+
87
+ # Flush is called by the semantic_logger during shutdown.
88
+ def flush
89
+ @socket.flush if @socket
90
+ end
91
+
92
+ # Close is called during shutdown, or with reopen
93
+ def close
94
+ @socket.close if @socket
95
+ end
96
+
97
+ private
98
+
99
+ # Returns [SemanticLogger::Formatters::Default] formatter default for this Appender
100
+ def default_formatter
101
+ SemanticLogger::Formatters::Json.new
102
+ end
103
+
104
+ end
105
+ end
106
+ end