opentelemetry-sdk 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -0
  3. data/LICENSE +201 -0
  4. data/lib/opentelemetry/sdk.rb +18 -0
  5. data/lib/opentelemetry/sdk/internal.rb +32 -0
  6. data/lib/opentelemetry/sdk/resources.rb +15 -0
  7. data/lib/opentelemetry/sdk/resources/resource.rb +75 -0
  8. data/lib/opentelemetry/sdk/trace.rb +24 -0
  9. data/lib/opentelemetry/sdk/trace/config.rb +18 -0
  10. data/lib/opentelemetry/sdk/trace/config/trace_config.rb +77 -0
  11. data/lib/opentelemetry/sdk/trace/export.rb +35 -0
  12. data/lib/opentelemetry/sdk/trace/export/batch_span_processor.rb +149 -0
  13. data/lib/opentelemetry/sdk/trace/export/console_span_exporter.rb +40 -0
  14. data/lib/opentelemetry/sdk/trace/export/in_memory_span_exporter.rb +86 -0
  15. data/lib/opentelemetry/sdk/trace/export/multi_span_exporter.rb +64 -0
  16. data/lib/opentelemetry/sdk/trace/export/noop_span_exporter.rb +42 -0
  17. data/lib/opentelemetry/sdk/trace/export/simple_span_processor.rb +63 -0
  18. data/lib/opentelemetry/sdk/trace/multi_span_processor.rb +51 -0
  19. data/lib/opentelemetry/sdk/trace/noop_span_processor.rb +41 -0
  20. data/lib/opentelemetry/sdk/trace/samplers.rb +106 -0
  21. data/lib/opentelemetry/sdk/trace/samplers/decision.rb +26 -0
  22. data/lib/opentelemetry/sdk/trace/samplers/probability_sampler.rb +74 -0
  23. data/lib/opentelemetry/sdk/trace/samplers/result.rb +54 -0
  24. data/lib/opentelemetry/sdk/trace/span.rb +317 -0
  25. data/lib/opentelemetry/sdk/trace/span_data.rb +32 -0
  26. data/lib/opentelemetry/sdk/trace/tracer.rb +67 -0
  27. data/lib/opentelemetry/sdk/trace/tracer_factory.rb +84 -0
  28. data/lib/opentelemetry/sdk/version.rb +12 -0
  29. metadata +196 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module SDK
