splunk-tracer 0.1.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.
@@ -0,0 +1,23 @@
1
+ module SplunkTracing
2
+ # Scope represents an OpenTracing Scope
3
+ #
4
+ # See http://www.opentracing.io for more information.
5
+ class Scope
6
+ attr_reader :span
7
+
8
+ def initialize(manager:, span:, finish_on_close: true)
9
+ @manager = manager
10
+ @span = span
11
+ @finish_on_close = finish_on_close
12
+ end
13
+
14
+ # Mark the end of the active period for the current thread and Scope,
15
+ # updating the ScopeManager#active in the process.
16
+ def close
17
+ raise(SplunkTracing::Error, 'already closed') if @closed
18
+ @closed = true
19
+ @span.finish if @finish_on_close
20
+ @manager.deactivate
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ module SplunkTracing
2
+ # ScopeManager represents an OpenTracing ScopeManager
3
+ #
4
+ # See http://www.opentracing.io for more information.
5
+ #
6
+ # The ScopeManager interface abstracts both the activation of Span instances
7
+ # via ScopeManager#activate and access to an active Span/Scope via
8
+ # ScopeManager#active
9
+ #
10
+ class ScopeManager
11
+ # Make a span instance active.
12
+ #
13
+ # @param span [Span] the Span that should become active
14
+ # @param finish_on_close [Boolean] whether the Span should automatically be
15
+ # finished when Scope#close is called
16
+ # @return [Scope] instance to control the end of the active period for the
17
+ # Span. It is a programming error to neglect to call Scope#close on the
18
+ # returned instance.
19
+ def activate(span:, finish_on_close: true)
20
+ return active if active && active.span == span
21
+ SplunkTracing::Scope.new(manager: self, span: span, finish_on_close: finish_on_close).tap do |scope|
22
+ add_scope(scope)
23
+ end
24
+ end
25
+
26
+ # @return [Scope] the currently active Scope which can be used to access the
27
+ # currently active Span.
28
+ #
29
+ # If there is a non-null Scope, its wrapped Span becomes an implicit parent
30
+ # (as Reference#CHILD_OF) of any newly-created Span at Tracer#start_active_span
31
+ # or Tracer#start_span time.
32
+ def active
33
+ scopes.last
34
+ end
35
+
36
+ def deactivate
37
+ scopes.pop
38
+ end
39
+
40
+ private
41
+
42
+ def scopes
43
+ Thread.current[object_id.to_s] || []
44
+ end
45
+
46
+ def add_scope(scope)
47
+ if Thread.current[object_id.to_s].nil?
48
+ Thread.current[object_id.to_s] = [scope]
49
+ else
50
+ Thread.current[object_id.to_s] << scope
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,191 @@
1
+ require 'concurrent'
2
+ require 'splunktracing/span_context'
3
+
4
+ module SplunkTracing
5
+ # Span represents an OpenTracer Span
6
+ #
7
+ # See http://www.opentracing.io for more information.
8
+ class Span
9
+ # Part of the OpenTracing API
10
+ attr_writer :operation_name
11
+
12
+ # Internal use only
13
+ # @private
14
+ attr_reader :start_micros, :end_micros, :tags, :operation_name, :context
15
+
16
+ # To keep backwards compatibility
17
+ alias_method :span_context, :context
18
+
19
+ # Creates a new {Span}
20
+ #
21
+ # @param tracer [Tracer] the tracer that created this span
22
+ # @param operation_name [String] the operation name of this span. If it's
23
+ # not a String it will be encoded with to_s.
24
+ # @param child_of [SpanContext] the parent SpanContext (per child_of)
25
+ # @param references [Array<SpanContext>] An array of SpanContexts
26
+ # that identify what Spans this Span follows from causally. Presently
27
+ # only one reference is supported, and cannot be provided in addition to
28
+ # a child_of.
29
+ # @param start_micros [Numeric] start time of the span in microseconds
30
+ # @param tags [Hash] initial key:value tags (per set_tag) for the Span
31
+ # @param max_log_records [Numeric] maximum allowable number of log records
32
+ # for the Span
33
+ # @return [Span] a started Span
34
+ def initialize(
35
+ tracer:,
36
+ operation_name:,
37
+ child_of: nil,
38
+ references: [],
39
+ start_micros:,
40
+ tags: nil,
41
+ max_log_records:
42
+ )
43
+
44
+ @tags = Concurrent::Hash.new
45
+ @tags.update(tags.each { |k, v| tags[k] = v.to_s }) unless tags.nil?
46
+ @log_records = Concurrent::Array.new
47
+ @dropped_logs = Concurrent::AtomicFixnum.new
48
+ @max_log_records = max_log_records
49
+
50
+ @tracer = tracer
51
+ self.operation_name = operation_name.to_s
52
+ self.start_micros = start_micros
53
+
54
+ ref = child_of ? child_of : references
55
+ ref = ref[0] if (Array === ref)
56
+ ref = ref.context if (Span === ref)
57
+
58
+ if SpanContext === ref
59
+ @context = SpanContext.new(id: SplunkTracing.guid, trace_id: ref.trace_id, parent_id: ref.id)
60
+ set_baggage(ref.baggage)
61
+ # set_tag(:parent_span_guid, ref.id)
62
+ else
63
+ @context = SpanContext.new(id: SplunkTracing.guid, trace_id: SplunkTracing.guid)
64
+ end
65
+ end
66
+
67
+ # Set a tag value on this span
68
+ # @param key [String] the key of the tag
69
+ # @param value [String] the value of the tag. If it's not a String
70
+ # it will be encoded with to_s
71
+ def set_tag(key, value)
72
+ tags[key] = value.to_s
73
+ self
74
+ end
75
+
76
+ # TODO(ngauthier@gmail.com) baggage keys have a restricted format according
77
+ # to the spec: http://opentracing.io/documentation/pages/spec#baggage-vs-span-tags
78
+
79
+ # Set a baggage item on the span
80
+ # @param key [String] the key of the baggage item
81
+ # @param value [String] the value of the baggage item
82
+ def set_baggage_item(key, value)
83
+ @context = SpanContext.new(
84
+ id: context.id,
85
+ trace_id: context.trace_id,
86
+ parent_id: context.parent_id,
87
+ baggage: context.baggage.merge({key => value})
88
+ )
89
+ self
90
+ end
91
+
92
+ # Set all baggage at once. This will reset the baggage to the given param.
93
+ # @param baggage [Hash] new baggage for the span
94
+ def set_baggage(baggage = {})
95
+ @context = SpanContext.new(
96
+ id: context.id,
97
+ trace_id: context.trace_id,
98
+ parent_id: context.parent_id,
99
+ baggage: baggage
100
+ )
101
+ end
102
+
103
+ # Get a baggage item
104
+ # @param key [String] the key of the baggage item
105
+ # @return Value of the baggage item
106
+ def get_baggage_item(key)
107
+ context.baggage[key]
108
+ end
109
+
110
+ # @deprecated Use {#log_kv} instead.
111
+ # Add a log entry to this span
112
+ # @param event [String] event name for the log
113
+ # @param timestamp [Time] time of the log
114
+ # @param fields [Hash] Additional information to log
115
+ def log(event: nil, timestamp: Time.now, **fields)
116
+ warn 'Span#log is deprecated. Please use Span#log_kv instead.'
117
+ return unless tracer.enabled?
118
+
119
+ fields = {} if fields.nil?
120
+ unless event.nil?
121
+ fields[:event] = event.to_s
122
+ end
123
+
124
+ log_kv(timestamp: timestamp, **fields)
125
+ end
126
+
127
+ def log_kv(timestamp: Time.now, **fields)
128
+ return unless tracer.enabled?
129
+
130
+ fields = {} if fields.nil?
131
+ record = {
132
+ timestamp_micros: SplunkTracing.micros(timestamp),
133
+ fields: fields,
134
+ }
135
+
136
+ log_records.push(record)
137
+ if log_records.size > @max_log_records
138
+ log_records.shift
139
+ dropped_logs.increment
140
+ end
141
+ end
142
+
143
+ # Finish the {Span}
144
+ # @param end_time [Time] custom end time, if not now
145
+ def finish(end_time: Time.now)
146
+ if end_micros.nil?
147
+ self.end_micros = SplunkTracing.micros(end_time)
148
+ end
149
+ tracer.finish_span(self)
150
+ self
151
+ end
152
+
153
+ # Hash representation of a span
154
+ def to_h
155
+ if end_micros.nil?
156
+ self.end_micros = SplunkTracing.micros(Time.now)
157
+ end
158
+ {
159
+ guid: tracer.guid,
160
+ span_id: context.id,
161
+ trace_id: context.trace_id,
162
+ parent_span_id: context.parent_id,
163
+ operation_name: operation_name,
164
+ tags: tags,
165
+ timestamp: start_micros/1000000.0,
166
+ duration: end_micros-start_micros,
167
+ error_flag: false,
168
+ dropped_logs: dropped_logs_count,
169
+ log_records: log_records,
170
+ baggage: context.baggage
171
+ }
172
+ end
173
+
174
+ # Internal use only
175
+ # @private
176
+ def dropped_logs_count
177
+ dropped_logs.value
178
+ end
179
+
180
+ # Internal use only
181
+ # @private
182
+ def logs_count
183
+ log_records.size
184
+ end
185
+
186
+ private
187
+
188
+ attr_reader :tracer, :dropped_logs, :log_records
189
+ attr_writer :start_micros, :end_micros
190
+ end
191
+ end
@@ -0,0 +1,13 @@
1
+ module SplunkTracing
2
+ # SpanContext holds the data for a span that gets inherited to child spans
3
+ class SpanContext
4
+ attr_reader :id, :trace_id, :parent_id, :baggage
5
+
6
+ def initialize(id:, trace_id:, parent_id: nil, baggage: {})
7
+ @id = id.freeze
8
+ @trace_id = trace_id.freeze
9
+ @parent_id = parent_id.freeze
10
+ @baggage = baggage.freeze
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,321 @@
1
+ require 'json'
2
+ require 'concurrent'
3
+
4
+ require 'opentracing'
5
+
6
+ require 'splunktracing/span'
7
+ require 'splunktracing/reporter'
8
+ require 'splunktracing/transport/http_json'
9
+ require 'splunktracing/transport/nil'
10
+ require 'splunktracing/transport/callback'
11
+
12
+ module SplunkTracing
13
+ class Tracer
14
+ class Error < SplunkTracing::Error; end
15
+ class ConfigurationError < SplunkTracing::Tracer::Error; end
16
+
17
+ attr_reader :access_token, :guid
18
+
19
+ # Initialize a new tracer. Either an access_token or a transport must be
20
+ # provided. A component_name is always required.
21
+ # @param component_name [String] Component name to use for the tracer
22
+ # @param access_token [String] The project access token when pushing to SplunkTracing
23
+ # @param transport [SplunkTracing::Transport] How the data should be transported
24
+ # @param tags [Hash] Tracer-level tags
25
+ # @return SplunkTracing::Tracer
26
+ # @raise SplunkTracing::ConfigurationError if the group name or access token is not a valid string.
27
+ def initialize(component_name:, access_token: nil, transport: nil, tags: {})
28
+ configure(component_name: component_name, access_token: access_token, transport: transport, tags: tags)
29
+ end
30
+
31
+ def max_log_records
32
+ @max_log_records ||= DEFAULT_MAX_LOG_RECORDS
33
+ end
34
+
35
+ def max_log_records=(max)
36
+ @max_log_records = [MIN_MAX_LOG_RECORDS, max].max
37
+ end
38
+
39
+ def max_span_records
40
+ @max_span_records ||= DEFAULT_MAX_SPAN_RECORDS
41
+ end
42
+
43
+ def max_span_records=(max)
44
+ @max_span_records = [MIN_MAX_SPAN_RECORDS, max].max
45
+ @reporter.max_span_records = @max_span_records
46
+ end
47
+
48
+ # Set the report flushing period. If set to 0, no flushing will be done, you
49
+ # must manually call flush.
50
+ def report_period_seconds=(seconds)
51
+ @reporter.period = seconds
52
+ end
53
+
54
+ # TODO(bhs): Support FollowsFrom and multiple references
55
+
56
+ # Creates a scope manager or returns the already-created one.
57
+ #
58
+ # @return [ScopeManager] the current ScopeManager, which may be a no-op but
59
+ # may not be nil.
60
+ def scope_manager
61
+ @scope_manager ||= SplunkTracing::ScopeManager.new
62
+ end
63
+
64
+ # Returns a newly started and activated Scope.
65
+ #
66
+ # If ScopeManager#active is not nil, no explicit references are provided,
67
+ # and `ignore_active_scope` is false, then an inferred References#CHILD_OF
68
+ # reference is created to the ScopeManager#active's SpanContext when
69
+ # start_active_span is invoked.
70
+ #
71
+ # @param operation_name [String] The operation name for the Span
72
+ # @param child_of [SpanContext, Span] SpanContext that acts as a parent to
73
+ # the newly-started Span. If a Span instance is provided, its
74
+ # context is automatically substituted. See [Reference] for more
75
+ # information.
76
+ #
77
+ # If specified, the `references` parameter must be omitted.
78
+ # @param references [Array<Reference>] An array of reference
79
+ # objects that identify one or more parent SpanContexts.
80
+ # @param start_time [Time] When the Span started, if not now
81
+ # @param tags [Hash] Tags to assign to the Span at start time
82
+ # @param ignore_active_scope [Boolean] whether to create an implicit
83
+ # References#CHILD_OF reference to the ScopeManager#active.
84
+ # @param finish_on_close [Boolean] whether span should automatically be
85
+ # finished when Scope#close is called
86
+ # @yield [Scope] If an optional block is passed to start_active it will
87
+ # yield the newly-started Scope. If `finish_on_close` is true then the
88
+ # Span will be finished automatically after the block is executed.
89
+ # @return [Scope] The newly-started and activated Scope
90
+ def start_active_span(operation_name,
91
+ child_of: nil,
92
+ references: nil,
93
+ start_time: Time.now,
94
+ tags: nil,
95
+ ignore_active_scope: false,
96
+ finish_on_close: true)
97
+ if child_of.nil? && references.nil? && !ignore_active_scope
98
+ child_of = active_span
99
+ end
100
+
101
+ span = start_span(
102
+ operation_name,
103
+ child_of: child_of,
104
+ references: references,
105
+ start_time: start_time,
106
+ tags: tags,
107
+ ignore_active_scope: ignore_active_scope
108
+ )
109
+
110
+ scope_manager.activate(span: span, finish_on_close: finish_on_close).tap do |scope|
111
+ if block_given?
112
+ yield scope
113
+ scope.close
114
+ end
115
+ end
116
+ end
117
+
118
+ # Returns the span from the active scope, if any.
119
+ #
120
+ # @return [Span, nil] the active span. This is a shorthand for
121
+ # `scope_manager.active.span`, and nil will be returned if
122
+ # Scope#active is nil.
123
+ def active_span
124
+ scope = scope_manager.active
125
+ scope.span if scope
126
+ end
127
+
128
+ # Starts a new span.
129
+ #
130
+ # @param operation_name [String] The operation name for the Span
131
+ # @param child_of [SpanContext] SpanContext that acts as a parent to
132
+ # the newly-started Span. If a Span instance is provided, its
133
+ # .span_context is automatically substituted.
134
+ # @param references [Array<SpanContext>] An array of SpanContexts that
135
+ # identify any parent SpanContexts of newly-started Span. If Spans
136
+ # are provided, their .span_context is automatically substituted.
137
+ # @param start_time [Time] When the Span started, if not now
138
+ # @param tags [Hash] Tags to assign to the Span at start time
139
+ # @param ignore_active_scope [Boolean] whether to create an implicit
140
+ # References#CHILD_OF reference to the ScopeManager#active.
141
+ # @return [Span]
142
+ def start_span(operation_name, child_of: nil, references: nil, start_time: nil, tags: nil, ignore_active_scope: false)
143
+ if child_of.nil? && references.nil? && !ignore_active_scope
144
+ child_of = active_span
145
+ end
146
+
147
+ Span.new(
148
+ tracer: self,
149
+ operation_name: operation_name,
150
+ child_of: child_of,
151
+ references: references,
152
+ start_micros: start_time.nil? ? SplunkTracing.micros(Time.now) : SplunkTracing.micros(start_time),
153
+ tags: tags,
154
+ max_log_records: max_log_records,
155
+ )
156
+ end
157
+
158
+ # Inject a SpanContext into the given carrier
159
+ #
160
+ # @param spancontext [SpanContext]
161
+ # @param format [OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY]
162
+ # @param carrier [Carrier] A carrier object of the type dictated by the specified `format`
163
+ def inject(span_context, format, carrier)
164
+ case format
165
+ when OpenTracing::FORMAT_TEXT_MAP
166
+ inject_to_text_map(span_context, carrier)
167
+ when OpenTracing::FORMAT_BINARY
168
+ warn 'Binary inject format not yet implemented'
169
+ when OpenTracing::FORMAT_RACK
170
+ inject_to_rack(span_context, carrier)
171
+ else
172
+ warn 'Unknown inject format'
173
+ end
174
+ end
175
+
176
+ # Extract a SpanContext from a carrier
177
+ # @param format [OpenTracing::FORMAT_TEXT_MAP, OpenTracing::FORMAT_BINARY, OpenTracing::FORMAT_RACK]
178
+ # @param carrier [Carrier] A carrier object of the type dictated by the specified `format`
179
+ # @return [SpanContext] the extracted SpanContext or nil if none could be found
180
+ def extract(format, carrier)
181
+ case format
182
+ when OpenTracing::FORMAT_TEXT_MAP
183
+ extract_from_text_map(carrier)
184
+ when OpenTracing::FORMAT_BINARY
185
+ warn 'Binary join format not yet implemented'
186
+ nil
187
+ when OpenTracing::FORMAT_RACK
188
+ extract_from_rack(carrier)
189
+ else
190
+ warn 'Unknown join format'
191
+ nil
192
+ end
193
+ end
194
+
195
+ # @return true if the tracer is enabled
196
+ def enabled?
197
+ return @enabled if defined?(@enabled)
198
+ @enabled = true
199
+ end
200
+
201
+ # Enables the tracer
202
+ def enable
203
+ @enabled = true
204
+ end
205
+
206
+ # Disables the tracer
207
+ # @param discard [Boolean] whether to discard queued data
208
+ def disable(discard: true)
209
+ @enabled = false
210
+ @reporter.clear if discard
211
+ @reporter.flush
212
+ end
213
+
214
+ # Flush to the Transport
215
+ def flush
216
+ return unless enabled?
217
+ @reporter.flush
218
+ end
219
+
220
+ # Internal use only.
221
+ # @private
222
+ def finish_span(span)
223
+ return unless enabled?
224
+ @reporter.add_span(span)
225
+ end
226
+
227
+ protected
228
+
229
+ def configure(component_name:, access_token: nil, transport: nil, tags: {})
230
+ raise ConfigurationError, "component_name must be a string" unless component_name.is_a?(String)
231
+ raise ConfigurationError, "component_name cannot be blank" if component_name.empty?
232
+
233
+ if transport.nil? and !access_token.nil?
234
+ transport = Transport::HTTPJSON.new(access_token: access_token)
235
+ end
236
+
237
+ raise ConfigurationError, "you must provide an access token or a transport" if transport.nil?
238
+ raise ConfigurationError, "#{transport} is not a SplunkTracing transport class" if !(SplunkTracing::Transport::Base === transport)
239
+
240
+ @guid = SplunkTracing.guid
241
+
242
+ @reporter = SplunkTracing::Reporter.new(
243
+ max_span_records: max_span_records,
244
+ transport: transport,
245
+ guid: guid,
246
+ component_name: component_name,
247
+ tags: tags
248
+ )
249
+ end
250
+
251
+ private
252
+
253
+ CARRIER_TRACER_STATE_PREFIX = 'ot-tracer-'.freeze
254
+ CARRIER_BAGGAGE_PREFIX = 'ot-baggage-'.freeze
255
+
256
+ CARRIER_SPAN_ID = (CARRIER_TRACER_STATE_PREFIX + 'spanid').freeze
257
+ CARRIER_TRACE_ID = (CARRIER_TRACER_STATE_PREFIX + 'traceid').freeze
258
+ CARRIER_SAMPLED = (CARRIER_TRACER_STATE_PREFIX + 'sampled').freeze
259
+
260
+ DEFAULT_MAX_LOG_RECORDS = 1000
261
+ MIN_MAX_LOG_RECORDS = 1
262
+ DEFAULT_MAX_SPAN_RECORDS = 1000
263
+ MIN_MAX_SPAN_RECORDS = 1
264
+
265
+ def inject_to_text_map(span_context, carrier)
266
+ carrier[CARRIER_SPAN_ID] = span_context.id
267
+ carrier[CARRIER_TRACE_ID] = span_context.trace_id unless span_context.trace_id.nil?
268
+ carrier[CARRIER_SAMPLED] = 'true'
269
+
270
+ span_context.baggage.each do |key, value|
271
+ carrier[CARRIER_BAGGAGE_PREFIX + key] = value
272
+ end
273
+ end
274
+
275
+ def extract_from_text_map(carrier)
276
+ # If the carrier does not have both the span_id and trace_id key
277
+ # skip the processing and just return a normal span
278
+ if !carrier.has_key?(CARRIER_SPAN_ID) || !carrier.has_key?(CARRIER_TRACE_ID)
279
+ return nil
280
+ end
281
+
282
+ baggage = carrier.reduce({}) do |baggage, tuple|
283
+ key, value = tuple
284
+ if key.start_with?(CARRIER_BAGGAGE_PREFIX)
285
+ plain_key = key.to_s[CARRIER_BAGGAGE_PREFIX.length..key.to_s.length]
286
+ baggage[plain_key] = value
287
+ end
288
+ baggage
289
+ end
290
+ SpanContext.new(
291
+ id: carrier[CARRIER_SPAN_ID],
292
+ trace_id: carrier[CARRIER_TRACE_ID],
293
+ baggage: baggage,
294
+ )
295
+ end
296
+
297
+ def inject_to_rack(span_context, carrier)
298
+ carrier[CARRIER_SPAN_ID] = span_context.id
299
+ carrier[CARRIER_TRACE_ID] = span_context.trace_id unless span_context.trace_id.nil?
300
+ carrier[CARRIER_SAMPLED] = 'true'
301
+
302
+ span_context.baggage.each do |key, value|
303
+ if key =~ /[^A-Za-z0-9\-_]/
304
+ # TODO: log the error internally
305
+ next
306
+ end
307
+ carrier[CARRIER_BAGGAGE_PREFIX + key] = value
308
+ end
309
+ end
310
+
311
+ def extract_from_rack(env)
312
+ extract_from_text_map(env.reduce({}){|memo, tuple|
313
+ raw_header, value = tuple
314
+ header = raw_header.to_s.gsub(/^HTTP_/, '').tr('_', '-').downcase
315
+
316
+ memo[header] = value if header.start_with?(CARRIER_TRACER_STATE_PREFIX, CARRIER_BAGGAGE_PREFIX)
317
+ memo
318
+ })
319
+ end
320
+ end
321
+ end