timber 1.1.14 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -2
- data/.travis.yml +47 -0
- data/Gemfile +1 -28
- data/README.md +83 -298
- data/bin/timber +13 -0
- data/gemfiles/rails-3.0.gemfile +5 -0
- data/gemfiles/rails-3.1.gemfile +5 -0
- data/gemfiles/rails-3.2.gemfile +5 -0
- data/gemfiles/rails-4.0.gemfile +9 -0
- data/gemfiles/rails-4.1.gemfile +9 -0
- data/gemfiles/rails-4.2.gemfile +9 -0
- data/gemfiles/rails-5.0.gemfile +9 -0
- data/gemfiles/rails-edge.gemfile +7 -0
- data/lib/timber.rb +7 -7
- data/lib/timber/cli.rb +72 -0
- data/lib/timber/cli/api.rb +104 -0
- data/lib/timber/cli/application.rb +28 -0
- data/lib/timber/cli/install.rb +186 -0
- data/lib/timber/cli/io_helper.rb +58 -0
- data/lib/timber/cli/messages.rb +170 -0
- data/lib/timber/config.rb +47 -6
- data/lib/timber/contexts/http.rb +2 -2
- data/lib/timber/current_context.rb +1 -1
- data/lib/timber/event.rb +8 -0
- data/lib/timber/events.rb +2 -0
- data/lib/timber/events/controller_call.rb +12 -3
- data/lib/timber/events/exception.rb +4 -3
- data/lib/timber/events/http_client_request.rb +61 -0
- data/lib/timber/events/http_client_response.rb +47 -0
- data/lib/timber/events/http_server_request.rb +15 -23
- data/lib/timber/events/http_server_response.rb +9 -9
- data/lib/timber/events/sql_query.rb +2 -2
- data/lib/timber/events/template_render.rb +2 -2
- data/lib/timber/frameworks/rails.rb +31 -6
- data/lib/timber/integrations.rb +22 -0
- data/lib/timber/integrations/action_controller/log_subscriber.rb +25 -0
- data/lib/timber/integrations/action_controller/log_subscriber/timber_log_subscriber.rb +40 -0
- data/lib/timber/integrations/action_dispatch/debug_exceptions.rb +51 -0
- data/lib/timber/integrations/action_view/log_subscriber.rb +25 -0
- data/lib/timber/integrations/action_view/log_subscriber/timber_log_subscriber.rb +73 -0
- data/lib/timber/integrations/active_record/log_subscriber.rb +25 -0
- data/lib/timber/integrations/active_record/log_subscriber/timber_log_subscriber.rb +39 -0
- data/lib/timber/integrations/active_support/tagged_logging.rb +71 -0
- data/lib/timber/integrations/rack.rb +16 -0
- data/lib/timber/integrations/rack/exception_event.rb +28 -0
- data/lib/timber/integrations/rack/http_context.rb +25 -0
- data/lib/timber/integrations/rack/http_events.rb +46 -0
- data/lib/timber/integrations/rack/user_context.rb +59 -0
- data/lib/timber/integrations/rails/rack_logger.rb +49 -0
- data/lib/timber/integrator.rb +24 -0
- data/lib/timber/log_devices/http.rb +14 -21
- data/lib/timber/log_entry.rb +1 -1
- data/lib/timber/logger.rb +38 -12
- data/lib/timber/overrides.rb +9 -0
- data/lib/timber/overrides/lograge.rb +14 -0
- data/lib/timber/overrides/rails_server.rb +10 -0
- data/lib/timber/util.rb +2 -0
- data/lib/timber/util/active_support_log_subscriber.rb +13 -9
- data/lib/timber/util/http_event.rb +54 -0
- data/lib/timber/util/request.rb +44 -0
- data/lib/timber/version.rb +1 -1
- data/spec/README.md +5 -9
- data/spec/spec_helper.rb +1 -4
- data/spec/support/action_controller.rb +7 -3
- data/spec/support/active_record.rb +23 -19
- data/spec/support/rails.rb +56 -32
- data/spec/support/timber.rb +2 -3
- data/spec/support/webmock.rb +1 -0
- data/spec/timber/integrations/action_controller/log_subscriber_spec.rb +55 -0
- data/spec/timber/integrations/action_dispatch/debug_exceptions_spec.rb +53 -0
- data/spec/timber/integrations/action_view/log_subscriber_spec.rb +115 -0
- data/spec/timber/integrations/active_record/log_subscriber_spec.rb +46 -0
- data/spec/timber/integrations/rack/http_context_spec.rb +60 -0
- data/spec/timber/integrations/rails/rack_logger_spec.rb +58 -0
- data/spec/timber/logger_spec.rb +45 -9
- data/timber.gemspec +29 -3
- metadata +143 -46
- data/Appraisals +0 -41
- data/circle.yml +0 -33
- data/lib/timber/overrides/logger_add.rb +0 -38
- data/lib/timber/probe.rb +0 -23
- data/lib/timber/probes.rb +0 -23
- data/lib/timber/probes/action_controller_log_subscriber.rb +0 -20
- data/lib/timber/probes/action_controller_log_subscriber/log_subscriber.rb +0 -64
- data/lib/timber/probes/action_controller_user_context.rb +0 -52
- data/lib/timber/probes/action_dispatch_debug_exceptions.rb +0 -80
- data/lib/timber/probes/action_view_log_subscriber.rb +0 -20
- data/lib/timber/probes/action_view_log_subscriber/log_subscriber.rb +0 -69
- data/lib/timber/probes/active_record_log_subscriber.rb +0 -20
- data/lib/timber/probes/active_record_log_subscriber/log_subscriber.rb +0 -31
- data/lib/timber/probes/active_support_tagged_logging.rb +0 -63
- data/lib/timber/probes/rails_rack_logger.rb +0 -77
- data/lib/timber/rack_middlewares.rb +0 -12
- data/lib/timber/rack_middlewares/http_context.rb +0 -30
- data/spec/support/action_view.rb +0 -4
- data/spec/support/coveralls.rb +0 -2
- data/spec/support/simplecov.rb +0 -9
- data/spec/timber/overrides/logger_add_spec.rb +0 -26
- data/spec/timber/probes/action_controller_log_subscriber_spec.rb +0 -65
- data/spec/timber/probes/action_controller_user_context_spec.rb +0 -53
- data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +0 -48
- data/spec/timber/probes/action_view_log_subscriber_spec.rb +0 -107
- data/spec/timber/probes/active_record_log_subscriber_spec.rb +0 -47
- data/spec/timber/probes/rails_rack_logger_spec.rb +0 -46
- data/spec/timber/rack_middlewares/http_context_spec.rb +0 -47
data/bin/timber
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
|
4
|
+
require "timber/cli"
|
5
|
+
|
6
|
+
begin
|
7
|
+
Timber::CLI.run
|
8
|
+
rescue => e
|
9
|
+
raise e if $DEBUG
|
10
|
+
STDERR.puts e.message
|
11
|
+
STDERR.puts e.backtrace.join("\n")
|
12
|
+
exit 1
|
13
|
+
end
|
data/lib/timber.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
# core classes
|
2
2
|
require "json" # brings to_json to the core classes
|
3
3
|
|
4
|
-
require "timber/overrides/logger_add"
|
5
|
-
require "timber/overrides/rails_stdout_logging"
|
6
|
-
|
7
4
|
# Base (must come first, order matters)
|
5
|
+
require "timber/overrides"
|
8
6
|
require "timber/config"
|
9
7
|
require "timber/context"
|
10
8
|
require "timber/event"
|
11
|
-
require "timber/
|
9
|
+
require "timber/integrator"
|
12
10
|
require "timber/util"
|
13
11
|
require "timber/version"
|
14
12
|
|
@@ -19,8 +17,10 @@ require "timber/events"
|
|
19
17
|
require "timber/log_devices"
|
20
18
|
require "timber/log_entry"
|
21
19
|
require "timber/logger"
|
22
|
-
require "timber/
|
23
|
-
require "timber/rack_middlewares"
|
20
|
+
require "timber/integrations"
|
24
21
|
|
25
22
|
# Load frameworks
|
26
|
-
require "timber/frameworks"
|
23
|
+
require "timber/frameworks"
|
24
|
+
|
25
|
+
module Timber
|
26
|
+
end
|
data/lib/timber/cli.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require "optparse"
|
2
|
+
require "yaml"
|
3
|
+
require "timber"
|
4
|
+
|
5
|
+
|
6
|
+
require "timber/cli/api"
|
7
|
+
require "timber/cli/application"
|
8
|
+
require "timber/cli/io_helper"
|
9
|
+
require "timber/cli/messages"
|
10
|
+
|
11
|
+
require "timber/cli/install"
|
12
|
+
|
13
|
+
module Timber
|
14
|
+
# @private
|
15
|
+
class CLI
|
16
|
+
AVAILABLE_COMMANDS = %w(install).freeze
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :options
|
20
|
+
|
21
|
+
def run(argv = ARGV)
|
22
|
+
@options = {}
|
23
|
+
global = global_option_parser
|
24
|
+
commands = command_option_parser
|
25
|
+
global.order!(argv)
|
26
|
+
command = argv.shift
|
27
|
+
if command
|
28
|
+
if AVAILABLE_COMMANDS.include?(command)
|
29
|
+
commands[command].parse!(argv)
|
30
|
+
case command.to_sym
|
31
|
+
when :install
|
32
|
+
Timber::CLI::Install.run(argv.shift)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
puts "Command '#{command}' does not exist, run timber -h to "\
|
36
|
+
"see the help"
|
37
|
+
exit(1)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
# Print help
|
41
|
+
puts global
|
42
|
+
exit(0)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def global_option_parser
|
47
|
+
OptionParser.new do |o|
|
48
|
+
o.banner = "Usage: timber <command> [options]"
|
49
|
+
|
50
|
+
o.on "-v", "--version", "Print version and exit" do |_arg|
|
51
|
+
puts "Timber #{Timber::VERSION}"
|
52
|
+
exit(0)
|
53
|
+
end
|
54
|
+
|
55
|
+
o.on "-h", "--help", "Show help and exit" do
|
56
|
+
puts o
|
57
|
+
exit(0)
|
58
|
+
end
|
59
|
+
|
60
|
+
o.separator ""
|
61
|
+
o.separator "Available commands: #{AVAILABLE_COMMANDS.join(", ")}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def command_option_parser
|
66
|
+
{
|
67
|
+
"install" => OptionParser.new
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "json"
|
3
|
+
require "net/https"
|
4
|
+
require "securerandom"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Timber
|
8
|
+
class CLI
|
9
|
+
class API
|
10
|
+
class APIKeyInvalidError < StandardError
|
11
|
+
def message
|
12
|
+
"Uh oh! The API key supplied is invalid. Please ensure that you copied the" \
|
13
|
+
" key properly.\n\n#{Messages.obtain_key_instructions}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class NoAPIKeyError < StandardError
|
18
|
+
def message
|
19
|
+
"Uh oh! You didn't supply an API key.\n\n#{Messages.obtain_key_instructions}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
TIMBER_API_URI = URI.parse('https://api.timber.io')
|
24
|
+
APPLICATION_PATH = "/installer/application".freeze
|
25
|
+
EVENT_PATH = "/installer/events".freeze
|
26
|
+
HAS_LOGS_PATH = "/installer/has_logs".freeze
|
27
|
+
USER_AGENT = "Timber Ruby/#{Timber::VERSION} (HTTP)".freeze
|
28
|
+
|
29
|
+
def initialize(api_key)
|
30
|
+
@api_key = api_key
|
31
|
+
@session_id = SecureRandom.uuid
|
32
|
+
end
|
33
|
+
|
34
|
+
def application!
|
35
|
+
get!(APPLICATION_PATH)
|
36
|
+
end
|
37
|
+
|
38
|
+
def event!(name, data = {})
|
39
|
+
post!(EVENT_PATH, event: {name: name, data: data})
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_logs(iteration = 0, &block)
|
43
|
+
if block_given?
|
44
|
+
yield iteration
|
45
|
+
end
|
46
|
+
|
47
|
+
sleep 0.5
|
48
|
+
|
49
|
+
res = get!(HAS_LOGS_PATH)
|
50
|
+
|
51
|
+
case res.code
|
52
|
+
when "202"
|
53
|
+
wait_for_logs(iteration + 1, &block)
|
54
|
+
|
55
|
+
when "204"
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def get!(path)
|
62
|
+
req = Net::HTTP::Get.new(path)
|
63
|
+
issue!(req)
|
64
|
+
end
|
65
|
+
|
66
|
+
def post!(path, body)
|
67
|
+
req = Net::HTTP::Post.new(path)
|
68
|
+
req.body = body.to_json
|
69
|
+
req['Content-Type'] = "application/json"
|
70
|
+
issue!(req)
|
71
|
+
end
|
72
|
+
|
73
|
+
def issue!(req)
|
74
|
+
req['Authorization'] = "Basic #{encoded_api_key}"
|
75
|
+
req['User-Agent'] = USER_AGENT
|
76
|
+
req['X-Installer-Session-Id'] = @session_id
|
77
|
+
res = http.start do |http|
|
78
|
+
http.request(req)
|
79
|
+
end
|
80
|
+
|
81
|
+
if res.code == "401"
|
82
|
+
raise NoAPIKeyError.new
|
83
|
+
elsif res.code == "403"
|
84
|
+
raise APIKeyInvalidError.new
|
85
|
+
else
|
86
|
+
res
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def http
|
91
|
+
@http ||= begin
|
92
|
+
http = Net::HTTP.new(TIMBER_API_URI.host, TIMBER_API_URI.port)
|
93
|
+
http.use_ssl = true
|
94
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
95
|
+
http
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def encoded_api_key
|
100
|
+
Base64.urlsafe_encode64(@api_key).chomp
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Timber
|
4
|
+
class CLI
|
5
|
+
class Application
|
6
|
+
|
7
|
+
attr_reader :api_key, :environment, :framework_type, :heroku_drain_url, :language_type,
|
8
|
+
:name, :platform_type
|
9
|
+
|
10
|
+
def initialize(api)
|
11
|
+
res = api.application!
|
12
|
+
parsed_body = JSON.parse(res.body)
|
13
|
+
application_data = parsed_body.fetch("data")
|
14
|
+
@api_key = application_data.fetch("api_key")
|
15
|
+
@environment = application_data.fetch("environment")
|
16
|
+
@framework_type = application_data.fetch("framework_type")
|
17
|
+
@heroku_drain_url = application_data.fetch("heroku_drain_url")
|
18
|
+
@language_type = application_data.fetch("language_type")
|
19
|
+
@name = application_data.fetch("name")
|
20
|
+
@platform_type = application_data.fetch("platform_type")
|
21
|
+
end
|
22
|
+
|
23
|
+
def heroku?
|
24
|
+
platform_type == "heroku"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module Timber
|
4
|
+
class CLI
|
5
|
+
class Install
|
6
|
+
EXCLUDED_ENVIRONMENTS = ["test"].freeze
|
7
|
+
|
8
|
+
class << self
|
9
|
+
include IOHelper
|
10
|
+
|
11
|
+
def run(api_key)
|
12
|
+
puts ""
|
13
|
+
puts colorize(Messages.header, :green)
|
14
|
+
puts colorize(Messages.separator, :green)
|
15
|
+
puts colorize(Messages.contact, :green)
|
16
|
+
puts colorize(Messages.separator, :green)
|
17
|
+
puts ""
|
18
|
+
|
19
|
+
if !api_key
|
20
|
+
puts colorize(Messages.no_api_key_provided, :red)
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
api = API.new(api_key)
|
25
|
+
|
26
|
+
api.event!(:started)
|
27
|
+
|
28
|
+
app = Application.new(api)
|
29
|
+
|
30
|
+
puts Messages.application_details(app)
|
31
|
+
|
32
|
+
case ask_yes_no("Are the above details correct?")
|
33
|
+
when :yes
|
34
|
+
if app.heroku?
|
35
|
+
create_initializer(:stdout)
|
36
|
+
|
37
|
+
puts ""
|
38
|
+
puts Messages.separator
|
39
|
+
puts ""
|
40
|
+
puts Messages.heroku_install(app)
|
41
|
+
puts ""
|
42
|
+
|
43
|
+
else
|
44
|
+
puts ""
|
45
|
+
puts Messages.separator
|
46
|
+
puts ""
|
47
|
+
puts "How would you like configure Timber?"
|
48
|
+
puts ""
|
49
|
+
puts "1) Using environment variables"
|
50
|
+
puts "2) Configuring in my app"
|
51
|
+
puts ""
|
52
|
+
|
53
|
+
case ask("Enter your choice: (1/2) ")
|
54
|
+
when "1"
|
55
|
+
create_initializer(:http, :api_key_code => "ENV['TIMBER_API_KEY']")
|
56
|
+
|
57
|
+
puts ""
|
58
|
+
puts Messages.http_environment_variables(app.api_key)
|
59
|
+
puts ""
|
60
|
+
|
61
|
+
when "2"
|
62
|
+
create_initializer(:http, :api_key_code => "'#{app.api_key}'")
|
63
|
+
|
64
|
+
puts ""
|
65
|
+
end
|
66
|
+
|
67
|
+
send_test_messages(api_key)
|
68
|
+
end
|
69
|
+
|
70
|
+
api.wait_for_logs do |iteration|
|
71
|
+
write Messages.task_start("Waiting for logs")
|
72
|
+
write Messages.spinner(iteration)
|
73
|
+
end
|
74
|
+
|
75
|
+
puts colorize(Messages.task_complete("Waiting for logs"), :green)
|
76
|
+
|
77
|
+
puts ""
|
78
|
+
puts Messages.separator
|
79
|
+
puts ""
|
80
|
+
puts Messages.free_data
|
81
|
+
puts ""
|
82
|
+
puts Messages.separator
|
83
|
+
puts ""
|
84
|
+
puts colorize(Messages.commit_and_deploy_reminder, :green)
|
85
|
+
|
86
|
+
api.event!(:success)
|
87
|
+
|
88
|
+
collect_feedback(api)
|
89
|
+
|
90
|
+
when :no
|
91
|
+
puts ""
|
92
|
+
puts "Bummer. Head to this URL to update the details:"
|
93
|
+
puts ""
|
94
|
+
puts " #{Messages.edit_app_url(app)}"
|
95
|
+
puts ""
|
96
|
+
puts "exiting..."
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def create_initializer(log_device_type, options = {})
|
103
|
+
puts ""
|
104
|
+
write Messages.task_start("Creating config/initializers/timber.rb")
|
105
|
+
|
106
|
+
logger_code = \
|
107
|
+
case log_device_type
|
108
|
+
when :http
|
109
|
+
api_key_code = options[:api_key_code] || raise(ArgumentError.new("the :api_key_code option is required"))
|
110
|
+
"log_device = Timber::LogDevices::HTTP.new(#{api_key_code})\n" +
|
111
|
+
"Timber::Logger.new(log_device)"
|
112
|
+
|
113
|
+
when :stdout
|
114
|
+
"Timber::Logger.new(STDOUT)"
|
115
|
+
end
|
116
|
+
|
117
|
+
body = <<-BODY
|
118
|
+
# Timber.io Ruby Library
|
119
|
+
#
|
120
|
+
# ^ ^ ^ ^ ___I_ ^ ^ ^ ^ ^ ^ ^
|
121
|
+
# /|\\/|\\/|\\ /|\\ /\\-_--\\ /|\\/|\\ /|\\/|\\/|\\ /|\\/|\\
|
122
|
+
# /|\\/|\\/|\\ /|\\ / \\_-__\\ /|\\/|\\ /|\\/|\\/|\\ /|\\/|\\
|
123
|
+
# /|\\/|\\/|\\ /|\\ |[]| [] | /|\\/|\\ /|\\/|\\/|\\ /|\\/|\\
|
124
|
+
#
|
125
|
+
# Library: http://github.com/timberio/timber-ruby
|
126
|
+
# Docs: http://www.rubydoc.info/github/timberio/timber-ruby
|
127
|
+
# Support: support@timber.io
|
128
|
+
|
129
|
+
logger = case Rails.env
|
130
|
+
when "development", "test"
|
131
|
+
logger = Timber::Logger.new(STDOUT)
|
132
|
+
logger.formatter = Timber::Logger::SimpleFormatter.new
|
133
|
+
logger
|
134
|
+
else
|
135
|
+
#{logger_code}
|
136
|
+
end
|
137
|
+
|
138
|
+
Timber::Frameworks::Rails.set_logger(logger)
|
139
|
+
BODY
|
140
|
+
|
141
|
+
FileUtils.mkdir_p(File.join(Dir.pwd, "config", "initializers"))
|
142
|
+
File.write(File.join(Dir.pwd, "config/initializers/timber.rb"), body)
|
143
|
+
|
144
|
+
puts colorize(Messages.task_complete("Creating config/initializers/timber.rb"), :green)
|
145
|
+
end
|
146
|
+
|
147
|
+
def send_test_messages(api_key)
|
148
|
+
write Messages.task_start("Sending test logs")
|
149
|
+
|
150
|
+
http_device = LogDevices::HTTP.new(api_key)
|
151
|
+
logger = Logger.new(http_device)
|
152
|
+
logger.info("test")
|
153
|
+
|
154
|
+
puts colorize(Messages.task_complete("Sending test logs"), :green)
|
155
|
+
end
|
156
|
+
|
157
|
+
def collect_feedback(api)
|
158
|
+
puts ""
|
159
|
+
puts Messages.separator
|
160
|
+
puts ""
|
161
|
+
rating = ask("How would rate this install experience? 1 (bad) - 5 (perfect)")
|
162
|
+
case rating
|
163
|
+
when "4", "5"
|
164
|
+
api.event!(:feedback, rating: rating.to_i)
|
165
|
+
puts ""
|
166
|
+
puts Messages.we_love_you_too
|
167
|
+
|
168
|
+
when "1", "2", "3"
|
169
|
+
puts ""
|
170
|
+
puts Messages.bad_experience_message
|
171
|
+
puts ""
|
172
|
+
|
173
|
+
comments = ask("Type your comments (enter sends)")
|
174
|
+
|
175
|
+
api.event!(:feedback, rating: rating.to_i, comments: comments)
|
176
|
+
|
177
|
+
puts ""
|
178
|
+
puts "Thank you! We take feedback seriously and will work to improve this."
|
179
|
+
end
|
180
|
+
|
181
|
+
puts ""
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|