module ZuoraConnect require 'uri' # Object of this class is passed to the ActiveSupport::Notification hook class PageRequest # This method is triggered when a non error page is loaded (not 404) def call(name, started, finished, unique_id, payload) # If the url contains any css or JavaScript files then do not collect metrics for them return nil if ["css", "assets", "jpg", "png", "jpeg", "ico"].any? { |word| payload[:path].include?(word) } # Getting the endpoint and the content_type content_hash = {:html => "text/html", :js => "application/javascript", :json => "application/json", :csv => "text/csv"} content_type = content_hash.key?(payload[:format]) ? content_hash[payload[:format]] : payload[:format] content_type = content_type.to_s.gsub('text/javascript', 'application/javascript') # payloads with 500 requests do not have status as it is not set by the controller # https://github.com/rails/rails/issues/33335 #status_code = payload[:status] ? payload[:status] : payload[:exception_object].present? ? 500 : "" if payload[:exception].present? status_code, exception = [500, payload[:exception].first] else status_code, exception = [payload[:status], nil] end tags = {method: payload[:method], status: status_code, error_type: exception, content_type: content_type, controller: payload[:controller], action: payload[:action]}.compact values = {view_time: payload[:view_runtime], db_time: payload[:db_runtime], response_time: ((finished-started)*1000)}.compact values = values.map{ |k,v| [k,v.round(2)]}.to_h ZuoraConnect::AppInstanceBase.write_to_telegraf(direction: :inbound, tags: tags, values: values) end end class MetricsMiddleware require "zuora_connect/version" require "zuora_api/version" def initialize(app) @app = app end def call(env) @bad_headers = ["HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED_HOST", "HTTP_X_FORWARDED_PORT", "HTTP_X_FORWARDED_PROTO", "HTTP_X_FORWARDED_SCHEME", "HTTP_X_FORWARDED_SSL"] if !ActionDispatch::Request::HTTP_METHODS.include?(env["REQUEST_METHOD"].upcase) [405, {"Content-Type" => "text/plain"}, ["Method Not Allowed"]] else if (env['HTTP_ZUORA_LAYOUT_FETCH_TEMPLATE_ID'].present?) Thread.current[:isHallway] = "/#{env['HTTP_ZUORA_LAYOUT_FETCH_TEMPLATE_ID']}" env['PATH_INFO'] = env['PATH_INFO'].gsub(Thread.current[:isHallway], '') env['REQUEST_URI'] = env['REQUEST_URI'].gsub(Thread.current[:isHallway], '') env['REQUEST_PATH'] = env['REQUEST_PATH'].gsub(Thread.current[:isHallway], '') #We need the forwarded host header to identify location of tenant whitelist = Regexp.new(".*[\.]zuora[\.]com$|^zuora[\.]com$") if whitelist.match(env['HTTP_X_FORWARDED_HOST']).present? @bad_headers.delete('HTTP_X_FORWARDED_HOST') end else Thread.current[:isHallway] = nil end #Remove bad headers @bad_headers.each { |header| env.delete(header) } if defined?(Prometheus) && env['PATH_INFO'] == '/connect/internal/metrics' # Prometheus Stuff metrics = ZuoraObservability::Metrics.resque metrics = defined?(ZuoraConnect::AppInstance.get_metrics) ? ZuoraConnect::AppInstance.get_metrics(metrics) : metrics redis_up = metrics.present? && metrics.dig(:Resque, :Workers_Total).present? ? 1 : 0 Prometheus::REDIS_CONNECTION.set(redis_up) process_prometheus_metric(metrics: metrics) if defined?(Unicorn) && Unicorn.respond_to?(:listener_names) ZuoraObservability::Metrics.unicorn_listener.each do |key, value| gauge = Prometheus.const_get("unicorn_#{key}".gsub(/[^a-zA-Z0-9_]/, '_').upcase) gauge.set(value) if gauge.present? end end end #Thread.current[:appinstance] = nil start_time = Time.now begin @status, @headers, @response = @app.call(env) ensure # Uncomment following block of code for handling engine requests/requests without controller # else # # Handling requests which do not have controllers (engines) if env["SCRIPT_NAME"].present? controller_path = "#{env['SCRIPT_NAME'][1..-1]}" controller_path = controller_path.sub("/", "::") request_path = "#{controller_path}#UnknownAction" else # Writing to telegraf: Handle 404 if [404, 500].include?(@status) content_type = @headers['Content-Type'].split(';')[0] if @headers['Content-Type'] content_type = content_type.gsub('text/javascript', 'application/javascript') tags = {status: @status, content_type: content_type} tags = tags.merge({controller: 'ActionController'}) tags = tags.merge({action: 'RoutingError' }) if @status == 404 values = {response_time: ((Time.now - start_time)*1000).round(2) } ZuoraConnect::AppInstanceBase.write_to_telegraf(direction: :inbound, tags: tags, values: values) end end Thread.current[:inbound_metric] = nil end [@status, @headers, @response] end end def process_prometheus_metric(type: 'none', metrics: {}) return if metrics.blank? prometheus = Prometheus::Client.registry most_recent_aggregation = {} if Prometheus::Client.config.data_store.is_a?(Prometheus::Client::DataStores::DirectFileStore) most_recent_aggregation[:aggregation] = :most_recent end metrics.each do |key, value| next if %w[app_name url].include?(key.to_s) if value.is_a?(Hash) process_prometheus_metric(type: key.to_s, metrics: value) else gauge_name = key.to_s.downcase.gsub(/[^a-z0-9_]/, '_') gauge = prometheus.get(gauge_name.to_sym) || prometheus.gauge( gauge_name.to_sym, docstring: "#{key} metric", labels: %i(type name), preset_labels: { type: type, name: ZuoraObservability::Env.app_name }, store_settings: most_recent_aggregation ) gauge.set(value) end end end end end