Class: Datadog::Tracing::SpanOperation

Inherits:
Object
  • Object
show all
Includes:
Metadata
Defined in:
lib/datadog/tracing/span_operation.rb

Overview

Represents the act of taking a span measurement. It gives a Span a context which can be used to build a Span. When completed, it yields the Span.

Defined Under Namespace

Classes: AlreadyStartedError, Events

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Metadata

included

Constructor Details

#initialize(name, logger: Datadog.logger, events: nil, on_error: nil, parent_id: 0, resource: name, service: nil, start_time: nil, tags: nil, trace_id: nil, type: nil, links: nil, span_events: nil, id: nil) ⇒ SpanOperation

Returns a new instance of SpanOperation.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/datadog/tracing/span_operation.rb', line 44

def initialize(
  name,
  logger: Datadog.logger,
  events: nil,
  on_error: nil,
  parent_id: 0,
  resource: name,
  service: nil,
  start_time: nil,
  tags: nil,
  trace_id: nil,
  type: nil,
  links: nil,
  span_events: nil,
  id: nil
)
  @logger = logger

  # Ensure dynamically created strings are UTF-8 encoded.
  #
  # All strings created in Ruby land are UTF-8. The only sources of non-UTF-8 string are:
  # * Strings explicitly encoded as non-UTF-8.
  # * Some natively created string, although most natively created strings are UTF-8.
  self.name = name
  self.service = service
  self.type = type
  self.resource = resource

  @id = id.nil? ? Tracing::Utils.next_id : id
  @parent_id = parent_id || 0
  @trace_id = trace_id || Tracing::Utils::TraceId.next_id

  @status = 0
  # stores array of span links
  @links = links || []
  # stores array of span events
  @span_events = span_events || []

  # start_time and end_time track wall clock. In Ruby, wall clock
  # has less accuracy than monotonic clock, so if possible we look to only use wall clock
  # to measure duration when a time is supplied by the user, or if monotonic clock
  # is unsupported.
  @start_time = nil
  @end_time = nil

  # duration_start and duration_end track monotonic clock, and may remain nil in cases where it
  # is known that we have to use wall clock to measure duration.
  @duration_start = nil
  @duration_end = nil

  # Set tags if provided.
  set_tags(tags) if tags

  # Some other SpanOperation-specific behavior
  @events = events || Events.new(logger: logger)
  @span = nil

  if on_error.nil?
    # Nothing, default error handler is already set up.
  elsif on_error.is_a?(Proc)
    # Subscribe :on_error event
    @events.on_error.wrap_default(&on_error)
  else
    logger.warn("on_error argument to SpanOperation ignored because is not a Proc: #{on_error}")
  end

  # Start the span with start time, if given.
  start(start_time) if start_time
end

Instance Attribute Details

#end_timeObject

Span attributes NOTE: In the future, we should drop the me



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def end_time
  @end_time
end

#idObject (readonly)

Span attributes NOTE: In the future, we should drop the me



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def id
  @id
end

Returns the value of attribute links.



42
43
44
# File 'lib/datadog/tracing/span_operation.rb', line 42

def links
  @links
end

#loggerObject (readonly)

Span attributes NOTE: In the future, we should drop the me



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def logger
  @logger
end

#nameString

Operation name.

Returns:

  • (String)


30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def name
  @name
end

#parent_idObject (readonly)

Span attributes NOTE: In the future, we should drop the me



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def parent_id
  @parent_id
end

#resourceString

Span resource.

Returns:

  • (String)

    String



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def resource
  @resource
end

#serviceString

Service name.

Returns:

  • (String)

    String



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def service
  @service
end

#span_eventsObject

Returns the value of attribute span_events.



42
43
44
# File 'lib/datadog/tracing/span_operation.rb', line 42

def span_events
  @span_events
end

#start_timeObject

Span attributes NOTE: In the future, we should drop the me



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def start_time
  @start_time
end

#statusObject

Returns the value of attribute status.



42
43
44
# File 'lib/datadog/tracing/span_operation.rb', line 42

def status
  @status
end

#trace_idObject (readonly)

Span attributes NOTE: In the future, we should drop the me



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def trace_id
  @trace_id
end

#typeString

Span type.

Returns:

  • (String)

    String



