lib/middleware/metrics_middleware.rb



module ZuoraConnect
  require 'uri'

  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) }

              ZuoraObservability::Metrics.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