aspecto-opentelemetry 0.1.5 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +14 -8
- data/lib/aspecto/opentelemetry/config/remote_config.rb +29 -15
- data/lib/aspecto/opentelemetry/configurator.rb +17 -2
- data/lib/aspecto/opentelemetry/propagator/aspecto.rb +30 -0
- data/lib/aspecto/opentelemetry/sampler/message_process_sampler.rb +1 -1
- data/lib/aspecto/opentelemetry/version.rb +1 -1
- data/lib/aspecto/opentelemetry.rb +12 -2
- metadata +19 -19
- data/lib/aspecto/opentelemetry/resource/detectors/deployment.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6dc0881d253f1a91182a09ae64a3464bc935b63744ea68654011bb317b1278b
|
4
|
+
data.tar.gz: b42327874bf790cfa4d4fbcea31f84a4bd9a8e31cf94362329f25c195202ec4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da5bc77255107ff1236861ab1ffcf61a98e27af86ad9a12787fa03f6d8d6ef3f32a7bb1de1032a717f0f51c647f59942924fd162e507c039d785f6b0272ea905
|
7
|
+
data.tar.gz: 905b157c77e02bb808ca5c4e5a2a644156b79be3fd8d39b2f8ebbc2a32c4cb7651d6ef4c6dec5b041d37ef574a05085a1d03dbc21f165ca5b32a217c613f6482
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Aspecto::OpenTelemetry
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/aspecto-opentelemetry.svg)](https://badge.fury.io/rb/aspecto-opentelemetry)
|
4
|
+
|
3
5
|
Aspecto's SDK for ruby.
|
4
6
|
This gem is a distribution of OpenTelemetry pre-configured to use all available instrumentations and export trace data to Aspecto.
|
5
7
|
|
@@ -97,14 +99,18 @@ The only required config options are [`aspecto_auth`](https://app.aspecto.io/app
|
|
97
99
|
|
98
100
|
### Configuration Options
|
99
101
|
|
100
|
-
| Option Name
|
101
|
-
|
|
102
|
-
| `aspecto_auth`
|
103
|
-
| `service_name`
|
104
|
-
| `env`
|
105
|
-
| `log_level`
|
106
|
-
| `sampling_ratio`
|
107
|
-
| `otel_exporter_otlp_traces_endpoint` | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | URL
|
102
|
+
| Option Name | Environment Variable | Type | Default | Description |
|
103
|
+
| --- | --- | --- | --- | --- |
|
104
|
+
| `aspecto_auth` | `ASPECTO_AUTH` | UUID string | - | Aspecto's [API key](https://app.aspecto.io/app/integration/api-key) for authentication |
|
105
|
+
| `service_name` | `OTEL_SERVICE_NAME` | string | - | Name of the service which is sending telemetry|
|
106
|
+
| `env` | `ASPECTO_ENV` | string | Extracted from Rails or Sinatra if used | Deployment environment: `production` / `staging` / `development`, etc. |
|
107
|
+
| `log_level` | `OTEL_LOG_LEVEL` | string | `ERROR` | `ERROR` / `WARN` / `INFO`, etc. |
|
108
|
+
| `sampling_ratio` | `ASPECTO_SAMPLING_RATIO` | float | 1.0| How many of the traces starting in this service should be sampled. set to number in range [0.0, 1.0] where 0.0 is no sampling, and 1.0 is sample all |
|
109
|
+
| `otel_exporter_otlp_traces_endpoint` | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | URL | `https://otelcol.aspecto.io/v1/trace` | Url |
|
110
|
+
| `require_config_for_traces` | `ASPECTO_REQUIRE_CONFIG_FOR_TRACES` | boolean | `false` | When `true`, the SDK will not trace anything until remote sampling configuration arrives (few hundreds ms). Can be used to enforce sampling configuration is always applied, with the cost of losing traces generated during service startup. |
|
111
|
+
| `extract_b3_context` | `ASPECTO_EXTRACT_B3_CONTEXT` | boolean | `false` | Set to `true` when the service receives requests from another instrumented component that propagate context via B3 protocol multi or single header. For example: Envoy Proxy, Ambassador and Istio |
|
112
|
+
| `inject_b3_context_single_header` | `ASPECTO_INJECT_B3_CONTEXT_SINGLE_HEADER` | boolean | `false` | Set to `true` when the service send traffic to another instrumented component that propagate context via B3 **single header** protocol |
|
113
|
+
| `inject_b3_context_multi_header` | `ASPECTO_INJECT_B3_CONTEXT_MULTI_HEADER` | boolean | `false` | Set to `true` when the service send traffic to another instrumented component that propagate context via B3 **multi header** protocol. For example: Envoy Proxy, Istio |
|
108
114
|
|
109
115
|
## Contributing
|
110
116
|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "faraday"
|
4
|
-
require "faraday_middleware"
|
5
3
|
require "rufus/scheduler"
|
4
|
+
require "net/http"
|
5
|
+
require "json"
|
6
6
|
|
7
7
|
require_relative "../sampler/rules_sampler"
|
8
8
|
require_relative "../sampler/message_process_sampler"
|
@@ -17,12 +17,9 @@ module Aspecto
|
|
17
17
|
@service_name = service_name
|
18
18
|
@env = env
|
19
19
|
@fallback_sampler = fallback_sampler
|
20
|
-
|
21
|
-
aspecto_config_url =
|
22
|
-
|
23
|
-
f.response :json # decode response bodies as JSON
|
24
|
-
end
|
25
|
-
@http_client.options.timeout = 10
|
20
|
+
aspecto_config_host = ENV.fetch("ASPECTO_CONFIG_HOST", "https://config.aspecto.io")
|
21
|
+
@aspecto_config_url = URI("#{aspecto_config_host}/config/#{aspecto_auth}")
|
22
|
+
init_http_client
|
26
23
|
|
27
24
|
@scheduler = Rufus::Scheduler.new
|
28
25
|
@remote_config_poll_frequency = ENV.fetch("ASPECTO_REMOTE_CONFIG_POLL_FREQUENCY", "30s")
|
@@ -39,19 +36,36 @@ module Aspecto
|
|
39
36
|
|
40
37
|
private
|
41
38
|
|
39
|
+
def init_http_client
|
40
|
+
write_timeout_supported = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6")
|
41
|
+
|
42
|
+
@http_client = Net::HTTP.new(@aspecto_config_url.host, @aspecto_config_url.port)
|
43
|
+
@http_client.read_timeout = 10
|
44
|
+
@http_client.open_timeout = 10
|
45
|
+
@http_client.write_timeout = 10 if write_timeout_supported
|
46
|
+
@http_client.max_retries = 0
|
47
|
+
|
48
|
+
# use uri.scheme == 'https' instead
|
49
|
+
@http_client.use_ssl = true
|
50
|
+
@http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
51
|
+
end
|
52
|
+
|
42
53
|
def update_config # rubocop:disable Metrics/AbcSize
|
43
54
|
::OpenTelemetry::Common::Utilities.untraced do
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
return if response.status == 304
|
55
|
+
request = Net::HTTP::Get.new(@aspecto_config_url.path)
|
56
|
+
request["If-None-Match"] = @latest_config_etag unless @latest_config_etag.nil?
|
57
|
+
response = @http_client.request(request)
|
58
|
+
response_code = response.code.to_i
|
49
59
|
|
50
|
-
if
|
60
|
+
return if response_code == 304
|
61
|
+
|
62
|
+
if response_code >= 400
|
51
63
|
::OpenTelemetry.logger.error("[Aspecto] error when trying to get remote config. will try again in #{@remote_config_poll_frequency}")
|
52
64
|
return
|
53
65
|
end
|
54
|
-
|
66
|
+
|
67
|
+
@latest_config_etag = response["etag"]
|
68
|
+
handle_new_config JSON.parse(response.body) if response_code < 300
|
55
69
|
end
|
56
70
|
rescue StandardError => e
|
57
71
|
::OpenTelemetry.logger.error "[Aspecto] updating remote config failed. using previous remote config"
|
@@ -4,7 +4,9 @@ module Aspecto
|
|
4
4
|
module OpenTelemetry
|
5
5
|
# Aspecto OpenTelemetry Distro Configurator
|
6
6
|
class Configurator
|
7
|
-
|
7
|
+
TRUTHY_VALUES = %w[1 T t true TRUE True].freeze
|
8
|
+
|
9
|
+
def initialize # rubocop:disable Metrics/AbcSize
|
8
10
|
# initialize config options from environment variables.
|
9
11
|
# they can later be overwritten with configurator attribute setters
|
10
12
|
# that have precedence over env
|
@@ -14,6 +16,12 @@ module Aspecto
|
|
14
16
|
self.env = ENV["ASPECTO_ENV"] if ENV["ASPECTO_ENV"]
|
15
17
|
self.sampling_ratio = Float(ENV.fetch("ASPECTO_SAMPLING_RATIO", 1.0))
|
16
18
|
self.otel_exporter_otlp_traces_endpoint = ENV.fetch("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "https://otelcol.aspecto.io/v1/trace")
|
19
|
+
self.require_config_for_traces = self.class.bool_env_variable "ASPECTO_REQUIRE_CONFIG_FOR_TRACES", false
|
20
|
+
|
21
|
+
# b3 propagattor
|
22
|
+
self.extract_b3_context = self.class.bool_env_variable "ASPECTO_EXTRACT_B3_CONTEXT", false
|
23
|
+
self.inject_b3_context_single_header = self.class.bool_env_variable "ASPECTO_INJECT_B3_CONTEXT_SINGLE_HEADER", false
|
24
|
+
self.inject_b3_context_multi_header = self.class.bool_env_variable "ASPECTO_INJECT_B3_CONTEXT_MULTI_HEADER", false
|
17
25
|
end
|
18
26
|
|
19
27
|
def service_name=(service_name)
|
@@ -24,7 +32,7 @@ module Aspecto
|
|
24
32
|
@override_resource_attributes[::OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT] = env
|
25
33
|
end
|
26
34
|
|
27
|
-
attr_accessor :sampling_ratio, :log_level, :otel_exporter_otlp_traces_endpoint
|
35
|
+
attr_accessor :sampling_ratio, :log_level, :otel_exporter_otlp_traces_endpoint, :require_config_for_traces, :extract_b3_context, :inject_b3_context_single_header, :inject_b3_context_multi_header
|
28
36
|
attr_reader :aspecto_auth
|
29
37
|
|
30
38
|
def aspecto_auth=(aspecto_auth)
|
@@ -35,6 +43,13 @@ module Aspecto
|
|
35
43
|
def config_override_resource
|
36
44
|
::OpenTelemetry::SDK::Resources::Resource.create(@override_resource_attributes)
|
37
45
|
end
|
46
|
+
|
47
|
+
def self.bool_env_variable(env_variable_name, default_value)
|
48
|
+
env_value = ENV[env_variable_name]
|
49
|
+
return default_value if env_value.nil?
|
50
|
+
|
51
|
+
TRUTHY_VALUES.include?(env_value.strip)
|
52
|
+
end
|
38
53
|
end
|
39
54
|
end
|
40
55
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry"
|
4
|
+
require "opentelemetry-propagator-b3"
|
5
|
+
|
6
|
+
module Aspecto
|
7
|
+
module OpenTelemetry
|
8
|
+
module Propagator
|
9
|
+
# Aspecto OpenTelemetry Propagator Configuration
|
10
|
+
module Aspecto
|
11
|
+
extend self
|
12
|
+
|
13
|
+
W3C_PROPAGATOR = ::OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator
|
14
|
+
B3_SINGLE_PROPAGATOR = ::OpenTelemetry::Propagator::B3::Single.text_map_propagator
|
15
|
+
B3_MULTI_PROPAGATOR = ::OpenTelemetry::Propagator::B3::Multi.text_map_propagator
|
16
|
+
|
17
|
+
def from_configurator(configurator)
|
18
|
+
injectors = [W3C_PROPAGATOR]
|
19
|
+
injectors.push(B3_SINGLE_PROPAGATOR) if configurator.inject_b3_context_single_header
|
20
|
+
injectors.push(B3_MULTI_PROPAGATOR) if configurator.inject_b3_context_multi_header
|
21
|
+
|
22
|
+
extractors = [W3C_PROPAGATOR]
|
23
|
+
extractors.push(B3_SINGLE_PROPAGATOR, B3_MULTI_PROPAGATOR) if configurator.extract_b3_context
|
24
|
+
|
25
|
+
::OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose(injectors: injectors, extractors: extractors)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -2,14 +2,15 @@
|
|
2
2
|
|
3
3
|
require_relative "opentelemetry/version"
|
4
4
|
require_relative "opentelemetry/configurator"
|
5
|
+
require_relative "opentelemetry/propagator/aspecto"
|
5
6
|
require_relative "opentelemetry/resource/detectors/aspecto"
|
6
|
-
require_relative "opentelemetry/resource/detectors/deployment"
|
7
7
|
require_relative "opentelemetry/config/remote_config"
|
8
8
|
|
9
9
|
require "opentelemetry/sdk"
|
10
10
|
require "opentelemetry/exporter/otlp"
|
11
11
|
require "opentelemetry/instrumentation/all"
|
12
12
|
require "opentelemetry-instrumentation-aws_sdk"
|
13
|
+
require "opentelemetry-resource-detector-deployment"
|
13
14
|
|
14
15
|
module Aspecto
|
15
16
|
# Aspecto OpenTelemetry Distro
|
@@ -27,7 +28,7 @@ module Aspecto
|
|
27
28
|
::OpenTelemetry::SDK.configure do |c|
|
28
29
|
c.logger = Logger.new($stdout, level: configurator.log_level)
|
29
30
|
c.resource = Aspecto::OpenTelemetry::Resource::Detectors::Aspecto.detect
|
30
|
-
c.resource =
|
31
|
+
c.resource = ::OpenTelemetry::Resource::Detector::Deployment.detect
|
31
32
|
c.resource = configurator.config_override_resource # must be last
|
32
33
|
c.use_all "OpenTelemetry::Instrumentation::ActionPack" => { enable_recognize_route: true },
|
33
34
|
"OpenTelemetry::Instrumentation::AwsSdk" => {
|
@@ -50,6 +51,15 @@ module Aspecto
|
|
50
51
|
end
|
51
52
|
end
|
52
53
|
|
54
|
+
# Propagation
|
55
|
+
::OpenTelemetry.propagation = ::Aspecto::OpenTelemetry::Propagator::Aspecto.from_configurator configurator
|
56
|
+
|
57
|
+
# Sampling
|
58
|
+
if configurator.require_config_for_traces
|
59
|
+
::OpenTelemetry.logger.info "[Aspecto] Require config for traces. Applying ALWAYS_OFF sampler"
|
60
|
+
::OpenTelemetry.tracer_provider.sampler = ::OpenTelemetry::SDK::Trace::Samplers::ALWAYS_OFF
|
61
|
+
end
|
62
|
+
|
53
63
|
fallback_sampler = ::OpenTelemetry::SDK::Trace::Samplers.trace_id_ratio_based(configurator.sampling_ratio)
|
54
64
|
# TODO: how to properly extract the data from resource?
|
55
65
|
_, service_name = ::OpenTelemetry.tracer_provider.resource.attribute_enumerator.detect { |elem| elem[0] == ::OpenTelemetry::SemanticConventions::Resource::SERVICE_NAME }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aspecto-opentelemetry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aspecto
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aspecto-opentelemetry-instrumentation-aws_sdk
|
@@ -25,61 +25,61 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.1.8
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: opentelemetry-exporter-otlp
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - '='
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.21.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - '='
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.21.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: opentelemetry-instrumentation-all
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - '='
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.22.0
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - '='
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.22.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name: opentelemetry-
|
56
|
+
name: opentelemetry-propagator-b3
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - '='
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.
|
61
|
+
version: 0.19.2
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - '='
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 0.
|
68
|
+
version: 0.19.2
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name: opentelemetry-
|
70
|
+
name: opentelemetry-resource-detector-deployment
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - '='
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.
|
75
|
+
version: 0.0.1
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - '='
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.
|
82
|
+
version: 0.0.1
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: opentelemetry-sdk
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -195,8 +195,8 @@ files:
|
|
195
195
|
- lib/aspecto/opentelemetry.rb
|
196
196
|
- lib/aspecto/opentelemetry/config/remote_config.rb
|
197
197
|
- lib/aspecto/opentelemetry/configurator.rb
|
198
|
+
- lib/aspecto/opentelemetry/propagator/aspecto.rb
|
198
199
|
- lib/aspecto/opentelemetry/resource/detectors/aspecto.rb
|
199
|
-
- lib/aspecto/opentelemetry/resource/detectors/deployment.rb
|
200
200
|
- lib/aspecto/opentelemetry/sampler/condition.rb
|
201
201
|
- lib/aspecto/opentelemetry/sampler/message_process_sampler.rb
|
202
202
|
- lib/aspecto/opentelemetry/sampler/operator.rb
|
@@ -1,49 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Aspecto
|
4
|
-
module OpenTelemetry
|
5
|
-
module Resource
|
6
|
-
module Detectors
|
7
|
-
# Deployment contains detect class method for determining deployment resource attributes
|
8
|
-
module Deployment
|
9
|
-
extend self
|
10
|
-
|
11
|
-
def detect
|
12
|
-
resource_attributes = {}
|
13
|
-
deployment_environment = rails_env || sinatra_env || rack_env
|
14
|
-
resource_attributes[::OpenTelemetry::SemanticConventions::Resource::DEPLOYMENT_ENVIRONMENT] = deployment_environment if deployment_environment
|
15
|
-
::OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def rails_env
|
21
|
-
# rails extract env like this:
|
22
|
-
# https://github.com/rails/rails/blob/5647a9c1ced68d20338552d47a3b755e10a271c4/railties/lib/rails.rb#L74
|
23
|
-
# ActiveSupport::EnvironmentInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development")
|
24
|
-
::Rails.env.to_s if defined?(::Rails.env)
|
25
|
-
end
|
26
|
-
|
27
|
-
def rack_env
|
28
|
-
ENV["RACK_ENV"]
|
29
|
-
end
|
30
|
-
|
31
|
-
def sinatra_env
|
32
|
-
# https://github.com/sinatra/sinatra/blob/e69b6b9dee7165d3a583fc8a6af10ceee1ea687d/lib/sinatra/base.rb#L1801
|
33
|
-
# cases:
|
34
|
-
#
|
35
|
-
# 1. if sinatra is "require"d before the detector, then we return the value from the library
|
36
|
-
# this case will return the default "development" fallback if not env variable is set which is good.
|
37
|
-
#
|
38
|
-
# 2. if sinatra is "require"d after the detector, then:
|
39
|
-
# 2.1 if user is setting environment via 'APP_ENV' or 'RACK_ENV' then those value will be picked up and reported
|
40
|
-
# 2.2 else, the sinatra environment will fallback to "development", but detector will return nil for it.
|
41
|
-
# this issue is not covered, as when detector initialize the immutable resource, it has no way
|
42
|
-
# of knowing if "sinatra" will be required later or not.
|
43
|
-
(::Sinatra::Base.environment.to_s if defined?(::Sinatra::Base.environment)) || ENV["APP_ENV"]
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|