opentelemetry-api 0.2.0 → 0.3.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +9 -0
  3. data/OVERVIEW.md +66 -0
  4. data/lib/{opentelemetry/distributed_context/manager.rb → opentelemetry-api.rb} +1 -6
  5. data/lib/opentelemetry.rb +34 -16
  6. data/lib/opentelemetry/context.rb +138 -15
  7. data/lib/opentelemetry/context/key.rb +29 -0
  8. data/lib/opentelemetry/context/propagation.rb +22 -0
  9. data/lib/opentelemetry/context/propagation/composite_propagator.rb +77 -0
  10. data/lib/opentelemetry/context/propagation/default_getter.rb +26 -0
  11. data/lib/opentelemetry/context/propagation/default_setter.rb +26 -0
  12. data/lib/opentelemetry/context/propagation/noop_extractor.rb +26 -0
  13. data/lib/opentelemetry/context/propagation/noop_injector.rb +26 -0
  14. data/lib/opentelemetry/context/propagation/propagation.rb +27 -0
  15. data/lib/opentelemetry/context/propagation/propagator.rb +64 -0
  16. data/lib/opentelemetry/correlation_context.rb +16 -0
  17. data/lib/opentelemetry/correlation_context/builder.rb +18 -0
  18. data/lib/opentelemetry/correlation_context/manager.rb +36 -0
  19. data/lib/opentelemetry/correlation_context/propagation.rb +57 -0
  20. data/lib/opentelemetry/correlation_context/propagation/context_keys.rb +27 -0
  21. data/lib/opentelemetry/correlation_context/propagation/text_extractor.rb +60 -0
  22. data/lib/opentelemetry/correlation_context/propagation/text_injector.rb +55 -0
  23. data/lib/opentelemetry/instrumentation.rb +15 -0
  24. data/lib/opentelemetry/instrumentation/adapter.rb +244 -0
  25. data/lib/opentelemetry/instrumentation/registry.rb +87 -0
  26. data/lib/opentelemetry/metrics.rb +1 -1
  27. data/lib/opentelemetry/metrics/handles.rb +5 -15
  28. data/lib/opentelemetry/metrics/instruments.rb +18 -69
  29. data/lib/opentelemetry/metrics/meter.rb +2 -39
  30. data/lib/opentelemetry/metrics/{meter_factory.rb → meter_provider.rb} +2 -2
  31. data/lib/opentelemetry/trace.rb +2 -2
  32. data/lib/opentelemetry/trace/event.rb +4 -3
  33. data/lib/opentelemetry/trace/link.rb +4 -3
  34. data/lib/opentelemetry/trace/propagation.rb +17 -0
  35. data/lib/opentelemetry/trace/propagation/context_keys.rb +35 -0
  36. data/lib/opentelemetry/trace/propagation/trace_context.rb +59 -0
  37. data/lib/opentelemetry/trace/propagation/trace_context/text_extractor.rb +58 -0
  38. data/lib/opentelemetry/trace/propagation/trace_context/text_injector.rb +55 -0
  39. data/lib/opentelemetry/trace/propagation/trace_context/trace_parent.rb +126 -0
  40. data/lib/opentelemetry/trace/span.rb +14 -6
  41. data/lib/opentelemetry/trace/status.rb +7 -2
  42. data/lib/opentelemetry/trace/tracer.rb +47 -13
  43. data/lib/opentelemetry/trace/tracer_provider.rb +22 -0
  44. data/lib/opentelemetry/trace/util/http_to_status.rb +47 -0
  45. data/lib/opentelemetry/version.rb +1 -1
  46. metadata +33 -13
  47. data/lib/opentelemetry/distributed_context.rb +0 -19
  48. data/lib/opentelemetry/distributed_context/distributed_context.rb +0 -24
  49. data/lib/opentelemetry/distributed_context/entry.rb +0 -66
  50. data/lib/opentelemetry/distributed_context/propagation.rb +0 -19
  51. data/lib/opentelemetry/distributed_context/propagation/binary_format.rb +0 -26
  52. data/lib/opentelemetry/distributed_context/propagation/text_format.rb +0 -76
  53. data/lib/opentelemetry/distributed_context/propagation/trace_parent.rb +0 -124
  54. data/lib/opentelemetry/trace/sampling_hint.rb +0 -22
  55. data/lib/opentelemetry/trace/tracer_factory.rb +0 -45
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ require 'opentelemetry/instrumentation/registry'
8
+ require 'opentelemetry/instrumentation/adapter'
9
+
10
+ module OpenTelemetry
11
+ # The instrumentation module contains functionality to register and install
12
+ # instrumentation adapters
13
+ module Instrumentation
14
+ end
15
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module Instrumentation
9
+ # The Adapter class holds all metadata and configuration for an
10
+ # instrumentation adapter. All instrumentation adapter packages should
11
+ # include a subclass of +Instrumentation::Adapter+ that will register
12
+ # it with +OpenTelemetry.instrumentation_registry+ and make it available for
13
+ # discovery and installation by an SDK.
14
+ #
15
+ # A typical subclass of Adapter will provide an install block, a present
16
+ # block, and possibly a compatible block. Below is an
17
+ # example:
18
+ #
19
+ # module OpenTelemetry
20
+ # module Adapters
21
+ # module Sinatra
22
+ # class Adapter < OpenTelemetry::Instrumentation::Adapter
23
+ # install do |config|
24
+ # # install instrumentation, either by library hook or applying
25
+ # # a monkey patch
26
+ # end
27
+ #
28
+ # # determine if the target library is present
29
+ # present do
30
+ # defined?(::Sinatra)
31
+ # end
32
+ #
33
+ # # if the target library is present, is it compatible?
34
+ # compatible do
35
+ # Gem.loaded_specs['sinatra'].version > MIN_VERSION
36
+ # end
37
+ # end
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # The adapter name and version will be inferred from the namespace of the
43
+ # class. In this example, they'd be 'OpenTelemetry::Adapters::Sinatra' and
44
+ # OpenTelemetry::Adapters::Sinatra::VERSION, but can be explicitly set using
45
+ # the +adapter_name+ and +adapter_version+ methods if necessary.
46
+ #
47
+ # All subclasses of OpenTelemetry::Instrumentation::Adapter are automatically
48
+ # registered with OpenTelemetry.instrumentation_registry which is used by
49
+ # SDKs for instrumentation discovery and installation.
50
+ #
51
+ # Instrumentation libraries can use the adapter subclass to easily gain
52
+ # a reference to its named tracer. For example:
53
+ #
54
+ # OpenTelemetry::Adapters::Sinatra.instance.tracer
55
+ #
56
+ # The adapter class establishes a convention for disabling an adapter
57
+ # by environment variable and local configuration. An adapter disabled
58
+ # by environment variable will take precedence over local config. The
59
+ # convention for environment variable name is the library name, upcased with
60
+ # '::' replaced by underscores, and '_ENABLED' appended. For example:
61
+ # OPENTELEMETRY_ADAPTERS_SINATRA_ENABLED = false.
62
+ class Adapter
63
+ class << self
64
+ NAME_REGEX = /^(?:(?<namespace>[a-zA-Z0-9_:]+):{2})?(?<classname>[a-zA-Z0-9_]+)$/.freeze
65
+ private_constant :NAME_REGEX
66
+
67
+ private :new # rubocop:disable Style/AccessModifierDeclarations
68
+
69
+ def inherited(subclass)
70
+ OpenTelemetry.instrumentation_registry.register(subclass)
71
+ end
72
+
73
+ # Optionally set the name of this instrumentation adapter. If not
74
+ # explicitly set, the name will default to the namespace of the class,
75
+ # or the class name if it does not have a namespace. If there is not
76
+ # a namespace, or a class name, it will default to 'unknown'.
77
+ #
78
+ # @param [String] adapter_name The full name of the adapter package
79
+ def adapter_name(adapter_name = nil)
80
+ if adapter_name
81
+ @adapter_name = adapter_name
82
+ else
83
+ @adapter_name ||= infer_name || 'unknown'
84
+ end
85
+ end
86
+
87
+ # Optionally set the version of this adapter. If not explicitly set,
88
+ # the version will default to the VERSION constant under namespace of
89
+ # the class, or the VERSION constant under the class name if it does not
90
+ # have a namespace. If a VERSION constant cannot be found, it defaults
91
+ # to '0.0.0'.
92
+ #
93
+ # @param [String] adapter_version The version of the adapter package
94
+ def adapter_version(adapter_version = nil)
95
+ if adapter_version
96
+ @adapter_version = adapter_version
97
+ else
98
+ @adapter_version ||= infer_version || '0.0.0'
99
+ end
100
+ end
101
+
102
+ # The install block for this adapter. This will be where you install
103
+ # instrumentation, either by framework hook or applying a monkey patch.
104
+ #
105
+ # @param [Callable] blk The install block for this adapter
106
+ # @yieldparam [Hash] config The adapter config will be yielded to the
107
+ # install block
108
+ def install(&blk)
109
+ @install_blk = blk
110
+ end
111
+
112
+ # The present block for this adapter. This block is used to detect if
113
+ # target library is present on the system. Typically this will involve
114
+ # checking to see if the target gem spec was loaded or if expected
115
+ # constants from the target library are present.
116
+ #
117
+ # @param [Callable] blk The present block for this adapter
118
+ def present(&blk)
119
+ @present_blk = blk
120
+ end
121
+
122
+ # The compatible block for this adapter. This check will be run if the
123
+ # target library is present to determine if it's compatible. It's not
124
+ # required, but a common use case will be to check to target library
125
+ # version for compatibility.
126
+ #
127
+ # @param [Callable] blk The compatibility block for this adapter
128
+ def compatible(&blk)
129
+ @compatible_blk = blk
130
+ end
131
+
132
+ def instance
133
+ @instance ||= new(adapter_name, adapter_version, install_blk,
134
+ present_blk, compatible_blk)
135
+ end
136
+
137
+ private
138
+
139
+ attr_reader :install_blk, :present_blk, :compatible_blk
140
+
141
+ def infer_name
142
+ @inferred_name ||= if (md = name.match(NAME_REGEX)) # rubocop:disable Naming/MemoizedInstanceVariableName
143
+ md['namespace'] || md['classname']
144
+ end
145
+ end
146
+
147
+ def infer_version
148
+ return unless (inferred_name = infer_name)
149
+
150
+ mod = inferred_name.split('::').map(&:to_sym).inject(Object) do |object, const|
151
+ object.const_get(const)
152
+ end
153
+ mod.const_get(:VERSION)
154
+ rescue NameError
155
+ nil
156
+ end
157
+ end
158
+
159
+ attr_reader :name, :version, :config, :installed, :tracer
160
+
161
+ alias installed? installed
162
+
163
+ def initialize(name, version, install_blk, present_blk,
164
+ compatible_blk)
165
+ @name = name
166
+ @version = version
167
+ @install_blk = install_blk
168
+ @present_blk = present_blk
169
+ @compatible_blk = compatible_blk
170
+ @config = {}
171
+ @installed = false
172
+ end
173
+
174
+ # Install adapter with the given config. The present? and compatible?
175
+ # will be run first, and install will return false if either fail. Will
176
+ # return true if install was completed successfully.
177
+ #
178
+ # @param [Hash] config The config for this adapter
179
+ def install(config = {})
180
+ return true if installed?
181
+ return false unless installable?(config)
182
+
183
+ @config = config unless config.nil?
184
+ instance_exec(@config, &@install_blk)
185
+ @tracer ||= OpenTelemetry.tracer_provider.tracer(name, version)
186
+ @installed = true
187
+ end
188
+
189
+ # Whether or not this adapter is installable in the current process. Will
190
+ # be true when the adapter defines an install block, is not disabled
191
+ # by environment or config, and the target library present and compatible.
192
+ #
193
+ # @param [Hash] config The config for this adapter
194
+ def installable?(config = {})
195
+ @install_blk && enabled?(config) && present? && compatible?
196
+ end
197
+
198
+ # Calls the present block of the Adapter subclasses, if no block is provided
199
+ # it's assumed the adapter is not present
200
+ def present?
201
+ return false unless @present_blk
202
+
203
+ instance_exec(&@present_blk)
204
+ end
205
+
206
+ # Calls the compatible block of the Adapter subclasses, if no block is provided
207
+ # it's assumed to be compatible
208
+ def compatible?
209
+ return true unless @compatible_blk
210
+
211
+ instance_exec(&@compatible_blk)
212
+ end
213
+
214
+ # Whether this adapter is enabled. It first checks to see if it's enabled
215
+ # by an environment variable and will proceed to check if it's enabled
216
+ # by local config, if given.
217
+ #
218
+ # @param [optional Hash] config The local config
219
+ def enabled?(config = nil)
220
+ return false unless enabled_by_env_var?
221
+ return config[:enabled] if config&.key?(:enabled)
222
+
223
+ true
224
+ end
225
+
226
+ private
227
+
228
+ # Checks to see if this adapter is enabled by env var. By convention, the
229
+ # environment variable will be the adapter name upper cased, with '::'
230
+ # replaced by underscores and _ENABLED appended. For example, the
231
+ # environment variable name for OpenTelemetry::Adapter::Sinatra will be
232
+ # OPENTELEMETRY_ADAPTERS_SINATRA_ENABLED. A value of 'false' will disable
233
+ # the adapter, all other values will enable it.
234
+ def enabled_by_env_var?
235
+ var_name = name.dup.tap do |n|
236
+ n.upcase!
237
+ n.gsub!('::', '_')
238
+ n << '_ENABLED'
239
+ end
240
+ ENV[var_name] != 'false'
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2020 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module Instrumentation
9
+ # The instrumentation Registry contains information about instrumentation
10
+ # adapters available and facilitates discovery, installation and
11
+ # configuration. This functionality is primarily useful for SDK
12
+ # implementors.
13
+ class Registry
14
+ def initialize
15
+ @lock = Mutex.new
16
+ @adapters = []
17
+ end
18
+
19
+ # @api private
20
+ def register(adapter)
21
+ @lock.synchronize do
22
+ @adapters << adapter
23
+ end
24
+ end
25
+
26
+ # Lookup an adapter definition by name. Returns nil if +adapter_name+
27
+ # is not found.
28
+ #
29
+ # @param [String] adapter_name A stringified class name for an adapter
30
+ # @return [Adapter]
31
+ def lookup(adapter_name)
32
+ @lock.synchronize do
33
+ find_adapter(adapter_name)
34
+ end
35
+ end
36
+
37
+ # Install the specified adapters with optionally specified configuration.
38
+ #
39
+ # @param [Array<String>] adapter_names An array of adapter names to
40
+ # install
41
+ # @param [optional Hash<String, Hash>] adapter_config_map A map of
42
+ # adapter_name to config. This argument is optional and config can be
43
+ # passed for as many or as few adapters as desired.
44
+ def install(adapter_names, adapter_config_map = {})
45
+ @lock.synchronize do
46
+ adapter_names.each do |adapter_name|
47
+ adapter = find_adapter(adapter_name)
48
+ OpenTelemetry.logger.warn "Could not install #{adapter_name} because it was not found" unless adapter
49
+
50
+ install_adapter(adapter, adapter_config_map[adapter.name])
51
+ end
52
+ end
53
+ end
54
+
55
+ # Install all instrumentation available and installable in this process.
56
+ #
57
+ # @param [optional Hash<String, Hash>] adapter_config_map A map of
58
+ # adapter_name to config. This argument is optional and config can be
59
+ # passed for as many or as few adapters as desired.
60
+ def install_all(adapter_config_map = {})
61
+ @lock.synchronize do
62
+ @adapters.map(&:instance).each do |adapter|
63
+ install_adapter(adapter, adapter_config_map[adapter.name])
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def find_adapter(adapter_name)
71
+ @adapters.detect { |a| a.instance.name == adapter_name }
72
+ &.instance
73
+ end
74
+
75
+ def install_adapter(adapter, config)
76
+ if adapter.install(config)
77
+ OpenTelemetry.logger.info "Adapter: #{adapter.name} was successfully installed"
78
+ else
79
+ OpenTelemetry.logger.warn "Adapter: #{adapter.name} failed to install"
80
+ end
81
+ rescue => e # rubocop:disable Style/RescueStandardError
82
+ OpenTelemetry.logger.warn "Adapter: #{adapter.name} unhandled exception" \
83
+ "during install #{e}: #{e.backtrace}"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -7,7 +7,7 @@
7
7
  require 'opentelemetry/metrics/handles'