9
+ module Trace
10
+ # Implementation of the SpanProcessor duck type that simply forwards all
11
+ # received events to a list of SpanProcessors.
12
+ class MultiSpanProcessor
13
+ # Creates a new {MultiSpanProcessor}.
14
+ #
15
+ # @param [Enumerable<SpanProcessor>] span_processors a collection of
16
+ # SpanProcessors.
17
+ # @return [MultiSpanProcessor]
18
+ def initialize(span_processors)
19
+ @span_processors = span_processors.to_a.freeze
20
+ end
21
+
22
+ # Called when a {Span} is started, if the {Span#recording?}
23
+ # returns true.
24
+ #
25
+ # This method is called synchronously on the execution thread, should
26
+ # not throw or block the execution thread.
27
+ #
28
+ # @param [Span] span the {Span} that just started.
29
+ def on_start(span)
30
+ @span_processors.each { |processor| processor.on_start(span) }
31
+ end
32
+
33
+ # Called when a {Span} is ended, if the {Span#recording?}
34
+ # returns true.
35
+ #
36
+ # This method is called synchronously on the execution thread, should
37
+ # not throw or block the execution thread.
38
+ #
39
+ # @param [Span] span the {Span} that just ended.
40
+ def on_finish(span)
41
+ @span_processors.each { |processor| processor.on_finish(span) }
42
+ end
43
+
44
+ # Called when {TracerFactory#shutdown} is called.
45
+ def shutdown
46
+ @span_processors.each(&:shutdown)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ require 'singleton'
8
+
9
+ module OpenTelemetry
10
+ module SDK
11
+ module Trace
12
+ # NoopSpanProcessor is a singleton implementation of the duck type
13
+ # SpanProcessor that provides synchronous no-op hooks for when a
14
+ # {Span} is started or when a {Span} is ended.
15
+ class NoopSpanProcessor
16
+ include Singleton
17
+
18
+ # Called when a {Span} is started, if the {Span#recording?}
19
+ # returns true.
20
+ #
21
+ # This method is called synchronously on the execution thread, should
22
+ # not throw or block the execution thread.
23
+ #
24
+ # @param [Span] span the {Span} that just started.
25
+ def on_start(span); end
26
+
27
+ # Called when a {Span} is ended, if the {Span#recording?}
28
+ # returns true.
29
+ #
30
+ # This method is called synchronously on the execution thread, should
31
+ # not throw or block the execution thread.
32
+ #
33
+ # @param [Span] span the {Span} that just ended.
34
+ def on_finish(span); end
35
+
36
+ # Called when {TracerFactory#shutdown} is called.
37
+ def shutdown; end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ require 'opentelemetry/sdk/trace/samplers/decision'
8
+ require 'opentelemetry/sdk/trace/samplers/result'
9
+ require 'opentelemetry/sdk/trace/samplers/probability_sampler'
10
+
11
+ module OpenTelemetry
12
+ module SDK
13
+ module Trace
14
+ # The Samplers module contains the sampling logic for OpenTelemetry. The
15
+ # reference implementation provides a {ProbabilitySampler}, {ALWAYS_ON},
16
+ # {ALWAYS_OFF}, and {ALWAYS_PARENT}.
17
+ #
18
+ # Custom samplers can be provided by SDK users. The required interface is
19
+ # a callable with the signature:
20
+ #
21
+ # (trace_id:, span_id:, parent_context:, hint:, links:, name:, kind:, attributes:) -> Result
22
+ #
23
+ # Where:
24
+ #
25
+ # @param [String] trace_id The trace_id of the {Span} to be created.
26
+ # @param [String] span_id The span_id of the {Span} to be created.
27
+ # @param [OpenTelemetry::Trace::SpanContext] parent_context The
28
+ # {OpenTelemetry::Trace::SpanContext} of a parent span, typically
29
+ # extracted from the wire. Can be nil for a root span.
30
+ # @param [Symbol] hint A {OpenTelemetry::Trace::SamplingHint} about
31
+ # whether the {Span} should be sampled and/or record events.
32
+ # @param [Enumerable<Link>] links A collection of links to be associated
33
+ # with the {Span} to be created. Can be nil.
34
+ # @param [String] name Name of the {Span} to be created.
35
+ # @param [Symbol] kind The {OpenTelemetry::Trace::SpanKind} of the {Span}
36
+ # to be created. Can be nil.
37
+ # @param [Hash<String, Object>] attributes Attributes to be attached
38
+ # to the {Span} to be created. Can be nil.
39
+ # @return [Result] The sampling result.
40
+ module Samplers
41
+ RECORD_AND_SAMPLED = Result.new(decision: Decision::RECORD_AND_SAMPLED)
42
+ NOT_RECORD = Result.new(decision: Decision::NOT_RECORD)
43
+ RECORD = Result.new(decision: Decision::RECORD)
44
+ SAMPLING_HINTS = [Decision::NOT_RECORD, Decision::RECORD, Decision::RECORD_AND_SAMPLED].freeze
45
+ APPLY_PROBABILITY_TO_SYMBOLS = %i[root_spans root_spans_and_remote_parent all_spans].freeze
46
+
47
+ private_constant(:RECORD_AND_SAMPLED, :NOT_RECORD, :RECORD, :SAMPLING_HINTS, :APPLY_PROBABILITY_TO_SYMBOLS)
48
+
49
+ # rubocop:disable Lint/UnusedBlockArgument
50
+
51
+ # Ignores all values in hint and returns a {Result} with
52
+ # {Decision::RECORD_AND_SAMPLED}.
53
+ ALWAYS_ON = ->(trace_id:, span_id:, parent_context:, hint:, links:, name:, kind:, attributes:) { RECORD_AND_SAMPLED }
54
+
55
+ # Ignores all values in hint and returns a {Result} with
56
+ # {Decision::NOT_RECORD}.
57
+ ALWAYS_OFF = ->(trace_id:, span_id:, parent_context:, hint:, links:, name:, kind:, attributes:) { NOT_RECORD }
58
+
59
+ # Ignores all values in hint and returns a {Result} with
60
+ # {Decision::RECORD_AND_SAMPLED} if the parent context is sampled or
61
+ # {Decision::NOT_RECORD} otherwise, or if there is no parent context.
62
+ # rubocop:disable Style/Lambda
63
+ ALWAYS_PARENT = ->(trace_id:, span_id:, parent_context:, hint:, links:, name:, kind:, attributes:) do
64
+ if parent_context&.trace_flags&.sampled?
65
+ RECORD_AND_SAMPLED
66
+ else
67
+ NOT_RECORD
68
+ end
69
+ end
70
+ # rubocop:enable Style/Lambda
71
+ # rubocop:enable Lint/UnusedBlockArgument
72
+
73
+ # Returns a new sampler. The probability of sampling a trace is equal
74
+ # to that of the specified probability.
75
+ #
76
+ # @param [Numeric] probability The desired probability of sampling.
77
+ # Must be within [0.0, 1.0].
78
+ # @param [optional Enumerable<Symbol>] ignore_hints Sampling hints to
79
+ # ignore. Defaults to ignore {OpenTelemetry::Trace::SamplingHint::RECORD}.
80
+ # @param [optional Boolean] ignore_parent Whether to ignore parent
81
+ # sampling. Defaults to not ignore parent sampling.
82
+ # @param [optional Symbol] apply_probability_to Whether to apply
83
+ # probability sampling to root spans, root spans and remote parents,
84
+ # or all spans. Allowed values include :root_spans, :root_spans_and_remote_parent,
85
+ # and :all_spans. Defaults to :root_spans_and_remote_parent.
86
+ # @raise [ArgumentError] if probability is out of range
87
+ # @raise [ArgumentError] if ignore_hints contains invalid hints
88
+ # @raise [ArgumentError] if apply_probability_to is not one of the allowed symbols
89
+ def self.probability(probability,
90
+ ignore_hints: [OpenTelemetry::Trace::SamplingHint::RECORD],
91
+ ignore_parent: false,
92
+ apply_probability_to: :root_spans_and_remote_parent)
93
+ raise ArgumentError, 'probability must be in range [0.0, 1.0]' unless (0.0..1.0).include?(probability)
94
+ raise ArgumentError, 'ignore_hints' unless (ignore_hints.to_a - SAMPLING_HINTS).empty?
95
+ raise ArgumentError, 'apply_probability_to' unless APPLY_PROBABILITY_TO_SYMBOLS.include?(apply_probability_to)
96
+
97
+ ProbabilitySampler.new(probability,
98
+ ignore_hints: ignore_hints.to_a,
99
+ ignore_parent: ignore_parent,
100
+ apply_to_remote_parent: apply_probability_to != :root_spans,
101
+ apply_to_all_spans: apply_probability_to == :all_spans)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module SDK
9
+ module Trace
10
+ module Samplers
11
+ # The Decision module contains a set of constants to be used in the
12
+ # decision part of a sampling {Result}.
13
+ module Decision
14
+ # Decision to not record events and not sample.
15
+ NOT_RECORD = OpenTelemetry::Trace::SamplingHint::NOT_RECORD
16
+
17
+ # Decision to record events and not sample.
18
+ RECORD = OpenTelemetry::Trace::SamplingHint::RECORD
19
+
20
+ # Decision to record events and sample.
21
+ RECORD_AND_SAMPLED = OpenTelemetry::Trace::SamplingHint::RECORD_AND_SAMPLED
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module SDK
9
+ module Trace
10
+ module Samplers
11
+ # @api private
12
+ #
13
+ # Implements sampling based on a probability.
14
+ class ProbabilitySampler
15
+ HINT_RECORD_AND_SAMPLED = OpenTelemetry::Trace::SamplingHint::RECORD_AND_SAMPLED
16
+ HINT_RECORD = OpenTelemetry::Trace::SamplingHint::RECORD
17
+
18
+ private_constant(:HINT_RECORD_AND_SAMPLED, :HINT_RECORD)
19
+
20
+ def initialize(probability, ignore_hints:, ignore_parent:, apply_to_remote_parent:, apply_to_all_spans:)
21
+ @probability = probability
22
+ @id_upper_bound = format('%016x', (probability * (2**64 - 1)).ceil)
23
+ @ignored_hints = ignore_hints
24
+ @use_parent_sampled_flag = !ignore_parent
25
+ @apply_to_remote_parent = apply_to_remote_parent
26
+ @apply_to_all_spans = apply_to_all_spans
27
+ end
28
+
29
+ # @api private
30
+ #
31
+ # Callable interface for probability sampler. See {Samplers}.
32
+ def call(trace_id:, span_id:, parent_context:, hint:, links:, name:, kind:, attributes:)
33
+ # Ignored for sampling decision: links, name, kind, attributes.
34
+
35
+ hint = nil if @ignored_hints.include?(hint)
36
+
37
+ sampled = sample?(hint, trace_id, parent_context)
38
+ recording = hint == HINT_RECORD || sampled
39
+
40
+ if sampled && recording
41
+ RECORD_AND_SAMPLED
42
+ elsif recording
43
+ RECORD
44
+ else
45
+ NOT_RECORD
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def sample?(hint, trace_id, parent_context)
52
+ if parent_context.nil?
53
+ hint == HINT_RECORD_AND_SAMPLED || sample_trace_id?(trace_id)
54
+ else
55
+ parent_sampled?(parent_context) || hint == HINT_RECORD_AND_SAMPLED || sample_trace_id_for_child?(parent_context, trace_id)
56
+ end
57
+ end
58
+
59
+ def parent_sampled?(parent_context)
60
+ @use_parent_sampled_flag && parent_context.trace_flags.sampled?
61
+ end
62
+
63
+ def sample_trace_id_for_child?(parent_context, trace_id)
64
+ (@apply_to_all_spans || (@apply_to_remote_parent && parent_context.remote?)) && sample_trace_id?(trace_id)
65
+ end
66
+
67
+ def sample_trace_id?(trace_id)
68
+ @probability == 1.0 || trace_id[16, 16] < @id_upper_bound
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module SDK
9
+ module Trace
10
+ module Samplers
11
+ # The Result class represents an arbitrary sampling result. It has
12
+ # boolean values for the sampling decision and whether to record
13
+ # events, and a collection of attributes to be attached to a sampled
14
+ # root span.
15
+ class Result
16
+ EMPTY_HASH = {}.freeze
17
+ DECISIONS = [Decision::RECORD, Decision::NOT_RECORD, Decision::RECORD_AND_SAMPLED].freeze
18
+ private_constant(:EMPTY_HASH, :DECISIONS)
19
+
20
+ # Returns a frozen hash of attributes to be attached span.
21
+ #
22
+ # @return [Hash<String, Object>]
23
+ attr_reader :attributes
24
+
25
+ # Returns a new sampling result with the specified decision and
26
+ # attributes.
27
+ #
28
+ # @param [Symbol] decision Whether or not a span should be sampled
29
+ # and/or record events.
30
+ # @param [optional Hash<String, Object>] attributes A frozen or freezable hash
31
+ # containing attributes to be attached to the span.
32
+ def initialize(decision:, attributes: nil)
33
+ @decision = decision
34
+ @attributes = attributes.freeze || EMPTY_HASH
35
+ end
36
+
37
+ # Returns true if this span should be sampled.
38
+ #
39
+ # @return [Boolean] sampling decision
40
+ def sampled?
41
+ @decision == Decision::RECORD_AND_SAMPLED
42
+ end
43
+
44
+ # Returns true if this span should record events, attributes, status, etc.
45
+ #
46
+ # @return [Boolean] recording decision
47
+ def recording?
48
+ @decision != Decision::NOT_RECORD
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 OpenTelemetry Authors
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ module OpenTelemetry
8
+ module SDK
9
+ module Trace
10
+ # Implementation of {OpenTelemetry::Trace::Span} that records trace events.
11
+ #
12
+ # This implementation includes reader methods intended to allow access to
13
+ # internal state by SpanProcessors (see {NoopSpanProcessor} for the interface).
14
+ # Instrumentation should use the API provided by {OpenTelemetry::Trace::Span}
15
+ # and should consider {Span} to be write-only.
16
+ #
17
+ # rubocop:disable Metrics/ClassLength
18
+ class Span < OpenTelemetry::Trace::Span
19
+ # The following readers are intended for the use of SpanProcessors and
20
+ # should not be considered part of the public interface for instrumentation.
21
+ attr_reader :name, :status, :kind, :parent_span_id, :start_timestamp, :end_timestamp, :links, :library_resource
22
+
23
+ # Return a frozen copy of the current attributes. This is intended for
24
+ # use of SpanProcesses and should not be considered part of the public
25
+ # interface for instrumentation.
26
+ #
27
+ # @return [Hash<String, Object>] may be nil.
28
+ def attributes
29
+ # Don't bother synchronizing. Access by SpanProcessors is expected to
30
+ # be serialized.
31
+ @attributes&.clone.freeze
32
+ end
33
+
34
+ # Return a frozen copy of the current events. This is intended for use
35
+ # of SpanProcessors and should not be considered part of the public
36
+ # interface for instrumentation.
37
+ #
38
+ # @return [Array<Event>] may be nil.
39
+ def events
40
+ # Don't bother synchronizing. Access by SpanProcessors is expected to
41
+ # be serialized.
42
+ @events&.clone.freeze
43
+ end
44
+
45
+ # Return the flag whether this span is recording events
46
+ #
47
+ # @return [Boolean] true if this Span is active and recording information
48
+ # like events with the #add_event operation and attributes using
49
+ # #set_attribute.
50
+ def recording?
51
+ true
52
+ end
53
+
54
+ # Set attribute
55
+ #
56
+ # Note that the OpenTelemetry project
57
+ # {https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md
58
+ # documents} certain "standard attributes" that have prescribed semantic
59
+ # meanings.
60
+ #
61
+ # @param [String] key
62
+ # @param [String, Boolean, Numeric] value
63
+ #
64
+ # @return [self] returns itself
65
+ def set_attribute(key, value)
66
+ super
67
+ @mutex.synchronize do
68
+ if @ended
69
+ OpenTelemetry.logger.warn('Calling set_attribute on an ended Span.')
70
+ else
71
+ @attributes ||= {}
72
+ @attributes[key] = value
73
+ trim_span_attributes(@attributes)
74
+ @total_recorded_attributes += 1
75
+ end
76
+ end
77
+ self
78
+ end
79
+
80
+ # Add an Event to a {Span}. This can be accomplished eagerly or lazily.
81
+ # Lazy evaluation is useful when the event attributes are expensive to
82
+ # build and where the cost can be avoided for an unsampled {Span}.
83
+ #
84
+ # Eager example:
85
+ #
86
+ # span.add_event(name: 'event', attributes: {'eager' => true})
87
+ #
88
+ # Lazy example:
89
+ #
90
+ # span.add_event { OpenTelemetry::Trace::Event.new(name: 'event', attributes: {'eager' => false}) }
91
+ #
92
+ # Note that the OpenTelemetry project
93
+ # {https://github.com/open-telemetry/opentelemetry-specification/blob/master/semantic-conventions.md
94
+ # documents} certain "standard event names and keys" which have
95
+ # prescribed semantic meanings.
96
+ #
97
+ # @param [optional String] name Optional name of the event. This is
98
+ # required if a block is not given.
99
+ # @param [optional Hash<String, Object>] attributes One or more key:value
100
+ # pairs, where the keys must be strings and the values may be string,
101
+ # boolean or numeric type. This argument should only be used when
102
+ # passing in a name.
103
+ # @param [optional Time] timestamp Optional timestamp for the event.
104
+ # This argument should only be used when passing in a name.
105
+ #
106
+ # @return [self] returns itself
107
+ def add_event(name: nil, attributes: nil, timestamp: nil)
108
+ super
109
+ event = block_given? ? yield : OpenTelemetry::Trace::Event.new(name: name, attributes: attributes, timestamp: timestamp || Time.now)
110
+ @mutex.synchronize do
111
+ if @ended
112
+ OpenTelemetry.logger.warn('Calling add_event on an ended Span.')
113
+ else
114
+ @events ||= []
115
+ @events = append_event(@events, event)
116
+ @total_recorded_events += 1
117
+ end
118
+ end
119
+ self
120
+ end
121
+
122
+ # Sets the Status to the Span
123
+ #
124
+ # If used, this will override the default Span status. Default is OK.
125
+ #
126
+ # Only the value of the last call will be recorded, and implementations
127
+ # are free to ignore previous calls.
128
+ #
129
+ # @param [Status] status The new status, which overrides the default Span
130
+ # status, which is OK.
131
+ #
132
+ # @return [void]
133
+ def status=(status)
134
+ super
135
+ @mutex.synchronize do
136
+ if @ended
137
+ OpenTelemetry.logger.warn('Calling status= on an ended Span.')
138
+ else
139
+ @status = status
140
+ end
141
+ end
142
+ end
143
+
144
+ # Updates the Span name
145
+ #
146
+ # Upon this update, any sampling behavior based on Span name will depend
147
+ # on the implementation.
148
+ #
149
+ # @param [String] new_name The new operation name, which supersedes
150
+ # whatever was passed in when the Span was started
151
+ #
152
+ # @return [void]
153
+ def name=(new_name)
154
+ super
155
+ @mutex.synchronize do
156
+ if @ended
157
+ OpenTelemetry.logger.warn('Calling name= on an ended Span.')
158
+ else
159
+ @name = new_name
160
+ end
161
+ end
162
+ end
163
+
164
+ # Finishes the Span
165
+ #
166
+ # Implementations MUST ignore all subsequent calls to {#finish} (there
167
+ # might be exceptions when Tracer is streaming event and has no mutable
168
+ # state associated with the Span).
169
+ #
170
+ # Call to {#finish} MUST not have any effects on child spans. Those may
171
+ # still be running and can be ended later.
172
+ #
173
+ # This API MUST be non-blocking*.
174
+ #
175
+ # (*) not actually non-blocking. In particular, it synchronizes on an
176
+ # internal mutex, which will typically be uncontended, and
177
+ # {Export::BatchSpanProcessor} will also synchronize on a mutex, if that
178
+ # processor is used.
179
+ #
180
+ # @param [Time] end_timestamp optional end timestamp for the span.
181
+ #
182
+ # @return [self] returns itself
183
+ def finish(end_timestamp: nil)
184
+ @mutex.synchronize do
185
+ if @ended
186
+ OpenTelemetry.logger.warn('Calling finish on an ended Span.')
187
+ return self
188
+ end
189
+ @end_timestamp = end_timestamp || Time.now
190
+ @attributes.freeze
191
+ @events.freeze
192
+ @ended = true
193
+ end
194
+ @span_processor.on_finish(self)
195
+ self
196
+ end
197
+
198
+ # @api private
199
+ #
200
+ # Returns a SpanData containing a snapshot of the Span fields. It is
201
+ # assumed that the Span has been finished, and that no further
202
+ # modifications will be made to the Span.
203
+ #
204
+ # This method should be called *only* from a SpanProcessor prior to
205
+ # calling the SpanExporter.
206
+ #
207
+ # @return [SpanData]
208
+ def to_span_data
209
+ SpanData.new(
210
+ @name,
211
+ @kind,
212
+ @status,
213
+ @parent_span_id,
214
+ @child_count,
215
+ @total_recorded_attributes,
216
+ @total_recorded_events,
217
+ @total_recorded_links,
218
+ @start_timestamp,
219
+ @end_timestamp,
220
+ @attributes,
221
+ @links,
222
+ @events,
223
+ @library_resource,
224
+ context.span_id,
225
+ context.trace_id,
226
+ context.trace_flags
227
+ )
228
+ end
229
+
230
+ # @api private
231
+ def initialize(context, name, kind, parent_span_id, trace_config, span_processor, attributes, links, start_timestamp, library_resource) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
232
+ super(span_context: context)
233
+ @mutex = Mutex.new
234
+ @name = name
235
+ @kind = kind
236
+ @parent_span_id = parent_span_id.freeze || OpenTelemetry::Trace::INVALID_SPAN_ID
237
+ @trace_config = trace_config
238
+ @span_processor = span_processor
239
+ @library_resource = library_resource
240
+ @ended = false
241
+ @status = nil
242
+ @child_count = 0
243
+ @total_recorded_events = 0
244
+ @total_recorded_links = links&.size || 0
245
+ @total_recorded_attributes = attributes&.size || 0
246
+ @start_timestamp = start_timestamp
247
+ @end_timestamp = nil
248
+ @attributes = attributes.nil? ? nil : Hash[attributes] # We need a mutable copy of attributes.
249
+ trim_span_attributes(@attributes)
250
+ @events = nil
251
+ @links = trim_links(links, trace_config.max_links_count, trace_config.max_attributes_per_link)
252
+ @span_processor.on_start(self)
253
+ end
254
+
255
+ # TODO: Java implementation overrides finalize to log if a span isn't finished.
256
+
257
+ private
258
+
259
+ def trim_span_attributes(attrs)
260
+ return if attrs.nil?
261
+
262
+ excess = attrs.size - @trace_config.max_attributes_count
263
+ # TODO: with Ruby 2.5, replace with the more efficient
264
+ # attrs.shift(excess) if excess.positive?
265
+ excess.times { attrs.shift } if excess.positive?
266
+ nil
267
+ end
268
+
269
+ def trim_links(links, max_links_count, max_attributes_per_link) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
270
+ # Fast path (likely) common cases.
271
+ return nil if links.nil?
272
+
273
+ if links.size <= max_links_count &&
274
+ links.all? { |link| link.attributes.size <= max_attributes_per_link && Internal.valid_attributes?(link.attributes) }
275
+ return links.frozen? ? links : links.clone.freeze
276
+ end
277
+
278
+ # Slow path: trim attributes for each Link.
279
+ links.last(max_links_count).map! do |link|
280
+ attrs = Hash[link.attributes] # link.attributes is frozen, so we need an unfrozen copy to adjust.
281
+ attrs.keep_if { |key, value| Internal.valid_key?(key) && Internal.valid_value?(value) }
282
+ excess = attrs.size - max_attributes_per_link
283
+ excess.times { attrs.shift } if excess.positive?
284
+ OpenTelemetry::Trace::Link.new(link.context, attrs)
285
+ end.freeze
286
+ end
287
+
288
+ def append_event(events, event) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
289
+ max_events_count = @trace_config.max_events_count
290
+ max_attributes_per_event = @trace_config.max_attributes_per_event
291
+
292
+ # Fast path (likely) common case.
293
+ if events.size < max_events_count &&
294
+ event.attributes.size <= max_attributes_per_event &&
295
+ Internal.valid_attributes?(event.attributes)
296
+ return events << event
297
+ end
298
+
299
+ # Slow path.
300
+ excess = events.size + 1 - max_events_count
301
+ events.shift(excess) if excess.positive?
302
+
303
+ excess = event.attributes.size - max_attributes_per_event
304
+ if excess.positive? || !Internal.valid_attributes?(event.attributes)
305
+ attrs = Hash[event.attributes] # event.attributes is frozen, so we need an unfrozen copy to adjust.
306
+ attrs.keep_if { |key, value| Internal.valid_key?(key) && Internal.valid_value?(value) }
307
+ excess = attrs.size - max_attributes_per_event
308
+ excess.times { attrs.shift } if excess.positive?
309
+ event = OpenTelemetry::Trace::Event.new(name: event.name, attributes: attrs, timestamp: event.timestamp)
310
+ end
311
+ events << event
312
+ end
313
+ end
314
+ # rubocop:enable Metrics/ClassLength
315
+ end
316
+ end
317
+ end