logster 2.1.2 → 2.2.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.
- checksums.yaml +4 -4
- data/.gitignore +21 -19
- data/.rubocop.yml +1 -1
- data/.travis.yml +16 -16
- data/CHANGELOG.md +224 -172
- data/Gemfile +4 -4
- data/Guardfile +8 -8
- data/LICENSE.txt +22 -22
- data/README.md +99 -99
- data/Rakefile +21 -21
- data/assets/fonts/FontAwesome.otf +0 -0
- data/assets/fonts/fontawesome-webfont.eot +0 -0
- data/assets/fonts/fontawesome-webfont.svg +639 -639
- data/assets/fonts/fontawesome-webfont.ttf +0 -0
- data/assets/fonts/fontawesome-webfont.woff +0 -0
- data/assets/fonts/fontawesome-webfont.woff2 +0 -0
- data/assets/images/Icon-144_rounded.png +0 -0
- data/assets/images/Icon-144_square.png +0 -0
- data/assets/images/icon_144x144.png +0 -0
- data/assets/images/icon_64x64.png +0 -0
- data/assets/javascript/client-app.js +115 -106
- data/assets/stylesheets/client-app.css +1 -1
- data/build_client_app.sh +0 -0
- data/client-app/.editorconfig +20 -20
- data/client-app/.ember-cli +9 -9
- data/client-app/.eslintignore +19 -19
- data/client-app/.eslintrc.js +46 -46
- data/client-app/.gitignore +23 -23
- data/client-app/.travis.yml +27 -27
- data/client-app/.watchmanconfig +3 -3
- data/client-app/README.md +57 -57
- data/client-app/app/app.js +0 -0
- data/client-app/app/components/actions-menu.js +43 -43
- data/client-app/app/components/env-tab.js +80 -80
- data/client-app/app/components/message-info.js +0 -0
- data/client-app/app/components/message-row.js +0 -0
- data/client-app/app/components/panel-resizer.js +0 -0
- data/client-app/app/components/patterns-list.js +109 -0
- data/client-app/app/components/tab-contents.js +27 -27
- data/client-app/app/components/tabbed-section.js +0 -0
- data/client-app/app/components/time-formatter.js +0 -0
- data/client-app/app/components/update-time.js +0 -0
- data/client-app/app/controllers/index.js +22 -6
- data/client-app/app/controllers/show.js +0 -0
- data/client-app/app/helpers/logster-url.js +12 -0
- data/client-app/app/helpers/or.js +7 -0
- data/client-app/app/index.html +30 -29
- data/client-app/app/initializers/app-init.js +67 -67
- data/client-app/app/lib/preload.js +20 -20
- data/client-app/app/lib/utilities.js +150 -149
- data/client-app/app/models/message-collection.js +0 -0
- data/client-app/app/models/message.js +100 -100
- data/client-app/app/models/pattern-item.js +25 -0
- data/client-app/app/resolver.js +0 -0
- data/client-app/app/router.js +1 -0
- data/client-app/app/routes/index.js +2 -9
- data/client-app/app/routes/settings.js +15 -0
- data/client-app/app/routes/show.js +0 -0
- data/client-app/app/styles/app.css +633 -527
- data/client-app/app/templates/application.hbs +2 -2
- data/client-app/app/templates/components/actions-menu.hbs +12 -12
- data/client-app/app/templates/components/env-tab.hbs +10 -10
- data/client-app/app/templates/components/message-info.hbs +41 -41
- data/client-app/app/templates/components/message-row.hbs +15 -15
- data/client-app/app/templates/components/panel-resizer.hbs +3 -3
- data/client-app/app/templates/components/patterns-list.hbs +25 -0
- data/client-app/app/templates/components/tabbed-section.hbs +10 -10
- data/client-app/app/templates/components/time-formatter.hbs +1 -1
- data/client-app/app/templates/index.hbs +65 -58
- data/client-app/app/templates/settings.hbs +26 -0
- data/client-app/app/templates/show.hbs +7 -7
- data/client-app/config/environment.js +51 -51
- data/client-app/config/optional-features.json +3 -3
- data/client-app/config/targets.js +18 -18
- data/client-app/ember-cli-build.js +29 -29
- data/client-app/package-lock.json +11357 -11365
- data/client-app/package.json +57 -56
- data/client-app/public/assets/images/icon_144x144.png +0 -0
- data/client-app/public/assets/images/icon_64x64.png +0 -0
- data/client-app/testem.js +25 -25
- data/client-app/tests/index.html +34 -34
- data/client-app/tests/integration/components/env-tab-test.js +134 -123
- data/client-app/tests/integration/components/message-info-test.js +111 -111
- data/client-app/tests/integration/components/patterns-list-test.js +56 -0
- data/client-app/tests/test-helper.js +8 -8
- data/client-app/tests/unit/controllers/index-test.js +12 -12
- data/client-app/tests/unit/controllers/show-test.js +12 -12
- data/client-app/tests/unit/initializers/app-init-test.js +31 -31
- data/client-app/tests/unit/routes/index-test.js +11 -11
- data/client-app/tests/unit/routes/show-test.js +11 -11
- data/lib/examples/sidekiq_logster_reporter.rb +21 -21
- data/lib/logster.rb +59 -54
- data/lib/logster/base_store.rb +169 -141
- data/lib/logster/cache.rb +20 -0
- data/lib/logster/configuration.rb +27 -26
- data/lib/logster/defer_logger.rb +14 -14
- data/lib/logster/ignore_pattern.rb +65 -65
- data/lib/logster/logger.rb +113 -113
- data/lib/logster/message.rb +212 -212
- data/lib/logster/middleware/debug_exceptions.rb +26 -26
- data/lib/logster/middleware/reporter.rb +55 -55
- data/lib/logster/middleware/viewer.rb +297 -222
- data/lib/logster/pattern.rb +95 -0
- data/lib/logster/rails/railtie.rb +63 -63
- data/lib/logster/redis_store.rb +584 -566
- data/lib/logster/scheduler.rb +54 -54
- data/lib/logster/suppression_pattern.rb +17 -0
- data/lib/logster/version.rb +3 -3
- data/lib/logster/web.rb +14 -14
- data/logster.gemspec +35 -35
- data/test/examples/test_sidekiq_reporter_example.rb +46 -46
- data/test/fake_data/Gemfile +4 -4
- data/test/fake_data/generate.rb +10 -10
- data/test/logster/middleware/test_reporter.rb +19 -19
- data/test/logster/middleware/test_viewer.rb +267 -96
- data/test/logster/test_base_store.rb +147 -147
- data/test/logster/test_cache.rb +38 -0
- data/test/logster/test_defer_logger.rb +34 -34
- data/test/logster/test_ignore_pattern.rb +41 -41
- data/test/logster/test_logger.rb +100 -86
- data/test/logster/test_message.rb +119 -119
- data/test/logster/test_pattern.rb +152 -0
- data/test/logster/test_redis_rate_limiter.rb +230 -230
- data/test/logster/test_redis_store.rb +689 -720
- data/test/test_helper.rb +38 -38
- data/vendor/assets/javascripts/logster.js.erb +39 -39
- metadata +24 -7
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
class SidekiqLogsterReporter
|
|
2
|
-
def call(ex, context = {})
|
|
3
|
-
# Pass context to Logster
|
|
4
|
-
fake_env = {}
|
|
5
|
-
context.each do |key, value|
|
|
6
|
-
Logster.add_to_env(fake_env, key, value)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
text = "Job exception: #{ex}\n"
|
|
10
|
-
if ex.backtrace
|
|
11
|
-
Logster.add_to_env(fake_env, :backtrace, ex.backtrace)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
Thread.current[Logster::Logger::LOGSTER_ENV] = fake_env
|
|
15
|
-
Logster.logger.error(text)
|
|
16
|
-
rescue => e
|
|
17
|
-
Logster.logger.fatal("Failed to log exception #{ex} #{hash}\nReason: #{e.class} #{e}\n#{e.backtrace.join("\n")}")
|
|
18
|
-
ensure
|
|
19
|
-
Thread.current[Logster::Logger::LOGSTER_ENV] = nil
|
|
20
|
-
end
|
|
21
|
-
end
|
|
1
|
+
class SidekiqLogsterReporter
|
|
2
|
+
def call(ex, context = {})
|
|
3
|
+
# Pass context to Logster
|
|
4
|
+
fake_env = {}
|
|
5
|
+
context.each do |key, value|
|
|
6
|
+
Logster.add_to_env(fake_env, key, value)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
text = "Job exception: #{ex}\n"
|
|
10
|
+
if ex.backtrace
|
|
11
|
+
Logster.add_to_env(fake_env, :backtrace, ex.backtrace)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Thread.current[Logster::Logger::LOGSTER_ENV] = fake_env
|
|
15
|
+
Logster.logger.error(text)
|
|
16
|
+
rescue => e
|
|
17
|
+
Logster.logger.fatal("Failed to log exception #{ex} #{hash}\nReason: #{e.class} #{e}\n#{e.backtrace.join("\n")}")
|
|
18
|
+
ensure
|
|
19
|
+
Thread.current[Logster::Logger::LOGSTER_ENV] = nil
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/logster.rb
CHANGED
|
@@ -1,54 +1,59 @@
|
|
|
1
|
-
require 'logster/logger'
|
|
2
|
-
require 'logster/message'
|
|
3
|
-
require 'logster/configuration'
|
|
4
|
-
require 'logster/web'
|
|
5
|
-
require 'logster/ignore_pattern'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def self.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
require 'logster/logger'
|
|
2
|
+
require 'logster/message'
|
|
3
|
+
require 'logster/configuration'
|
|
4
|
+
require 'logster/web'
|
|
5
|
+
require 'logster/ignore_pattern'
|
|
6
|
+
require 'logster/pattern'
|
|
7
|
+
require 'logster/suppression_pattern'
|
|
8
|
+
require 'logster/cache'
|
|
9
|
+
|
|
10
|
+
if defined? Redis
|
|
11
|
+
require 'logster/redis_store'
|
|
12
|
+
else
|
|
13
|
+
STDERR.puts "ERROR: Redis is not loaded, ensure redis gem is required before logster"
|
|
14
|
+
exit
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Logster
|
|
18
|
+
PATTERNS = [SuppressionPattern]
|
|
19
|
+
|
|
20
|
+
def self.logger=(logger)
|
|
21
|
+
@logger = logger
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.logger
|
|
25
|
+
@logger
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.store=(store)
|
|
29
|
+
@store = store
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.store
|
|
33
|
+
@store
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.config=(config)
|
|
37
|
+
@config = config
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.config
|
|
41
|
+
@config ||= Configuration.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.add_to_env(env, key, value)
|
|
45
|
+
logster_env = Logster::Message.populate_from_env(env)
|
|
46
|
+
logster_env[key] = value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.set_environments(envs)
|
|
50
|
+
config.environments = envs
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# check logster/configuration.rb for config options
|
|
55
|
+
# Logster.config.environments << :staging
|
|
56
|
+
|
|
57
|
+
if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i >= 3
|
|
58
|
+
require 'logster/rails/railtie'
|
|
59
|
+
end
|
data/lib/logster/base_store.rb
CHANGED
|
@@ -1,141 +1,169 @@
|
|
|
1
|
-
module Logster
|
|
2
|
-
class BaseStore
|
|
3
|
-
|
|
4
|
-
attr_accessor :level, :max_retention, :skip_empty, :ignore
|
|
5
|
-
|
|
6
|
-
def initialize
|
|
7
|
-
@max_retention = 60 * 60 * 24 * 7
|
|
8
|
-
@skip_empty = true
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1
|
+
module Logster
|
|
2
|
+
class BaseStore
|
|
3
|
+
|
|
4
|
+
attr_accessor :level, :max_retention, :skip_empty, :ignore, :allow_custom_patterns
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@max_retention = 60 * 60 * 24 * 7
|
|
8
|
+
@skip_empty = true
|
|
9
|
+
@allow_custom_patterns = false
|
|
10
|
+
@patterns_cache = Logster::Cache.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Save a new message at the front of the latest list.
|
|
14
|
+
def save(message)
|
|
15
|
+
not_implemented
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Modify the saved message to the given one (identified by message.key) and bump it to the top of the latest list
|
|
19
|
+
def replace_and_bump(message, save_env: true)
|
|
20
|
+
not_implemented
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Check if another message with the same grouping_key is already stored.
|
|
24
|
+
# Returns the similar message's message.key
|
|
25
|
+
def similar_key(message)
|
|
26
|
+
not_implemented
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The number of messages currently stored.
|
|
30
|
+
def count
|
|
31
|
+
not_implemented
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Delete all unprotected messages in the store.
|
|
35
|
+
def clear
|
|
36
|
+
not_implemented
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Delete all messages, including protected messages.
|
|
40
|
+
def clear_all
|
|
41
|
+
not_implemented
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get a message by its message_key
|
|
45
|
+
def get(message_key, load_env: true)
|
|
46
|
+
not_implemented
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get a group of messages by their message_keys
|
|
50
|
+
def bulk_get(message_keys)
|
|
51
|
+
not_implemented
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get a message's env by its message_key
|
|
55
|
+
def get_env(message_key)
|
|
56
|
+
not_implemented
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Mark a message as protected; i.e. it is not deleted by the #clear method
|
|
60
|
+
def protect(message_key)
|
|
61
|
+
not_implemented
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def delete(message_key)
|
|
65
|
+
not_implemented
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Clear the protected mark for a message.
|
|
69
|
+
def unprotect(message_key)
|
|
70
|
+
not_implemented
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Solve a particular message, causing all old messages with matching version and backtrace
|
|
74
|
+
# to be deleted (report should delete any solved messages when called)
|
|
75
|
+
def solve(message_key)
|
|
76
|
+
not_implemented
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Registers a rate limit on the given severities of logs
|
|
80
|
+
def register_rate_limit(severities, limit, duration, &block)
|
|
81
|
+
not_implemented
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Checks all the existing rate limiters to check if any has been exceeded
|
|
85
|
+
def check_rate_limits(severity)
|
|
86
|
+
not_implemented
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# takes a string as `pattern` and places it under the set `set_name`
|
|
90
|
+
def insert_pattern(set_name, pattern)
|
|
91
|
+
not_implemented
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# takes a string as `pattern` and removes it from the set `set_name`
|
|
95
|
+
def remove_pattern(set_name, pattern)
|
|
96
|
+
not_implemented
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# returns an array of strings each of which must be convertible to regexp
|
|
100
|
+
def get_patterns(set_name)
|
|
101
|
+
not_implemented
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def report(severity, progname, msg, opts = {})
|
|
105
|
+
return if (!msg || (String === msg && msg.empty?)) && skip_empty
|
|
106
|
+
return if level && severity < level
|
|
107
|
+
|
|
108
|
+
message = Logster::Message.new(severity, progname, msg, opts[:timestamp], count: opts[:count])
|
|
109
|
+
|
|
110
|
+
env = opts[:env] || {}
|
|
111
|
+
backtrace = opts[:backtrace]
|
|
112
|
+
if Hash === env && env[:backtrace]
|
|
113
|
+
# Special - passing backtrace through env
|
|
114
|
+
backtrace = env.delete(:backtrace)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
message.populate_from_env(env)
|
|
118
|
+
|
|
119
|
+
if backtrace
|
|
120
|
+
if backtrace.respond_to? :join
|
|
121
|
+
backtrace = backtrace.join("\n")
|
|
122
|
+
end
|
|
123
|
+
message.backtrace = backtrace
|
|
124
|
+
else
|
|
125
|
+
message.backtrace = caller.join("\n")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
return if ignore && ignore.any? { |pattern| message =~ pattern }
|
|
129
|
+
|
|
130
|
+
if Logster.config.enable_custom_patterns_via_ui || allow_custom_patterns
|
|
131
|
+
custom_ignore = @patterns_cache.fetch do
|
|
132
|
+
Logster::SuppressionPattern.find_all(store: self)
|
|
133
|
+
end
|
|
134
|
+
return if custom_ignore.any? { |pattern| message =~ pattern }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
similar = nil
|
|
138
|
+
|
|
139
|
+
if Logster.config.allow_grouping
|
|
140
|
+
key = self.similar_key(message)
|
|
141
|
+
similar = get(key, load_env: false) if key
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if similar
|
|
145
|
+
has_env = !similar.env.nil? && !similar.env.empty?
|
|
146
|
+
if similar.count < Logster::MAX_GROUPING_LENGTH && !has_env
|
|
147
|
+
similar.env = get_env(similar.key) || {}
|
|
148
|
+
end
|
|
149
|
+
save_env = similar.merge_similar_message(message)
|
|
150
|
+
|
|
151
|
+
replace_and_bump(similar, save_env: save_env || has_env)
|
|
152
|
+
similar
|
|
153
|
+
else
|
|
154
|
+
save message
|
|
155
|
+
message
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def clear_suppression_patterns_cache
|
|
160
|
+
@patterns_cache.clear
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def not_implemented
|
|
166
|
+
raise "Not Implemented"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|