8
8
  require 'opentelemetry/metrics/instruments'
9
9
  require 'opentelemetry/metrics/meter'
10
- require 'opentelemetry/metrics/meter_factory'
10
+ require 'opentelemetry/metrics/meter_provider'
11
11
 
12
12
  module OpenTelemetry
13
13
  # The Metrics API allows reporting raw measurements as well as metrics with known aggregation and labels.
@@ -7,29 +7,19 @@
7
7
  module OpenTelemetry
8
8
  module Metrics
9
9
  # In situations where performance is a requirement and a metric is
10
- # repeatedly used with the same set of labels, the developer may elect to
10
+ # repeatedly used with the same labels, the developer may elect to
11
11
  # use instrument {Handles} as an optimization. For handles to be a benefit,
12
12
  # it requires that a specific instrument will be re-used with specific
13
- # labels. If an instrument will be used with the same label set more than
14
- # once, obtaining an instrument handle corresponding to the label set
13
+ # labels. If an instrument will be used with the same labels more than
14
+ # once, obtaining an instrument handle corresponding to the labels
15
15
  # ensures the highest performance available.
16
16
  #
17
- # To obtain a handle given an instrument and label set, use the #handle
18
- # method to return an interface that supports the #add, #set, or #record
17
+ # To obtain a handle given an instrument and labels, use the #handle
18
+ # method to return an interface that supports the #add or #record
19
19
  # method of the instrument in question.
