Module: OpenTelemetry::Exporter::OTLP::Metrics::Util

Included in:
MetricsExporter
Defined in:
lib/opentelemetry/exporter/otlp/metrics/util.rb

Overview

Util module provide essential functionality for exporter

Constant Summary collapse

KEEP_ALIVE_TIMEOUT =
30
RETRY_COUNT =
5
ERROR_MESSAGE_INVALID_HEADERS =
'headers must be a String with comma-separated URL Encoded UTF-8 k=v pairs or a Hash'
DEFAULT_USER_AGENT =
"OTel-OTLP-MetricsExporter-Ruby/#{OpenTelemetry::Exporter::OTLP::Metrics::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze

Instance Method Summary collapse

Instance Method Details

#around_request ⇒ Object

Yields the block with OpenTelemetry instrumentation suppressed.



31
32
33
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 31

def around_request
  OpenTelemetry::Common::Utilities.untraced { yield } # rubocop:disable Style/ExplicitBlockArgument
end

#as_otlp_any_value(value) ⇒ Object

Converts a Ruby value to its OTLP proto AnyValue representation.



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 46

def as_otlp_any_value(value)
  result = Opentelemetry::Proto::Common::V1::AnyValue.new
  case value
  when String
    result.string_value = OpenTelemetry::Common::Utilities.utf8_encode(value, placeholder: value)
  when Integer
    result.int_value = value
  when Float
    result.double_value = value
  when true, false
    result.bool_value = value
  when Array
    values = value.map { |element| as_otlp_any_value(element) }
    result.array_value = Opentelemetry::Proto::Common::V1::ArrayValue.new(values: values)
  end
  result
end

#as_otlp_key_value(key, value) ⇒ Object

Converts a key/value pair to an OTLP proto KeyValue.



36
37
38
39
40
41
42
43
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 36

def as_otlp_key_value(key, value)
  key = OpenTelemetry::Common::Utilities.utf8_encode(key, placeholder: 'Encoding Error')
  Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value(value))
rescue Encoding::UndefinedConversionError => e
  encoded_value = value.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '�')
  OpenTelemetry.handle_error(exception: e, message: "encoding error for key #{key} and value #{encoded_value}")
  Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value('Encoding Error'))
end

#backoff?(retry_count:, reason:, retry_after: nil) ⇒ Boolean

Sleeps for a computed backoff interval and returns whether a retry should happen.

Returns:

  • (Boolean)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 100

def backoff?(retry_count:, reason:, retry_after: nil)
  return false if retry_count > RETRY_COUNT

  sleep_interval = nil
  unless retry_after.nil?
    sleep_interval =
      Integer(retry_after, exception: false)
    sleep_interval ||=
      begin
        Time.httpdate(retry_after) - Time.now
      rescue # rubocop:disable Style/RescueStandardError
        nil
      end
    sleep_interval = nil unless sleep_interval&.positive?
  end
  sleep_interval ||= rand(2**retry_count)

  sleep(sleep_interval)
  true
end

#handle_redirect(location) ⇒ Object

No-op: HTTP redirects are not followed.



136
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 136

def handle_redirect(location); end

#http_connection(uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file) ⇒ Object

Builds a configured Net::HTTP connection for the given URI.



19
20
21
22
23
24
25
26
27
28
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 19

def http_connection(uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
  http = Net::HTTP.new(uri.hostname, uri.port)
  http.use_ssl = uri.scheme == 'https'
  http.verify_mode = ssl_verify_mode
  http.ca_file = certificate_file unless certificate_file.nil?
  http.cert = OpenSSL::X509::Certificate.new(File.read(client_certificate_file)) unless client_certificate_file.nil?
  http.key = OpenSSL::PKey::RSA.new(File.read(client_key_file)) unless client_key_file.nil?
  http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
  http
end

#log_status(body) ⇒ Object

Decodes and logs an rpc.Status error response body.



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 122

def log_status(body)
  status = Google::Rpc::Status.decode(body)
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
  details = status.details.filter_map do |detail|
    type_name = detail.type_url.to_s.split('/').last.to_s
    klass = pool.lookup(type_name)&.msgclass
    detail.unpack(klass) if klass
  end
  OpenTelemetry.handle_error(message: "OTLP metrics_exporter received rpc.Status{message=#{status.message}, details=#{details}}")
rescue StandardError => e
  OpenTelemetry.handle_error(exception: e, message: 'unexpected error decoding rpc.Status in OTLP::MetricsExporter#log_status')
end

#parse_headers(raw) ⇒ Object

Parses a comma-separated, URL-encoded header string into a Hash.

Raises:

  • (ArgumentError)


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 79

def parse_headers(raw)
  entries = raw.split(',')
  raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if entries.empty?

  entries.each_with_object({}) do |entry, headers|
    k, v = entry.split('=', 2).map { |part| URI.decode_uri_component part }
    begin
      k = k.to_s.strip
      v = v.to_s.strip
    rescue Encoding::CompatibilityError
      raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
    rescue ArgumentError => e
      raise e, ERROR_MESSAGE_INVALID_HEADERS
    end
    raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if k.empty? || v.empty?

    headers[k] = v
  end
end

#prepare_headers(config_headers) ⇒ Object

Normalizes config_headers into a Hash, appending the default User-Agent.



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/opentelemetry/exporter/otlp/metrics/util.rb', line 65

def prepare_headers(config_headers)
  headers = case config_headers
            when String then parse_headers(config_headers)
            when Hash then config_headers.dup
            else
              raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
            end

  headers['User-Agent'] = "#{headers.fetch('User-Agent', '')} #{DEFAULT_USER_AGENT}".strip

  headers
end