class Labkit::ExconPublisher

A middleware for Excon HTTP library to publish a notification whenever a HTTP request is triggered.

Excon supports a middleware system that allows request/response interception freely. Whenever a new Excon connection is created, a list of default middlewares is injected. This list of middlewares can be altered thanks to Excon.defaults accessor. ExconPublisher is inserted into this list. It affects all connections created in future. There is a limitation that this approach doesn't work if a user decides to override the default middleware list. It is unlikely though, at least in the dependency tree of GitLab.

ExconPublisher instance is created once and shared between all Excon connections later. Each connection may be triggered by different threads in parallel. In such cases, a connection objects creates multiple sockets for each thread. Therfore in the implementation of this middleware, the instrumation payload for each connection is stored inside a thread-isolated storage.

For more information: github.com/excon/excon/blob/81a0130537f2f8cd00d6daafb05d02d9a90dc9f7/lib/excon/middlewares/base.rb github.com/excon/excon/blob/fa3ec51e9bb062a12846a1cfff09534e76c99f4b/lib/excon/constants.rb#L146 github.com/excon/excon/blob/fa3ec51e9bb062a12846a1cfff09534e76c99f4b/lib/excon/connection.rb#L474

Public Class Methods

labkit_prepend!() click to toggle source
# File lib/labkit/excon_publisher.rb, line 31
def self.labkit_prepend!
  @prepend_mutex.synchronize do
    return if !defined?(Excon) || @prepended

    defaults = Excon.defaults
    defaults[:middlewares] << ExconPublisher

    @prepended = true
  end
end
new(stack) click to toggle source
# File lib/labkit/excon_publisher.rb, line 42
def initialize(stack)
  @stack = stack
  @instrumenter = ActiveSupport::Notifications.instrumenter
end

Public Instance Methods

error_call(datum) click to toggle source
# File lib/labkit/excon_publisher.rb, line 68
def error_call(datum)
  payload = fetch_connection_payload(datum)

  return @stack.error_call(datum) if payload.nil?

  calculate_duration(payload)

  if datum[:error].is_a?(Exception)
    payload[:exception] = [datum[:error].class.name, datum[:error].message]
    payload[:exception_object] = datum[:error]
  elsif datum[:error].is_a?(String)
    exception = StandardError.new(datum[:error])
    payload[:exception] = [exception.class.name, exception.message]
    payload[:exception_object] = exception
  end

  @instrumenter.finish(::Labkit::EXTERNAL_HTTP_NOTIFICATION_TOPIC, payload)
  @stack.error_call(datum)
ensure
  remove_connection_payload(datum)
end
request_call(datum) click to toggle source
# File lib/labkit/excon_publisher.rb, line 47
def request_call(datum)
  payload = start_payload(datum)
  store_connection_payload(datum, payload)
  @instrumenter.start(::Labkit::EXTERNAL_HTTP_NOTIFICATION_TOPIC, payload)
  @stack.request_call(datum)
end
response_call(datum) click to toggle source
# File lib/labkit/excon_publisher.rb, line 54
def response_call(datum)
  payload = fetch_connection_payload(datum)

  return @stack.response_call(datum) if payload.nil?

  calculate_duration(payload)
  payload[:code] = datum[:response][:status].to_s

  @instrumenter.finish(::Labkit::EXTERNAL_HTTP_NOTIFICATION_TOPIC, payload)
  @stack.response_call(datum)
ensure
  remove_connection_payload(datum)
end

Private Instance Methods

calculate_duration(payload) click to toggle source
# File lib/labkit/excon_publisher.rb, line 111
def calculate_duration(payload)
  start_time = payload.delete(:start_time) || ::Labkit::System.monotonic_time
  payload[:duration] = (::Labkit::System.monotonic_time - start_time).to_f
end
connection_payload() click to toggle source
# File lib/labkit/excon_publisher.rb, line 116
def connection_payload
  Thread.current[:__labkit_http_excon_payload] ||= {}
end
fetch_connection_payload(datum) click to toggle source
# File lib/labkit/excon_publisher.rb, line 124
def fetch_connection_payload(datum)
  connection_payload.fetch(datum[:connection], nil)
end
generate_query_string(query) click to toggle source
# File lib/labkit/excon_publisher.rb, line 142
def generate_query_string(query)
  if query.is_a?(Hash)
    query.to_query
  else
    nil_or_string(query)
  end
end
nil_or_int(int) click to toggle source
# File lib/labkit/excon_publisher.rb, line 136
def nil_or_int(int)
  int&.to_i
rescue StandardError
  nil
end
nil_or_string(str) click to toggle source
# File lib/labkit/excon_publisher.rb, line 132
def nil_or_string(str)
  str&.to_s
end
remove_connection_payload(datum) click to toggle source
# File lib/labkit/excon_publisher.rb, line 128
def remove_connection_payload(datum)
  connection_payload.delete(datum[:connection])
end
start_payload(datum) click to toggle source
# File lib/labkit/excon_publisher.rb, line 92
def start_payload(datum)
  payload = {
    method: datum[:method].to_s.upcase,
    host: nil_or_string(datum[:host]),
    path: nil_or_string(datum[:path]),
    port: nil_or_int(datum[:port]),
    scheme: nil_or_string(datum[:scheme]),
    query: generate_query_string(datum[:query]),
    start_time: ::Labkit::System.monotonic_time,
  }

  unless datum[:proxy].nil?
    payload[:proxy_host] = datum[:proxy][:host]
    payload[:proxy_port] = datum[:proxy][:port]
  end

  payload
end
store_connection_payload(datum, payload) click to toggle source
# File lib/labkit/excon_publisher.rb, line 120
def store_connection_payload(datum, payload)
  connection_payload[datum[:connection]] = payload
end