timber 1.1.0 → 1.1.1
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/README.md +41 -7
- data/lib/timber/log_entry.rb +7 -5
- data/lib/timber/logger.rb +15 -6
- data/lib/timber/version.rb +1 -1
- data/spec/timber/log_devices/http_spec.rb +4 -4
- data/spec/timber/log_entry_spec.rb +1 -1
- data/spec/timber/logger_spec.rb +21 -6
- data/spec/timber/probes/action_controller_log_subscriber_spec.rb +2 -2
- data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +1 -1
- data/spec/timber/probes/action_view_log_subscriber_spec.rb +1 -1
- data/spec/timber/probes/active_record_log_subscriber_spec.rb +2 -2
- data/spec/timber/probes/rails_rack_logger_spec.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8edaf795dfbf8e705fb737cdb7f43ce3821ef2f
|
4
|
+
data.tar.gz: e2d21bde5b38fe63b346070cf0e5ac4f86e158ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 162e70c46240ca58184950aa7165ff146f2f7b7e78c7e012a694b3aa97b968112870327a8bba978e503ab8a418b86989e8fdecacdf61c6dcefb4204d239322b3
|
7
|
+
data.tar.gz: fdbbff594c9caf2a773db228648f8384c5e345b120d90878ea25289dab529955ce762af955fd0a73de9d00c5fabc22085ef6733ed2a41d3cef0643ae60d14c41
|
data/README.md
CHANGED
@@ -55,6 +55,7 @@ blog post.
|
|
55
55
|
5. **Long term retention.** Timber is designed on modern big-data principles. As a result, we can
|
56
56
|
offer 6+ months of retention at prices cheaper than alternatives offering <1 month.
|
57
57
|
This allows you to unlock your logs for purposes beyond debugging.
|
58
|
+
|
58
59
|
---
|
59
60
|
|
60
61
|
</p></details>
|
@@ -63,8 +64,9 @@ blog post.
|
|
63
64
|
|
64
65
|
1. Captures and structures your framework and 3rd party logs. (see next question)
|
65
66
|
2. Adds useful context to every log line. (see next question)
|
66
|
-
3.
|
67
|
-
4.
|
67
|
+
3. Allows you to easily add tags and timings to log. (see [Usage](#usage))
|
68
|
+
4. Provides a framework for logging custom structured events. (see [Usage](#usage))
|
69
|
+
5. Offers transport strategies to [send your logs](#send-your-logs) to the Timber service.
|
68
70
|
|
69
71
|
---
|
70
72
|
|
@@ -122,7 +124,7 @@ logger.info("My log message")
|
|
122
124
|
# My log message @metadata {"level": "info", "context": {...}}
|
123
125
|
```
|
124
126
|
|
125
|
-
Timber will never deviate from the public `::Logger` interface in *any* way.
|
127
|
+
Timber will *never* deviate from the public `::Logger` interface in *any* way.
|
126
128
|
|
127
129
|
---
|
128
130
|
|
@@ -130,7 +132,7 @@ Timber will never deviate from the public `::Logger` interface in *any* way.
|
|
130
132
|
|
131
133
|
<details><summary><strong>Tagging logs</strong></summary><p>
|
132
134
|
|
133
|
-
Need a quick
|
135
|
+
Need a quick way to identify logs? Use tags!:
|
134
136
|
|
135
137
|
```ruby
|
136
138
|
logger.info(message: "My log message", tag: "tag")
|
@@ -156,10 +158,41 @@ end
|
|
156
158
|
# My log message @metadata {"level": "info", "tags": ["tag"], "context": {...}}
|
157
159
|
```
|
158
160
|
|
161
|
+
* In the Timber console use the query: `tags:tag`.
|
162
|
+
|
163
|
+
---
|
164
|
+
|
159
165
|
</p></details>
|
160
166
|
|
167
|
+
<details><summary><strong>Timings, Durations, & Metrics</strong></summary><p>
|
168
|
+
|
169
|
+
Timings allow you to easily capture one-off timings in your code; a simple
|
170
|
+
way to benchmark code execution:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
start = Time.now
|
174
|
+
# ...my code to time...
|
175
|
+
time_ms = (Time.now - start) * 1000
|
176
|
+
logger.info(message: "Task complete", tag: "my_task", time_ms: time_ms)
|
177
|
+
|
178
|
+
# My log message @metadata {"level": "info", tags: ["my_task"], "time_ms": 54.2132, "context": {...}}
|
179
|
+
```
|
180
|
+
|
181
|
+
* In the Timber console use the query: `tags:my_task time_ms>500`
|
182
|
+
* The Timber console will also display this value inline with your logs. No need to include it
|
183
|
+
in the log message, but you certainly can if you'd prefer.
|
184
|
+
|
185
|
+
---
|
186
|
+
|
187
|
+
</p></details>
|
188
|
+
|
189
|
+
|
161
190
|
<details><summary><strong>Custom events</strong></summary><p>
|
162
191
|
|
192
|
+
Custom events can be used to structure information about events that are central
|
193
|
+
to your line of business like receiving credit card payments, saving a draft of a post,
|
194
|
+
or changing a user's password. You have 2 options to do this:
|
195
|
+
|
163
196
|
1. Log a structured Hash (simplest)
|
164
197
|
|
165
198
|
```ruby
|
@@ -168,7 +201,8 @@ end
|
|
168
201
|
# Payment rejected @metadata {"level": "warn", "event": {"payment_rejected": {"customer_id": "abcd1234", "amount": 100, "reason": "Card expired"}}, "context": {...}}
|
169
202
|
```
|
170
203
|
|
171
|
-
* The hash can *only* have
|
204
|
+
* The hash can *only* have 2 keys: `:message` and "event type" key; `:payment_rejected` in this example.
|
205
|
+
* Timber will keyspace your event data by the event type key passed.
|
172
206
|
|
173
207
|
2. Log a Struct (recommended)
|
174
208
|
|
@@ -185,7 +219,7 @@ end
|
|
185
219
|
# Payment rejected @metadata {"level": "warn", "event": {"payment_rejected": {"customer_id": "abcd1234", "amount": 100, "reason": "Card expired"}}, "context": {...}}
|
186
220
|
```
|
187
221
|
|
188
|
-
*
|
222
|
+
* In the Timber console use queries like: `payment_rejected.customer_id:xiaus1934` or `payment_rejected.amount>100`
|
189
223
|
* For more advanced examples see [`Timber::Logger`](lib/timber.logger.rb).
|
190
224
|
* Also, notice there is no mention of Timber in the above code. Just plain old logging.
|
191
225
|
|
@@ -261,7 +295,7 @@ gem 'timber'
|
|
261
295
|
|
262
296
|
## Setup
|
263
297
|
|
264
|
-
<details><summary><strong>Rails
|
298
|
+
<details><summary><strong>Rails (all versions, including edge)</strong></summary><p>
|
265
299
|
|
266
300
|
*Replace* any existing `config.logger=` calls in `config/environments/production.rb` with:
|
267
301
|
|
data/lib/timber/log_entry.rb
CHANGED
@@ -3,9 +3,9 @@ module Timber
|
|
3
3
|
# `Logger` and the log device that you set it up with.
|
4
4
|
class LogEntry #:nodoc:
|
5
5
|
DT_PRECISION = 6.freeze
|
6
|
-
SCHEMA = "https://raw.githubusercontent.com/timberio/log-event-json-schema/1.2.
|
6
|
+
SCHEMA = "https://raw.githubusercontent.com/timberio/log-event-json-schema/1.2.4/schema.json".freeze
|
7
7
|
|
8
|
-
attr_reader :context_snapshot, :event, :level, :message, :progname, :tags, :time
|
8
|
+
attr_reader :context_snapshot, :event, :level, :message, :progname, :tags, :time, :time_ms
|
9
9
|
|
10
10
|
# Creates a log entry suitable to be sent to the Timber API.
|
11
11
|
# @param severity [Integer] the log level / severity
|
@@ -17,12 +17,13 @@ module Timber
|
|
17
17
|
# @param event [Timber.Event] structured data representing the log line event. This should be
|
18
18
|
# an instance of `Timber.Event`.
|
19
19
|
# @return [LogEntry] the resulting LogEntry object
|
20
|
-
def initialize(level, time, progname, message, context_snapshot, event,
|
20
|
+
def initialize(level, time, progname, message, context_snapshot, event, options = {})
|
21
21
|
@level = level
|
22
22
|
@time = time.utc
|
23
23
|
@progname = progname
|
24
24
|
@message = message
|
25
|
-
@tags = tags
|
25
|
+
@tags = options[:tags]
|
26
|
+
@time_ms = options[:time_ms]
|
26
27
|
|
27
28
|
context_snapshot = {} if context_snapshot.nil?
|
28
29
|
system_context = Contexts::System.new(pid: Process.pid)
|
@@ -34,7 +35,8 @@ module Timber
|
|
34
35
|
|
35
36
|
def as_json(options = {})
|
36
37
|
options ||= {}
|
37
|
-
hash = {:level => level, :dt => formatted_dt, :message => message, :tags => tags
|
38
|
+
hash = {:level => level, :dt => formatted_dt, :message => message, :tags => tags,
|
39
|
+
:time_ms => time_ms}
|
38
40
|
|
39
41
|
if !event.nil?
|
40
42
|
hash[:event] = event
|
data/lib/timber/logger.rb
CHANGED
@@ -76,15 +76,24 @@ module Timber
|
|
76
76
|
def build_log_entry(severity, time, progname, msg)
|
77
77
|
level = SEVERITY_MAP.fetch(severity)
|
78
78
|
context_snapshot = CurrentContext.instance.snapshot
|
79
|
+
|
79
80
|
tags = extract_active_support_tagged_logging_tags
|
80
|
-
|
81
|
-
|
81
|
+
time_ms = nil
|
82
|
+
if msg.is_a?(Hash)
|
83
|
+
tags << msg.delete(:tag) if msg.key?(:tag)
|
84
|
+
tags += msg.delete(:tags) if msg.key?(:tags)
|
85
|
+
tags.uniq!
|
86
|
+
time_ms = msg.delete(:time_ms) if msg.key?(:time_ms)
|
87
|
+
|
88
|
+
msg = msg[:message] if msg.length == 1
|
89
|
+
end
|
90
|
+
|
82
91
|
event = Events.build(msg)
|
83
92
|
|
84
93
|
if event
|
85
|
-
LogEntry.new(level, time, progname, event.message, context_snapshot, event, tags)
|
94
|
+
LogEntry.new(level, time, progname, event.message, context_snapshot, event, tags: tags, time_ms: time_ms)
|
86
95
|
else
|
87
|
-
LogEntry.new(level, time, progname, msg, context_snapshot, nil, tags)
|
96
|
+
LogEntry.new(level, time, progname, msg, context_snapshot, nil, tags: tags, time_ms: time_ms)
|
88
97
|
end
|
89
98
|
end
|
90
99
|
|
@@ -104,10 +113,10 @@ module Timber
|
|
104
113
|
#
|
105
114
|
# Example message:
|
106
115
|
#
|
107
|
-
# My log message @
|
116
|
+
# My log message @metadata {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00"}
|
108
117
|
#
|
109
118
|
class HybridFormatter < Formatter
|
110
|
-
METADATA_CALLOUT = "@
|
119
|
+
METADATA_CALLOUT = "@metadata".freeze
|
111
120
|
|
112
121
|
def call(severity, time, progname, msg)
|
113
122
|
log_entry = build_log_entry(severity, time, progname, msg)
|
data/lib/timber/version.rb
CHANGED
@@ -58,9 +58,9 @@ describe Timber::LogDevices::HTTP do
|
|
58
58
|
|
59
59
|
it "should add a request to the queue" do
|
60
60
|
http = described_class.new("MYKEY", threads: false)
|
61
|
-
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil
|
61
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
|
62
62
|
http.write(log_entry)
|
63
|
-
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil
|
63
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
|
64
64
|
http.write(log_entry)
|
65
65
|
http.send(:flush)
|
66
66
|
request_queue = http.instance_variable_get(:@request_queue)
|
@@ -109,9 +109,9 @@ describe Timber::LogDevices::HTTP do
|
|
109
109
|
to_return(:status => 200, :body => "", :headers => {})
|
110
110
|
|
111
111
|
http = described_class.new("MYKEY", flush_interval: 0.1)
|
112
|
-
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil
|
112
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
|
113
113
|
http.write(log_entry)
|
114
|
-
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil
|
114
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
|
115
115
|
http.write(log_entry)
|
116
116
|
sleep 0.3
|
117
117
|
|
@@ -7,7 +7,7 @@ describe Timber::LogEntry, :rails_23 => true do
|
|
7
7
|
it "should encode properly with an event and context" do
|
8
8
|
event = Timber::Events::Custom.new(type: :event_type, message: "event_message", data: {a: 1})
|
9
9
|
context = {custom: Timber::Contexts::Custom.new(type: :context_type, data: {b: 1})}
|
10
|
-
log_entry = described_class.new("INFO", time, nil, "log message", context, event
|
10
|
+
log_entry = described_class.new("INFO", time, nil, "log message", context, event)
|
11
11
|
msgpack = log_entry.to_msgpack
|
12
12
|
expect(msgpack).to start_with("\x86\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z".force_encoding("ASCII-8BIT"))
|
13
13
|
end
|
data/spec/timber/logger_spec.rb
CHANGED
@@ -15,7 +15,7 @@ describe Timber::Logger, :rails_23 => true do
|
|
15
15
|
|
16
16
|
it "should accept strings" do
|
17
17
|
logger.info("this is a test")
|
18
|
-
expect(io.string).to start_with("this is a test @
|
18
|
+
expect(io.string).to start_with("this is a test @metadata {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\"")
|
19
19
|
end
|
20
20
|
|
21
21
|
context "with a context" do
|
@@ -37,7 +37,7 @@ describe Timber::Logger, :rails_23 => true do
|
|
37
37
|
it "should snapshot and include the context" do
|
38
38
|
expect(Timber::CurrentContext.instance).to receive(:snapshot).and_call_original
|
39
39
|
logger.info("this is a test")
|
40
|
-
expect(io.string).to start_with("this is a test @
|
40
|
+
expect(io.string).to start_with("this is a test @metadata {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\"")
|
41
41
|
expect(io.string).to include("\"http\":{\"method\":\"POST\",\"path\":\"/checkout\",\"remote_addr\":\"123.456.789.10\",\"request_id\":\"abcd1234\"}")
|
42
42
|
end
|
43
43
|
end
|
@@ -46,28 +46,43 @@ describe Timber::Logger, :rails_23 => true do
|
|
46
46
|
message = {message: "payment rejected", payment_rejected: {customer_id: "abcde1234", amount: 100}}
|
47
47
|
expect(Timber::Events).to receive(:build).with(message).and_call_original
|
48
48
|
logger.info(message)
|
49
|
-
expect(io.string).to start_with("payment rejected @
|
49
|
+
expect(io.string).to start_with("payment rejected @metadata {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",")
|
50
50
|
expect(io.string).to include("\"event\":{\"server_side_app\":{\"custom\":{\"payment_rejected\":{\"customer_id\":\"abcde1234\",\"amount\":100}}}}")
|
51
51
|
end
|
52
52
|
|
53
53
|
it "should log properly when an event is passed" do
|
54
54
|
message = Timber::Events::SQLQuery.new(sql: "select * from users", time_ms: 56, message: "select * from users")
|
55
55
|
logger.info(message)
|
56
|
-
expect(io.string).to start_with("select * from users @
|
56
|
+
expect(io.string).to start_with("select * from users @metadata {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",")
|
57
57
|
expect(io.string).to include("\"event\":{\"server_side_app\":{\"sql_query\":{\"sql\":\"select * from users\",\"time_ms\":56.0}}}")
|
58
58
|
end
|
59
59
|
|
60
|
+
it "should allow :time_ms" do
|
61
|
+
logger.info(message: "event complete", time_ms: 54.5)
|
62
|
+
expect(io.string).to include("\"time_ms\":54.5")
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should allow :tag" do
|
66
|
+
logger.info(message: "event complete", tag: "tag1")
|
67
|
+
expect(io.string).to include("\"tags\":[\"tag1\"]")
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should allow :tags" do
|
71
|
+
logger.info(message: "event complete", tags: ["tag1", "tag2"])
|
72
|
+
expect(io.string).to include("\"tags\":[\"tag1\",\"tag2\"]")
|
73
|
+
end
|
74
|
+
|
60
75
|
it "should allow functions" do
|
61
76
|
logger.info do
|
62
77
|
{message: "payment rejected", payment_rejected: {customer_id: "abcde1234", amount: 100}}
|
63
78
|
end
|
64
|
-
expect(io.string).to start_with("payment rejected @
|
79
|
+
expect(io.string).to start_with("payment rejected @metadata {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",")
|
65
80
|
expect(io.string).to include("\"event\":{\"server_side_app\":{\"custom\":{\"payment_rejected\":{\"customer_id\":\"abcde1234\",\"amount\":100}}}}")
|
66
81
|
end
|
67
82
|
|
68
83
|
it "should escape new lines" do
|
69
84
|
logger.info "first\nsecond"
|
70
|
-
expect(io.string).to start_with("first\\nsecond @
|
85
|
+
expect(io.string).to start_with("first\\nsecond @metadata")
|
71
86
|
end
|
72
87
|
end
|
73
88
|
|
@@ -57,9 +57,9 @@ describe Timber::Probes::ActionControllerLogSubscriber do
|
|
57
57
|
dispatch_rails_request("/log_subscriber")
|
58
58
|
lines = io.string.split("\n")
|
59
59
|
expect(lines.length).to eq(2)
|
60
|
-
expect(lines[0]).to start_with('Processing by LogSubscriberController#index as HTML @
|
60
|
+
expect(lines[0]).to start_with('Processing by LogSubscriberController#index as HTML @metadata {"level":"info","dt":"2016-09-01T12:00:00.000000Z"')
|
61
61
|
expect(lines[0]).to include('"event":{"server_side_app":{"controller_call":{"controller":"LogSubscriberController","action":"index"}}}')
|
62
|
-
expect(lines[1]).to start_with('Completed 200 OK in 0.0ms (Views: 1.0ms) @
|
62
|
+
expect(lines[1]).to start_with('Completed 200 OK in 0.0ms (Views: 1.0ms) @metadata {"level":"info","dt":"2016-09-01T12:00:00.000000Z"')
|
63
63
|
expect(lines[1]).to include('"event":{"server_side_app":{"http_server_response":{"status":200,"time_ms":0.0}}}')
|
64
64
|
end
|
65
65
|
end
|
@@ -38,7 +38,7 @@ describe Timber::Probes::ActionDispatchDebugExceptions do
|
|
38
38
|
dispatch_rails_request("/exception")
|
39
39
|
# Because constantly updating the line numbers sucks :/
|
40
40
|
expect(io.string).to include("RuntimeError (boom):\\n\\n")
|
41
|
-
expect(io.string).to include("@
|
41
|
+
expect(io.string).to include("@metadata")
|
42
42
|
expect(io.string).to include("\"event\":{\"server_side_app\":{\"exception\":{\"name\":\"RuntimeError\",\"message\":\"boom\",\"backtrace\":[\"")
|
43
43
|
end
|
44
44
|
|
@@ -51,7 +51,7 @@ describe Timber::Probes::ActionViewLogSubscriber do
|
|
51
51
|
it "should log the controller call event" do
|
52
52
|
allow_any_instance_of(Timber::Probes::ActionViewLogSubscriber::LogSubscriber).to receive(:logger).and_return(logger)
|
53
53
|
dispatch_rails_request("/action_view_log_subscriber")
|
54
|
-
expect(io.string).to start_with(" Rendered spec/support/rails/templates/template.html (0.0ms) @
|
54
|
+
expect(io.string).to start_with(" Rendered spec/support/rails/templates/template.html (0.0ms) @metadata {\"level\":\"info\"")
|
55
55
|
expect(io.string).to include("\"event\":{\"server_side_app\":{\"template_render\":{\"name\":\"spec/support/rails/templates/template.html\",\"time_ms\":0.0}}},")
|
56
56
|
end
|
57
57
|
end
|
@@ -36,12 +36,12 @@ describe Timber::Probes::ActiveRecordLogSubscriber do
|
|
36
36
|
|
37
37
|
it "should log the sql query" do
|
38
38
|
User.order("users.id DESC").all.collect # collect kicks the sql because it is lazily executed
|
39
|
-
message = " \e[1m\e[36mUser Load (0.0ms)\e[0m \e[1m\e[34mSELECT \"users\".* FROM \"users\" ORDER BY users.id DESC\e[0m @
|
39
|
+
message = " \e[1m\e[36mUser Load (0.0ms)\e[0m \e[1m\e[34mSELECT \"users\".* FROM \"users\" ORDER BY users.id DESC\e[0m @metadata {\"level\":\"debug\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"sql_query\":{\"sql\":\"SELECT \\\"users\\\".* FROM \\\"users\\\" ORDER BY users.id DESC\",\"time_ms\":0.0}}}\n"
|
40
40
|
# Rails 4.X adds random spaces :/
|
41
41
|
string = io.string.gsub(" ORDER BY", " ORDER BY")
|
42
42
|
string = string.gsub(" ORDER BY", " ORDER BY")
|
43
43
|
expect(string).to include("users.id DESC")
|
44
|
-
expect(string).to include("@
|
44
|
+
expect(string).to include("@metadata")
|
45
45
|
expect(string).to include("\"level\":\"debug\"")
|
46
46
|
expect(string).to include("\"sql\":")
|
47
47
|
end
|
@@ -38,7 +38,7 @@ describe Timber::Probes::RailsRackLogger do
|
|
38
38
|
allow(::Rails).to receive(:logger).and_return(logger) # Rails 3.2.X
|
39
39
|
allow_any_instance_of(::Rails::Rack::Logger).to receive(:logger).and_return(logger)
|
40
40
|
dispatch_rails_request("/rails_rack_logger")
|
41
|
-
expect(io.string).to start_with("Started GET \"/rails_rack_logger\" for 123.456.789.10 @
|
41
|
+
expect(io.string).to start_with("Started GET \"/rails_rack_logger\" for 123.456.789.10 @metadata {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\"")
|
42
42
|
expect(io.string).to include("\"event\":{\"server_side_app\":{\"http_request\":{\"host\":\"example.org\",\"method\":\"GET\",\"path\":\"/rails_rack_logger\",\"port\":80,\"headers\":{\"remote_addr\":\"123.456.789.10\",\"request_id\":\"unique-request-id-1234\"}}}")
|
43
43
|
end
|
44
44
|
end
|