lhc 13.0.0 → 15.0.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/.github/workflows/rubocop.yml +15 -0
- data/.github/workflows/test.yml +15 -0
- data/.rubocop.yml +344 -19
- data/.ruby-version +1 -1
- data/README.md +89 -0
- data/Rakefile +3 -3
- data/lhc.gemspec +3 -1
- data/lib/lhc.rb +70 -59
- data/lib/lhc/concerns/lhc/fix_invalid_encoding_concern.rb +1 -0
- data/lib/lhc/config.rb +16 -0
- data/lib/lhc/endpoint.rb +3 -0
- data/lib/lhc/error.rb +7 -4
- data/lib/lhc/interceptor.rb +4 -0
- data/lib/lhc/interceptors.rb +1 -0
- data/lib/lhc/interceptors/auth.rb +10 -5
- data/lib/lhc/interceptors/caching.rb +14 -3
- data/lib/lhc/interceptors/logging.rb +4 -2
- data/lib/lhc/interceptors/monitoring.rb +46 -11
- data/lib/lhc/interceptors/retry.rb +2 -0
- data/lib/lhc/interceptors/rollbar.rb +3 -2
- data/lib/lhc/interceptors/throttle.rb +7 -2
- data/lib/lhc/interceptors/zipkin.rb +2 -0
- data/lib/lhc/request.rb +37 -4
- data/lib/lhc/response.rb +1 -0
- data/lib/lhc/response/data.rb +1 -1
- data/lib/lhc/scrubber.rb +45 -0
- data/lib/lhc/scrubbers/auth_scrubber.rb +33 -0
- data/lib/lhc/scrubbers/body_scrubber.rb +28 -0
- data/lib/lhc/scrubbers/headers_scrubber.rb +38 -0
- data/lib/lhc/scrubbers/params_scrubber.rb +14 -0
- data/lib/lhc/version.rb +1 -1
- data/spec/config/scrubs_spec.rb +108 -0
- data/spec/error/to_s_spec.rb +13 -8
- data/spec/formats/multipart_spec.rb +2 -2
- data/spec/formats/plain_spec.rb +1 -1
- data/spec/interceptors/after_response_spec.rb +1 -1
- data/spec/interceptors/caching/main_spec.rb +2 -2
- data/spec/interceptors/caching/multilevel_cache_spec.rb +2 -1
- data/spec/interceptors/define_spec.rb +1 -0
- data/spec/interceptors/logging/main_spec.rb +21 -1
- data/spec/interceptors/monitoring/caching_spec.rb +66 -0
- data/spec/interceptors/response_competition_spec.rb +2 -2
- data/spec/interceptors/return_response_spec.rb +2 -2
- data/spec/interceptors/rollbar/main_spec.rb +27 -15
- data/spec/request/scrubbed_headers_spec.rb +101 -0
- data/spec/request/scrubbed_options_spec.rb +194 -0
- data/spec/request/scrubbed_params_spec.rb +35 -0
- data/spec/response/data_spec.rb +2 -2
- data/spec/support/zipkin_mock.rb +1 -0
- metadata +40 -21
- data/.rubocop.localch.yml +0 -325
- data/cider-ci.yml +0 -5
- data/cider-ci/bin/bundle +0 -51
- data/cider-ci/bin/ruby_install +0 -8
- data/cider-ci/bin/ruby_version +0 -25
- data/cider-ci/jobs/rspec-activesupport-5.yml +0 -27
- data/cider-ci/jobs/rspec-activesupport-6.yml +0 -28
- data/cider-ci/jobs/rubocop.yml +0 -18
- data/cider-ci/task_components/bundle.yml +0 -22
- data/cider-ci/task_components/rspec.yml +0 -36
- data/cider-ci/task_components/rubocop.yml +0 -29
- data/cider-ci/task_components/ruby.yml +0 -15
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-2.
|
1
|
+
ruby-2.7.2
|
data/README.md
CHANGED
@@ -51,6 +51,7 @@ use it like:
|
|
51
51
|
* [Configuration](#configuration)
|
52
52
|
* [Configuring endpoints](#configuring-endpoints)
|
53
53
|
* [Configuring placeholders](#configuring-placeholders)
|
54
|
+
* [Configuring scrubs](#configuring-scrubs)
|
54
55
|
* [Interceptors](#interceptors)
|
55
56
|
* [Quick start: Configure/Enable Interceptors](#quick-start-configureenable-interceptors)
|
56
57
|
* [Interceptors on local request level](#interceptors-on-local-request-level)
|
@@ -73,6 +74,10 @@ use it like:
|
|
73
74
|
* [Installation](#installation-1)
|
74
75
|
* [Environment](#environment)
|
75
76
|
* [What it tracks](#what-it-tracks)
|
77
|
+
* [Before and after request tracking](#before-and-after-request-tracking)
|
78
|
+
* [Response tracking](#response-tracking)
|
79
|
+
* [Timeout tracking](#timeout-tracking)
|
80
|
+
* [Caching tracking](#caching-tracking)
|
76
81
|
* [Configure](#configure-1)
|
77
82
|
* [Prometheus Interceptor](#prometheus-interceptor)
|
78
83
|
* [Retry Interceptor](#retry-interceptor)
|
@@ -95,6 +100,7 @@ use it like:
|
|
95
100
|
|
96
101
|
|
97
102
|
|
103
|
+
|
98
104
|
## Basic methods
|
99
105
|
|
100
106
|
Available are `get`, `post`, `put` & `delete`.
|
@@ -486,6 +492,50 @@ You can configure global placeholders, that are used when generating urls from u
|
|
486
492
|
LHC.get(:feedbacks) # http://datastore/v2/feedbacks
|
487
493
|
```
|
488
494
|
|
495
|
+
### Configuring scrubs
|
496
|
+
|
497
|
+
You can filter out sensitive request data from your log files and rollbar by appending them to `LHS.config.scrubs`. These values will be marked `[FILTERED]` in the log and on rollbar. Also nested parameters are being filtered.
|
498
|
+
The scrubbing configuration affects all request done by LHC independent of the endpoint. You can scrub any attribute within `:params`, `:headers` or `:body`. For `:auth` you either can choose `:bearer` or `:basic` (default is both).
|
499
|
+
|
500
|
+
LHS scrubs per default:
|
501
|
+
- Bearer Token within the Request Header
|
502
|
+
- Basic Auth `username` and `password` within the Request Header
|
503
|
+
- `password` and `password_confirmation` within the Request Body
|
504
|
+
|
505
|
+
Enhance the default scrubbing by pushing the name of the parameter, which should be scrubbed, as string to the existing configuration.
|
506
|
+
You can also add multiple parameters at once by pushing multiple strings.
|
507
|
+
|
508
|
+
Example:
|
509
|
+
```ruby
|
510
|
+
LHC.configure do |c|
|
511
|
+
c.scrubs[:params] << 'api_key'
|
512
|
+
c.scrubs[:body].push('user_token', 'secret_key')
|
513
|
+
end
|
514
|
+
```
|
515
|
+
|
516
|
+
For disabling scrubbing, add following configuration:
|
517
|
+
```ruby
|
518
|
+
LHC.configure do |c|
|
519
|
+
c.scrubs = {}
|
520
|
+
end
|
521
|
+
```
|
522
|
+
|
523
|
+
If you want to turn off `:bearer` or `:basic` scrubbing, then just overwrite the `:auth` configuration.
|
524
|
+
|
525
|
+
Example:
|
526
|
+
```ruby
|
527
|
+
LHC.configure do |c|
|
528
|
+
c.scrubs[:auth] = [:bearer]
|
529
|
+
end
|
530
|
+
```
|
531
|
+
|
532
|
+
If your app has a different authentication strategy than Bearer Authentication or Basic Authentication then you can filter the data by scrubbing the whole header:
|
533
|
+
```ruby
|
534
|
+
LHC.configure do |c|
|
535
|
+
c.scrubs[:headers] << 'Authorization'
|
536
|
+
end
|
537
|
+
```
|
538
|
+
|
489
539
|
## Interceptors
|
490
540
|
|
491
541
|
To monitor and manipulate the HTTP communication done with LHC, you can define interceptors that follow the (Inteceptor Pattern)[https://en.wikipedia.org/wiki/Interceptor_pattern].
|
@@ -744,11 +794,15 @@ It tracks request attempts with `before_request` and `after_request` (counts).
|
|
744
794
|
In case your workers/processes are getting killed due limited time constraints,
|
745
795
|
you are able to detect deltas with relying on "before_request", and "after_request" counts:
|
746
796
|
|
797
|
+
###### Before and after request tracking
|
798
|
+
|
747
799
|
```ruby
|
748
800
|
"lhc.<app_name>.<env>.<host>.<http_method>.before_request", 1
|
749
801
|
"lhc.<app_name>.<env>.<host>.<http_method>.after_request", 1
|
750
802
|
```
|
751
803
|
|
804
|
+
###### Response tracking
|
805
|
+
|
752
806
|
In case of a successful response it reports the response code with a count and the response time with a gauge value.
|
753
807
|
|
754
808
|
```ruby
|
@@ -759,6 +813,17 @@ In case of a successful response it reports the response code with a count and t
|
|
759
813
|
"lhc.<app_name>.<env>.<host>.<http_method>.time", 43
|
760
814
|
```
|
761
815
|
|
816
|
+
In case of a unsuccessful response it reports the response code with a count but no time:
|
817
|
+
|
818
|
+
```ruby
|
819
|
+
LHC.get('http://local.ch')
|
820
|
+
|
821
|
+
"lhc.<app_name>.<env>.<host>.<http_method>.count", 1
|
822
|
+
"lhc.<app_name>.<env>.<host>.<http_method>.500", 1
|
823
|
+
```
|
824
|
+
|
825
|
+
###### Timeout tracking
|
826
|
+
|
762
827
|
Timeouts are also reported:
|
763
828
|
|
764
829
|
```ruby
|
@@ -767,6 +832,30 @@ Timeouts are also reported:
|
|
767
832
|
|
768
833
|
All the dots in the host are getting replaced with underscore, because dot is the default separator in graphite.
|
769
834
|
|
835
|
+
###### Caching tracking
|
836
|
+
|
837
|
+
When you want to track caching stats please make sure you have enabled the `LHC::Caching` and the `LHC::Monitoring` interceptor.
|
838
|
+
|
839
|
+
Make sure that the `LHC::Caching` is listed before `LHC::Monitoring` interceptor when configuring interceptors:
|
840
|
+
|
841
|
+
```ruby
|
842
|
+
LHC.configure do |c|
|
843
|
+
c.interceptors = [LHC::Caching, LHC::Monitoring]
|
844
|
+
end
|
845
|
+
```
|
846
|
+
|
847
|
+
If a response was served from cache it tracks:
|
848
|
+
|
849
|
+
```ruby
|
850
|
+
"lhc.<app_name>.<env>.<host>.<http_method>.cache.hit", 1
|
851
|
+
```
|
852
|
+
|
853
|
+
If a response was not served from cache it tracks:
|
854
|
+
|
855
|
+
```ruby
|
856
|
+
"lhc.<app_name>.<env>.<host>.<http_method>.cache.miss", 1
|
857
|
+
```
|
858
|
+
|
770
859
|
##### Configure
|
771
860
|
|
772
861
|
It is possible to set the key for Monitoring Interceptor on per request basis:
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
begin
|
2
4
|
require 'bundler/setup'
|
3
5
|
rescue LoadError
|
@@ -17,9 +19,7 @@ end
|
|
17
19
|
begin
|
18
20
|
require 'rspec/core/rake_task'
|
19
21
|
RSpec::Core::RakeTask.new(:spec)
|
20
|
-
task :
|
21
|
-
rescue LoadError
|
22
|
-
# no rspec available
|
22
|
+
task default: :spec
|
23
23
|
end
|
24
24
|
|
25
25
|
Bundler::GemHelper.install_tasks
|
data/lhc.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.require_paths = ['lib']
|
21
21
|
|
22
22
|
s.requirements << 'Ruby >= 2.0.0'
|
23
|
+
s.required_ruby_version = '>= 2.7' # Needed for rubocop
|
23
24
|
|
24
25
|
s.add_dependency 'activesupport', '>= 5.2'
|
25
26
|
s.add_dependency 'addressable'
|
@@ -31,7 +32,8 @@ Gem::Specification.new do |s|
|
|
31
32
|
s.add_development_dependency 'rails', '>= 5.2'
|
32
33
|
s.add_development_dependency 'redis'
|
33
34
|
s.add_development_dependency 'rspec-rails', '>= 3.0.0'
|
34
|
-
s.add_development_dependency 'rubocop', '~> 0
|
35
|
+
s.add_development_dependency 'rubocop', '~> 1.0'
|
36
|
+
s.add_development_dependency 'rubocop-performance', '~> 1.0'
|
35
37
|
s.add_development_dependency 'rubocop-rspec', '~> 1.26.0'
|
36
38
|
s.add_development_dependency 'timecop'
|
37
39
|
s.add_development_dependency 'webmock'
|
data/lib/lhc.rb
CHANGED
@@ -6,131 +6,142 @@ require 'active_support/core_ext/hash/keys'
|
|
6
6
|
|
7
7
|
module LHC
|
8
8
|
autoload :BasicMethodsConcern,
|
9
|
-
|
9
|
+
'lhc/concerns/lhc/basic_methods_concern'
|
10
10
|
autoload :ConfigurationConcern,
|
11
|
-
|
11
|
+
'lhc/concerns/lhc/configuration_concern'
|
12
12
|
autoload :FixInvalidEncodingConcern,
|
13
|
-
|
13
|
+
'lhc/concerns/lhc/fix_invalid_encoding_concern'
|
14
14
|
autoload :FormatsConcern,
|
15
|
-
|
15
|
+
'lhc/concerns/lhc/formats_concern'
|
16
16
|
|
17
17
|
include BasicMethodsConcern
|
18
18
|
include ConfigurationConcern
|
19
19
|
include FormatsConcern
|
20
20
|
|
21
21
|
autoload :Auth,
|
22
|
-
|
22
|
+
'lhc/interceptors/auth'
|
23
23
|
autoload :Caching,
|
24
|
-
|
24
|
+
'lhc/interceptors/caching'
|
25
25
|
autoload :DefaultTimeout,
|
26
|
-
|
26
|
+
'lhc/interceptors/default_timeout'
|
27
27
|
autoload :Logging,
|
28
|
-
|
28
|
+
'lhc/interceptors/logging'
|
29
29
|
autoload :Prometheus,
|
30
|
-
|
30
|
+
'lhc/interceptors/prometheus'
|
31
31
|
autoload :Retry,
|
32
|
-
|
32
|
+
'lhc/interceptors/retry'
|
33
33
|
autoload :Throttle,
|
34
|
-
|
34
|
+
'lhc/interceptors/throttle'
|
35
35
|
|
36
36
|
autoload :Config,
|
37
|
-
|
37
|
+
'lhc/config'
|
38
38
|
autoload :Endpoint,
|
39
|
-
|
39
|
+
'lhc/endpoint'
|
40
40
|
|
41
41
|
autoload :Error,
|
42
|
-
|
42
|
+
'lhc/error'
|
43
43
|
autoload :ClientError,
|
44
|
-
|
44
|
+
'lhc/errors/client_error'
|
45
45
|
autoload :BadRequest,
|
46
|
-
|
46
|
+
'lhc/errors/client_error'
|
47
47
|
autoload :Unauthorized,
|
48
|
-
|
48
|
+
'lhc/errors/client_error'
|
49
49
|
autoload :PaymentRequired,
|
50
|
-
|
50
|
+
'lhc/errors/client_error'
|
51
51
|
autoload :Forbidden,
|
52
|
-
|
52
|
+
'lhc/errors/client_error'
|
53
53
|
autoload :Forbidden,
|
54
|
-
|
54
|
+
'lhc/errors/client_error'
|
55
55
|
autoload :NotFound,
|
56
|
-
|
56
|
+
'lhc/errors/client_error'
|
57
57
|
autoload :MethodNotAllowed,
|
58
|
-
|
58
|
+
'lhc/errors/client_error'
|
59
59
|
autoload :NotAcceptable,
|
60
|
-
|
60
|
+
'lhc/errors/client_error'
|
61
61
|
autoload :ProxyAuthenticationRequired,
|
62
|
-
|
62
|
+
'lhc/errors/client_error'
|
63
63
|
autoload :RequestTimeout,
|
64
|
-
|
64
|
+
'lhc/errors/client_error'
|
65
65
|
autoload :Conflict,
|
66
|
-
|
66
|
+
'lhc/errors/client_error'
|
67
67
|
autoload :Gone,
|
68
|
-
|
68
|
+
'lhc/errors/client_error'
|
69
69
|
autoload :LengthRequired,
|
70
|
-
|
70
|
+
'lhc/errors/client_error'
|
71
71
|
autoload :PreconditionFailed,
|
72
|
-
|
72
|
+
'lhc/errors/client_error'
|
73
73
|
autoload :RequestEntityTooLarge,
|
74
|
-
|
74
|
+
'lhc/errors/client_error'
|
75
75
|
autoload :RequestUriToLong,
|
76
|
-
|
76
|
+
'lhc/errors/client_error'
|
77
77
|
autoload :UnsupportedMediaType,
|
78
|
-
|
78
|
+
'lhc/errors/client_error'
|
79
79
|
autoload :RequestedRangeNotSatisfiable,
|
80
|
-
|
80
|
+
'lhc/errors/client_error'
|
81
81
|
autoload :ExpectationFailed,
|
82
|
-
|
82
|
+
'lhc/errors/client_error'
|
83
83
|
autoload :UnprocessableEntity,
|
84
|
-
|
84
|
+
'lhc/errors/client_error'
|
85
85
|
autoload :Locked,
|
86
|
-
|
86
|
+
'lhc/errors/client_error'
|
87
87
|
autoload :FailedDependency,
|
88
|
-
|
88
|
+
'lhc/errors/client_error'
|
89
89
|
autoload :UpgradeRequired,
|
90
|
-
|
90
|
+
'lhc/errors/client_error'
|
91
91
|
autoload :ParserError,
|
92
|
-
|
92
|
+
'lhc/errors/parser_error'
|
93
93
|
autoload :ServerError,
|
94
|
-
|
94
|
+
'lhc/errors/server_error'
|
95
95
|
autoload :InternalServerError,
|
96
|
-
|
96
|
+
'lhc/errors/server_error'
|
97
97
|
autoload :NotImplemented,
|
98
|
-
|
98
|
+
'lhc/errors/server_error'
|
99
99
|
autoload :BadGateway,
|
100
|
-
|
100
|
+
'lhc/errors/server_error'
|
101
101
|
autoload :ServiceUnavailable,
|
102
|
-
|
102
|
+
'lhc/errors/server_error'
|
103
103
|
autoload :GatewayTimeout,
|
104
|
-
|
104
|
+
'lhc/errors/server_error'
|
105
105
|
autoload :HttpVersionNotSupported,
|
106
|
-
|
106
|
+
'lhc/errors/server_error'
|
107
107
|
autoload :InsufficientStorage,
|
108
|
-
|
108
|
+
'lhc/errors/server_error'
|
109
109
|
autoload :NotExtended,
|
110
|
-
|
110
|
+
'lhc/errors/server_error'
|
111
111
|
autoload :Timeout,
|
112
|
-
|
112
|
+
'lhc/errors/timeout'
|
113
113
|
autoload :UnknownError,
|
114
|
-
|
114
|
+
'lhc/errors/unknown_error'
|
115
|
+
|
116
|
+
autoload :Scrubber,
|
117
|
+
'lhc/scrubber'
|
118
|
+
autoload :AuthScrubber,
|
119
|
+
'lhc/scrubbers/auth_scrubber'
|
120
|
+
autoload :BodyScrubber,
|
121
|
+
'lhc/scrubbers/body_scrubber'
|
122
|
+
autoload :HeadersScrubber,
|
123
|
+
'lhc/scrubbers/headers_scrubber'
|
124
|
+
autoload :ParamsScrubber,
|
125
|
+
'lhc/scrubbers/params_scrubber'
|
115
126
|
|
116
127
|
autoload :Interceptor,
|
117
|
-
|
128
|
+
'lhc/interceptor'
|
118
129
|
autoload :Interceptors,
|
119
|
-
|
130
|
+
'lhc/interceptors'
|
120
131
|
autoload :Formats,
|
121
|
-
|
132
|
+
'lhc/formats'
|
122
133
|
autoload :Format,
|
123
|
-
|
134
|
+
'lhc/format'
|
124
135
|
autoload :Monitoring,
|
125
|
-
|
136
|
+
'lhc/interceptors/monitoring'
|
126
137
|
autoload :Request,
|
127
|
-
|
138
|
+
'lhc/request'
|
128
139
|
autoload :Response,
|
129
|
-
|
140
|
+
'lhc/response'
|
130
141
|
autoload :Rollbar,
|
131
|
-
|
142
|
+
'lhc/interceptors/rollbar'
|
132
143
|
autoload :Zipkin,
|
133
|
-
|
144
|
+
'lhc/interceptors/zipkin'
|
134
145
|
|
135
146
|
require 'lhc/railtie' if defined?(Rails)
|
136
147
|
end
|
data/lib/lhc/config.rb
CHANGED
@@ -5,14 +5,18 @@ require 'singleton'
|
|
5
5
|
class LHC::Config
|
6
6
|
include Singleton
|
7
7
|
|
8
|
+
attr_accessor :scrubs
|
9
|
+
|
8
10
|
def initialize
|
9
11
|
@endpoints = {}
|
10
12
|
@placeholders = {}
|
13
|
+
@scrubs = default_scrubs
|
11
14
|
end
|
12
15
|
|
13
16
|
def endpoint(name, url, options = {})
|
14
17
|
name = name.to_sym
|
15
18
|
raise 'Endpoint already exists for that name' if @endpoints[name]
|
19
|
+
|
16
20
|
@endpoints[name] = LHC::Endpoint.new(url, options)
|
17
21
|
end
|
18
22
|
|
@@ -23,6 +27,7 @@ class LHC::Config
|
|
23
27
|
def placeholder(name, value)
|
24
28
|
name = name.to_sym
|
25
29
|
raise 'Placeholder already exists for that name' if @placeholders[name]
|
30
|
+
|
26
31
|
@placeholders[name] = value
|
27
32
|
end
|
28
33
|
|
@@ -36,12 +41,23 @@ class LHC::Config
|
|
36
41
|
|
37
42
|
def interceptors=(interceptors)
|
38
43
|
raise 'Default interceptors already set and can only be set once' if @interceptors
|
44
|
+
|
39
45
|
@interceptors = interceptors
|
40
46
|
end
|
41
47
|
|
48
|
+
def default_scrubs
|
49
|
+
{
|
50
|
+
auth: [:bearer, :basic],
|
51
|
+
params: [],
|
52
|
+
headers: [],
|
53
|
+
body: ['password', 'password_confirmation']
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
42
57
|
def reset
|
43
58
|
@endpoints = {}
|
44
59
|
@placeholders = {}
|
45
60
|
@interceptors = nil
|
61
|
+
@scrubs = default_scrubs
|
46
62
|
end
|
47
63
|
end
|
data/lib/lhc/endpoint.rb
CHANGED
@@ -55,6 +55,7 @@ class LHC::Endpoint
|
|
55
55
|
# Example: {+datastore}/contracts/{id} == http://local.ch/contracts/1
|
56
56
|
def match?(url)
|
57
57
|
return true if url == uri.pattern
|
58
|
+
|
58
59
|
match_data = match_data(url)
|
59
60
|
return false if match_data.nil?
|
60
61
|
|
@@ -75,6 +76,7 @@ class LHC::Endpoint
|
|
75
76
|
def values_as_params(url)
|
76
77
|
match_data = match_data(url)
|
77
78
|
return if match_data.nil?
|
79
|
+
|
78
80
|
Hash[match_data.variables.map(&:to_sym).zip(match_data.values)]
|
79
81
|
end
|
80
82
|
|
@@ -103,6 +105,7 @@ class LHC::Endpoint
|
|
103
105
|
# creates params according to template
|
104
106
|
def self.values_as_params(template, url)
|
105
107
|
raise("#{url} does not match the template: #{template}") if !match?(url, template)
|
108
|
+
|
106
109
|
new(template).values_as_params(url)
|
107
110
|
end
|
108
111
|
|