20
20
  #
21
21
  # Instrument handles may consume SDK resources indefinitely.
22
22
  module Handles
23
- # A float gauge handle.
24
- class FloatGauge
25
- def set(value); end
26
- end
27
-
28
- # An integer gauge handle.
29
- class IntegerGauge
30
- def set(value); end
31
- end
32
-
33
23
  # A float counter handle.
34
24
  class FloatCounter
35
25
  def add(value); end
@@ -13,67 +13,19 @@ module OpenTelemetry
13
13
  # program to record observations about their behavior. Therefore, we use
14
14
  # "metric instrument" to refer to a program object, allocated through the
15
15
  # API, used for recording metrics. There are three distinct instruments in
16
- # the Metrics API, commonly known as Counters, Gauges, and Measures.
16
+ # the Metrics API, commonly known as Counters, Observers, and Measures.
17
17
  module Instruments
18
- # A float gauge instrument.
19
- class FloatGauge
20
- # Set the value of the gauge.
21
- #
22
- # @param [Float] value The value to set.
23
- # @param [optional LabelSet, Hash<String, String>] labels_or_label_set
24
- # A {LabelSet} returned from {Meter#labels} or a Hash of Strings.
25
- def set(value, labels_or_label_set = {}); end
26
-
27
- # Obtain a handle from the instrument and label set.
28
- #
29
- # @param [optional LabelSet, Hash<String, String>] labels_or_label_set
30
- # A {LabelSet} returned from {Meter#labels} or a Hash of Strings.
31
- # @return [Handles::FloatGauge]
32
- def handle(labels_or_label_set = {})
33
- Handles::FloatGauge.new
34
- end
35
-
36
- # Return a measurement to be recorded via {Meter#record_batch}.
37
- #
38
- # @param [Float] value
39
- # @return [Object, Measurement]
40
- def measurement(value)
41
- NOOP_MEASUREMENT
42
- end
43
- end
44
-
45
- # An integer gauge instrument.
46
- class IntegerGauge
47
- def set(value, labels_or_label_set = {}); end
48
-
49
- # Obtain a handle from the instrument and label set.
50
- #
51
- # @param [optional LabelSet, Hash<String, String>] labels_or_label_set
52
- # A {LabelSet} returned from {Meter#labels} or a Hash of Strings.
53
- # @return [Handles::IntegerGauge]
54
- def handle(labels_or_label_set = {})
55
- Handles::IntegerGauge.new
56
- end
57
-
58
- # Return a measurement to be recorded via {Meter#record_batch}.
59
- #
60
- # @param [Integer] value
61
- # @return [Object, Measurement]
62
- def measurement(value)
63
- NOOP_MEASUREMENT
64
- end
65
- end
18
+ # TODO: Observers.
66
19
 