30
31
32
# File 'lib/datadog/tracing/span_operation.rb', line 30

def type
  @type
end

Instance Method Details

#durationObject



287
288
289
290
291
292
293
294
295
296
297
# File 'lib/datadog/tracing/span_operation.rb', line 287

def duration
  # Steep: https://github.com/soutaro/steep/issues/477
  # @type ivar @duration_end: Time
  # @type ivar @duration_start: Time
  return @duration_end - @duration_start if @duration_start && @duration_end

  # Steep: https://github.com/soutaro/steep/issues/477
  # @type ivar @end_time: Time
  # @type ivar @start_time: Time
  @end_time - @start_time if @start_time && @end_time
end

#finish(end_time = nil) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/datadog/tracing/span_operation.rb', line 266

def finish(end_time = nil)
  # Returned memoized span if already finished
  return span if finished?

  # Stop timing
  stop(end_time)

  # Build span
  # Memoize for performance reasons
  @span = build_span

  # Trigger after_finish event
  events.after_finish.publish(span, self)

  span
end

#finished?Boolean

Returns:

  • (Boolean)


283
284
285
# File 'lib/datadog/tracing/span_operation.rb', line 283

def finished?
  !span.nil?
end

#get_collector_or_initializeObject



144
145
146
# File 'lib/datadog/tracing/span_operation.rb', line 144

def get_collector_or_initialize
  @collector ||= yield
end

#measureObject

Raises:

  • (ArgumentError)


148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/datadog/tracing/span_operation.rb', line 148

def measure
  raise ArgumentError, 'Must provide block to measure!' unless block_given?
  # TODO: Should we just invoke the block and skip tracing instead?
  raise AlreadyStartedError if started?

  return_value = nil

  begin
    # If span fails to start, don't prevent the operation from
    # running, to minimize impact on normal application function.
    begin
      start
    rescue => e
      logger.debug { "Failed to start span: #{e}" }
    ensure
      # We should yield to the provided block when possible, as this
      # block is application code that we don't want to hinder.
      # * We don't yield during a fatal error, as the application is likely trying to
      #   end its execution (either due to a system error or graceful shutdown).
      # @type var e: Exception?
      # Steep: https://github.com/soutaro/steep/issues/919
      return_value = yield(self) unless e && !e.is_a?(StandardError) # steep:ignore FallbackAny
    end
  # rubocop:disable Lint/RescueException
  # Here we really want to catch *any* exception, not only StandardError,
  # as we really have no clue of what is in the block,
  # and it is user code which should be executed no matter what.
  # It's not a problem since we re-raise it afterwards so for example a
  # SignalException::Interrupt would still bubble up.
  rescue Exception => e
    # Stop the span first, so timing is a more accurate.
    # If the span failed to start, timing may be inaccurate,
    # but this is not really a serious concern.
    stop(exception: e)

    # Trigger the on_error event
    events.on_error.publish(self, e)

    # We must finish the span to trigger callbacks,
    # and build the final span.
    finish

    raise e
  # Use an ensure block here to make sure the span closes.
  # NOTE: It's not sufficient to use "else": when a function
  #       uses "return", it will skip "else".
  ensure
    # Finish the span
    # NOTE: If an error was raised, this "finish" might be redundant.
    finish unless finished?
  end
  # rubocop:enable Lint/RescueException

  return_value
end

#pretty_print(q) ⇒ Object

Return a human readable version of the span



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/datadog/tracing/span_operation.rb', line 358

def pretty_print(q)
  start_time = (self.start_time.to_f * 1e9).to_i
  end_time = (self.end_time.to_f * 1e9).to_i
  q.group 0 do
    q.breakable
    q.text "Name: #{@name}\n"
    q.text "Span ID: #{@id}\n"
    q.text "Parent ID: #{@parent_id}\n"
    q.text "Trace ID: #{@trace_id}\n"
    q.text "Type: #{@type}\n"
    q.text "Service: #{@service}\n"
    q.text "Resource: #{@resource}\n"
    q.text "Error: #{@status}\n"
    q.text "Start: #{start_time}\n"
    q.text "End: #{end_time}\n"
    q.text "Duration: #{duration.to_f if stopped?}\n"
    q.group(2, 'Tags: [', "]\n") do
      q.breakable
      q.seplist meta.each do |key, value|
        q.text "#{key} => #{value}"
      end
    end
    q.group(2, 'Metrics: [', "]\n") do
      q.breakable
      q.seplist metrics.each do |key, value|
        q.text "#{key} => #{value}"
      end
    end
    q.group(2, 'Metastruct: [', ']') do
      metastruct.pretty_print(q)
    end
  end
