Class: Tasker::Telemetry::MetricTypes::Histogram

Inherits:
Object
  • Object
show all
Defined in:
lib/tasker/telemetry/metric_types.rb

Overview

Histogram represents a metric that samples observations and counts them in buckets

Histograms are thread-safe and provide statistical analysis of observed values. They track count, sum, and bucket distributions for duration and size metrics.

Examples:

Production usage

histogram = Histogram.new('task_duration_seconds', buckets: [0.1, 0.5, 1.0, 5.0])
histogram.observe(0.45)            # Record a 0.45 second duration
histogram.observe(2.1)             # Record a 2.1 second duration
histogram.count                    # Total observations
histogram.sum                      # Sum of all observed values
histogram.buckets                  # Bucket counts

Constant Summary collapse

DEFAULT_BUCKETS =

Default bucket boundaries for duration metrics (in seconds)

[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, labels: {}, buckets: DEFAULT_BUCKETS) ⇒ Histogram

Initialize a new histogram metric

Parameters:

  • name (String)

    The metric name (must be present)

  • labels (Hash) (defaults to: {})

    Optional labels for dimensional metrics

  • buckets (Array<Numeric>) (defaults to: DEFAULT_BUCKETS)

    Bucket boundaries (must be sorted ascending)

Raises:

  • (ArgumentError)

    If name is nil/empty or buckets are invalid



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/tasker/telemetry/metric_types.rb', line 287

def initialize(name, labels: {}, buckets: DEFAULT_BUCKETS)
  raise ArgumentError, 'Metric name cannot be nil or empty' if name.nil? || name.strip.empty?
  raise ArgumentError, 'Buckets must be an array' unless buckets.is_a?(Array)
  raise ArgumentError, 'Buckets cannot be empty' if buckets.empty?

  @name = name.to_s.freeze
  @labels = labels.freeze
  @bucket_boundaries = buckets.sort.freeze
  @created_at = Time.current.freeze

  # Thread-safe counters for each bucket + infinity bucket
  @bucket_counts = (@bucket_boundaries + [Float::INFINITY]).map do |_|
    Concurrent::AtomicFixnum.new(0)
  end

  @count = Concurrent::AtomicFixnum.new(0)
  @sum = Concurrent::AtomicReference.new(0.0)
end

Instance Attribute Details

#bucket_boundariesArray<Numeric> (readonly)

Returns The histogram bucket boundaries.

Returns:

  • (Array<Numeric>)

    The histogram bucket boundaries



273
274
275
# File 'lib/tasker/telemetry/metric_types.rb', line 273

def bucket_boundaries
  @bucket_boundaries
end

#created_atTime (readonly)

Returns When this metric was first created.

Returns:

  • (Time)

    When this metric was first created



276
277
278
# File 'lib/tasker/telemetry/metric_types.rb', line 276

def created_at
  @created_at
end

#labelsHash (readonly)

Returns The metric labels for dimensional data.

Returns:

  • (Hash)

    The metric labels for dimensional data



270
271
272
# File 'lib/tasker/telemetry/metric_types.rb', line 270

def labels
  @labels
end

#nameString (readonly)

Returns The metric name.

Returns:

  • (String)

    The metric name



267
268
269
# File 'lib/tasker/telemetry/metric_types.rb', line 267

def name
  @name
end

Instance Method Details

#averageFloat

Calculate the average of observed values

Returns:

  • (Float)

    Average value, or 0.0 if no observations



358
359
360
361
362
363
# File 'lib/tasker/telemetry/metric_types.rb', line 358

def average
  current_count = count
  return 0.0 if current_count.zero?

  sum.to_f / current_count
end

#bucketsHash

Get the current bucket counts

Returns:

  • (Hash)

    Bucket boundaries to counts mapping



346
347
348
349
350
351
352
353
# File 'lib/tasker/telemetry/metric_types.rb', line 346

def buckets
  result = {}
  @bucket_boundaries.each_with_index do |boundary, index|
    result[boundary] = @bucket_counts[index].value
  end
  result[Float::INFINITY] = @bucket_counts.last.value
  result
end

#countInteger

Get the total number of observations

Returns:

  • (Integer)

    Total observation count



332
333
334
# File 'lib/tasker/telemetry/metric_types.rb', line 332

def count
  @count.value
end

#descriptionString

Get a description of this metric for debugging

Returns:

  • (String)

    Human-readable description



393
394
395
396
# File 'lib/tasker/telemetry/metric_types.rb', line 393

def description
  label_str = labels.empty? ? '' : format_labels_for_description(labels)
  "#{name}#{label_str} = #{count} observations, avg: #{average.round(3)} (histogram)"
end

#observe(value) ⇒ Numeric

Observe a value and update histogram buckets

Parameters:

  • value (Numeric)

    The observed value

Returns:

  • (Numeric)

    The observed value (for chaining)

Raises:

  • (ArgumentError)

    If value is not numeric



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/tasker/telemetry/metric_types.rb', line 311

def observe(value)
  raise ArgumentError, "Observed value must be numeric, got: #{value.class}" unless value.is_a?(Numeric)

  # Update count and sum atomically
  @count.increment
  @sum.update { |current| current + value }

  # Increment all buckets where value <= boundary (cumulative histogram)
  @bucket_boundaries.each_with_index do |boundary, index|
    @bucket_counts[index].increment if value <= boundary
  end

  # Always increment the infinity bucket (total count)
  @bucket_counts.last.increment

  value
end

#reset!void

This method returns an undefined value.

Reset the histogram (primarily for testing)



384
385
386
387
388
# File 'lib/tasker/telemetry/metric_types.rb', line 384

def reset!
  @count.value = 0
  @sum.set(0.0)
  @bucket_counts.each { |bucket| bucket.value = 0 }
end

#sumNumeric

Get the sum of all observed values

Returns:

  • (Numeric)

    Sum of observations



339
340
341
# File 'lib/tasker/telemetry/metric_types.rb', line 339

def sum
  @sum.get
end

#to_hHash

Get a hash representation of this metric

Returns:

  • (Hash)

    Metric data including name, labels, buckets, count, sum, type



368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/tasker/telemetry/metric_types.rb', line 368

def to_h
  {
    name: name,
    labels: labels,
    buckets: buckets,
    count: count,
    sum: sum,
    average: average,
    type: :histogram,
    created_at: created_at
  }
end