67
20
  # A float counter instrument.
68
21
  class FloatCounter
69
- def add(value, labels_or_label_set = {}); end
22
+ def add(value, labels = {}); end
70
23
 
71
- # Obtain a handle from the instrument and label set.
24
+ # Obtain a handle from the instrument and labels.
72
25
  #
73
- # @param [optional LabelSet, Hash<String, String>] labels_or_label_set
74
- # A {LabelSet} returned from {Meter#labels} or a Hash of Strings.
26
+ # @param [optional Hash<String, String>] labels A Hash of Strings.
75
27
  # @return [Handles::FloatCounter]
76
- def handle(labels_or_label_set = {})
28
+ def handle(labels = {})
77
29
  Handles::FloatCounter.new
78
30
  end
79
31
 
@@ -88,14 +40,13 @@ module OpenTelemetry
88
40
 
89
41
  # An integer counter instrument.
90
42
  class IntegerCounter
91
- def add(value, labels_or_label_set = {}); end
43
+ def add(value, labels = {}); end
92
44
 
93
- # Obtain a handle from the instrument and label set.
45
+ # Obtain a handle from the instrument and labels.
94
46
  #
95
- # @param [optional LabelSet, Hash<String, String>] labels_or_label_set
96
- # A {LabelSet} returned from {Meter#labels} or a Hash of Strings.
47
+ # @param [optional Hash<String, String>] labels A Hash of Strings.
97
48
  # @return [Handles::IntegerCounter]