end

#record_exception(exception, attributes: {}) ⇒ void

This method returns an undefined value.

Record an exception during the execution of this span. Multiple exceptions can be recorded on a span.

Parameters:

  • exception (Exception)

    The exception to record

  • attributes (optional Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}) (defaults to: {})

    One or more key:value pairs, where the keys must be strings and the values may be (array of) string, boolean or numeric type.



314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/datadog/tracing/span_operation.rb', line 314

def record_exception(exception, attributes: {})
  exc = Core::Error.build_from(exception)

  event_attributes = {
    'exception.type' => exc.type,
    'exception.message' => exc.message,
    'exception.stacktrace' => exc.backtrace,
  }

  # Steep: Caused by wrong declaration, should be the same parameters as `merge`
  # https://github.com/ruby/rbs/blob/3d0fb3a7fdde60af7120e875fe3bd7237b5b6a88/core/hash.rbs#L1468
  @span_events << SpanEvent.new('exception', attributes: event_attributes.merge!(attributes)) # steep:ignore ArgumentTypeMismatch
end

#root?Boolean

Returns:

  • (Boolean)


252
253
254
# File 'lib/datadog/tracing/span_operation.rb', line 252

def root?
  parent_id == 0
end

#set_error(e) ⇒ Object



299
300
301
302
# File 'lib/datadog/tracing/span_operation.rb', line 299

def set_error(e)
  @status = Metadata::Ext::Errors::STATUS
  set_error_tags(e)
end

#start(start_time = nil) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/datadog/tracing/span_operation.rb', line 204

def start(start_time = nil)
  # Span can only be started once
  return self if started?

  # Trigger before_start event
  events.before_start.publish(self)

  # Start the span
  @start_time = start_time || Core::Utils::Time.now.utc
  @duration_start = start_time.nil? ? duration_marker : nil

  self
end

#started?Boolean

Return whether the duration is started or not

Returns:

  • (Boolean)


243
244
245
# File 'lib/datadog/tracing/span_operation.rb', line 243

def started?
  !@start_time.nil?
end

#stop(stop_time = nil, exception: nil) ⇒ Object

Mark the span stopped at the current time



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/datadog/tracing/span_operation.rb', line 219

def stop(stop_time = nil, exception: nil)
  # A span should not be stopped twice. Note that this is not thread-safe,
  # stop is called from multiple threads, a given span might be stopped
  # several times. Again, one should not do this, so this test is more a
  # fallback to avoid very bad things and protect you in most common cases.
  return if stopped?

  now = Core::Utils::Time.now.utc

  # Provide a default start_time if unset.
  # Using `now` here causes duration to be 0; this is expected
  # behavior when start_time is unknown.
  start(stop_time || now) unless started?

  @end_time = stop_time || now
  @duration_end = stop_time.nil? ? duration_marker : nil

  # Trigger after_stop event
  events.after_stop.publish(self, exception)

  self
end

#stopped?Boolean

Return whether the duration is stopped or not.

Returns:

  • (Boolean)


248
249
250
# File 'lib/datadog/tracing/span_operation.rb', line 248

def stopped?
  !@end_time.nil?
end

#to_hashObject

Return the hash representation of the current span.



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/datadog/tracing/span_operation.rb', line 334

def to_hash
  h = {
    error: @status,
    id: @id,
    meta: meta,
    metrics: metrics,
    metastruct: metastruct,
    name: @name,
    parent_id: @parent_id,
    resource: @resource,
    service: @service,
    trace_id: @trace_id,
    type: @type
  }

  if stopped?
    h[:start] = start_time_nano
    h[:duration] = duration_nano
  end

  h
end

#to_sObject

Return a string representation of the span.



329
330
331
# File 'lib/datadog/tracing/span_operation.rb', line 329

def to_s
  "SpanOperation(name:#{@name},sid:#{@id},tid:#{@trace_id},pid:#{@parent_id})"
end