Class: OpenC3::Metric

Inherits:
Object show all
Defined in:
lib/openc3/utilities/metric.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(microservice:, scope:) ⇒ Metric

Returns a new instance of Metric.



46
47
48
49
50
51
52
53
54
55
56
# File 'lib/openc3/utilities/metric.rb', line 46

def initialize(microservice:, scope:)
  if microservice.include? '|' or scope.include? '|'
    raise ArgumentError.new('invalid input must not contain: |')
  end

  @items = {}
  @scope = scope
  @microservice = microservice
  @size = 5000
  @mutex = Mutex.new
end

Instance Attribute Details

#itemsObject (readonly)

This class is designed to output metrics to the openc3-cosmos-cmd-tlm-api InternalMetricsController. Output format can be read about here prometheus.io/docs/concepts/data_model/

Warning contains some sorcery.

examples:

TYPE foobar histogram
HELP foobar internal metric generated from openc3/utilities/metric.rb
foobar{code="200",method="get",path="/metrics"} 5.0

items = => [value_array], …



41
42
43
# File 'lib/openc3/utilities/metric.rb', line 41

def items
  @items
end

#microserviceObject (readonly)

Returns the value of attribute microservice.



44
45
46
# File 'lib/openc3/utilities/metric.rb', line 44

def microservice
  @microservice
end

#scopeObject (readonly)

Returns the value of attribute scope.



43
44
45
# File 'lib/openc3/utilities/metric.rb', line 43

def scope
  @scope
end

#sizeObject

Returns the value of attribute size.



42
43
44
# File 'lib/openc3/utilities/metric.rb', line 42

def size
  @size
end

Instance Method Details

#add_sample(name:, value:, labels:) ⇒ Object



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
# File 'lib/openc3/utilities/metric.rb', line 58

def add_sample(name:, value:, labels:)
  # add a value to the metric to report out later or a seperate thread
  # name is a string often function_name_duration_seconds
  #    name: debug_duration_seconds
  # value is a numerical value to add to a round robin array.
  #    value: 0.1211
  # labels is a hash of values that could have an effect on the value.
  #    labels: {"code"=>200,"method"=>"get","path"=>"/metrics"}
  # internal:
  # the internal items hash is used as a lookup table to store unique
  # varients of a similar metric. these varients are values that could
  # cause a difference in run time to add context to the metric. the
  # microservice and scope are added to the labels the labels are
  # converted to a string and joined with the name to create a unique
  # metric item. this is looked up in the items hash and if not found
  # the key is created and an array of size @size is allocated. then
  # the value is added to @items and the count of the value is increased
  # if the count of the values exceed the size of the array it sets the
  # count back to zero and the array will over write older data.
  @mutex.synchronize do
    key = "#{name}|" + labels.map { |k, v| "#{k}=#{v}" }.join(',')
    if not @items.has_key?(key)
      Logger.debug("new data for #{@scope}, #{key}")
      @items[key] = { 'values' => Array.new(@size), 'count' => 0 }
    end
    count = @items[key]['count']
    # Logger.info("adding data for #{@scope}, #{count} #{key}, #{value}")
    @items[key]['values'][count] = value
    @items[key]['count'] = count + 1 >= @size ? 0 : count + 1
  end
end

#destroyObject



140
141
142
# File 'lib/openc3/utilities/metric.rb', line 140

def destroy
  MetricModel.destroy(scope: @scope, name: @microservice)
end

#outputObject



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/openc3/utilities/metric.rb', line 100

def output
  # Output percentile based metrics to Redis under the key of the
  # #{@scope}__openc3__metric we will use hset with a subkey.
  # internal:
  # loop over the key value pairs within the @items hash, remove nil
  # and sort the values added via the add_sample method. calculate the
  # different percentiles. the labels are still only contained in the
  # key of the @items hash to extract these you split the key on | the
  # name|labels then to make the labels back into a hash we split the ,
  # into an array ["foo=bar", ...] and split again this time on the =
  # into [["foo","bar"], ...] to map the internal array into a hash
  # [{"foo"=>"bar"}, ...] finally reducing the array into a single hash
  # to add the percentile and percentile value. this hash is added to an
  # array. to store the array as the value with the metric name again joined
  # with the @microservice and @scope.
  Logger.debug("#{@microservice} #{@scope} sending metrics to redis, #{@items.length}") if @items.length > 0
  @mutex.synchronize do
    @items.each do |key, values|
      label_list = []
      name, labels = key.split('|')
      metric_labels = labels.nil? ? {} : labels.split(',').map { |x| x.split('=') }.map { |k, v| { k => v } }.reduce({}, :merge)
      sorted_values = values['values'].compact.sort
      for percentile_value in [10, 50, 90, 95, 99]
        percentile_result = percentile(sorted_values, percentile_value)
        labels = metric_labels.clone.merge({ 'scope' => @scope, 'microservice' => @microservice })
        labels['percentile'] = percentile_value
        labels['metric__value'] = percentile_result
        label_list.append(labels)
      end
      begin
        Logger.debug("sending metrics summary to redis key: #{@microservice}")
        metric = MetricModel.new(name: @microservice, scope: @scope, metric_name: name, label_list: label_list)
        metric.create(force: true)
      rescue RuntimeError
        Logger.error("failed attempt to update metric, #{key}, #{name} #{@scope}")
      end
    end
  end
end

#percentile(sorted_values, percentile) ⇒ Object



90
91
92
93
94
95
96
97
98
# File 'lib/openc3/utilities/metric.rb', line 90

def percentile(sorted_values, percentile)
  # get the percentile out of an ordered array
  len = sorted_values.length
  return sorted_values.first if len == 1

  k = ((percentile / 100.0) * (len - 1) + 1).floor - 1
  f = ((percentile / 100.0) * (len - 1) + 1).modulo(1)
  return sorted_values[k] + (f * (sorted_values[k + 1] - sorted_values[k]))
end