98
- def handle(labels_or_label_set = {})
49
+ def handle(labels = {})
99
50
  Handles::IntegerCounter.new
100
51
  end
101
52
 
@@ -110,14 +61,13 @@ module OpenTelemetry
110
61
 
111
62
  # A float measure instrument.
112
63
  class FloatMeasure
113
- def record(value, labels_or_label_set = {}); end
64
+ def record(value, labels = {}); end
114
65
 
115
- # Obtain a handle from the instrument and label set.
66
+ # Obtain a handle from the instrument and labels.
116
67
  #
117
- # @param [optional LabelSet, Hash<String, String>] labels_or_label_set
118
- # A {LabelSet} returned from {Meter#labels} or a Hash of Strings.
68
+ # @param [optional Hash<String, String>] labels A Hash of Strings.
119
69
  # @return [Handles::FloatMeasure]
120
- def handle(labels_or_label_set = {})
70
+ def handle(labels = {})
121
71
  Handles::FloatMeasure.new
122
72
  end
123
73
 
@@ -132,14 +82,13 @@ module OpenTelemetry
132
82
 
133
83
  # An integer measure instrument.
134
84
  class IntegerMeasure
135
- def record(value, labels_or_label_set = {}); end
85
+ def record(value, labels = {}); end
136
86
 
137
- # Obtain a handle from the instrument and label set.
87
+ # Obtain a handle from the instrument and labels.
138
88
  #
139
- # @param [optional LabelSet, Hash<String, String>] labels_or_label_set
140
- # A {LabelSet} returned from {Meter#labels} or a Hash of Strings.
89
+ # @param [optional Hash<String, String>] labels A Hash of Strings.
141
90
  # @return [Handles::IntegerMeasure]
142
- def handle(labels_or_label_set = {})
91
+ def handle(labels = {})
143
92
  Handles::IntegerMeasure.new
144
93
  end
145
94