# encoding: UTF-8

require 'benchmark'
require 'prometheus/client'

module Prometheus
  module Middleware
    # Collector is a Rack middleware that provides a sample implementation of a
    # HTTP tracer.
    #
    # By default metrics are registered on the global registry. Set the
    # `:registry` option to use a custom registry.
    #
    # By default metrics all have the prefix "http_server". Set to something
    # else if you like.
    #
    # The request counter metric is broken down by code, method and path by
    # default. Set the `:counter_label_builder` option to use a custom label
    # builder.
    #
    # The request duration metric is broken down by method and path by default.
    # Set the `:duration_label_builder` option to use a custom label builder.
    class Collector
      attr_reader :app, :registry

      def initialize(app, options = {})
        @app = app
        @registry = options[:registry] || Client.registry
        @metrics_prefix = options[:metrics_prefix] || 'http_server'
        @counter_lb = options[:counter_label_builder] || COUNTER_LB
        @duration_lb = options[:duration_label_builder] || DURATION_LB

        init_request_metrics
        init_exception_metrics
      end

      def call(env) # :nodoc:
        trace(env) { @app.call(env) }
      end

      protected

      aggregation = lambda do |str|
        str
          .gsub(%r{/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(/|$)}, '/:uuid\\1')
          .gsub(%r{/\d+(/|$)}, '/:id\\1')
      end

      COUNTER_LB = proc do |env, code|
        {
          code:   code,
          method: env['REQUEST_METHOD'].downcase,
          path:   aggregation.call(env['PATH_INFO']),
        }
      end

      DURATION_LB = proc do |env, _|
        {
          method: env['REQUEST_METHOD'].downcase,
          path:   aggregation.call(env['PATH_INFO']),
        }
      end

      def init_request_metrics
        @requests = @registry.counter(
          :"#{@metrics_prefix}_requests_total",
          'The total number of HTTP requests handled by the Rack application.',
        )
        @durations = @registry.histogram(
          :"#{@metrics_prefix}_request_duration_seconds",
          'The HTTP response duration of the Rack application.',
        )
      end

      def init_exception_metrics
        @exceptions = @registry.counter(
          :"#{@metrics_prefix}_exceptions_total",
          'The total number of exceptions raised by the Rack application.',
        )
      end

      def trace(env)
        response = nil
        duration = Benchmark.realtime { response = yield }
        record(env, response.first.to_s, duration)
        return response
      rescue => exception
        @exceptions.increment(exception: exception.class.name)
        raise
      end

      def record(env, code, duration)
        @requests.increment(@counter_lb.call(env, code))
        @durations.observe(@duration_lb.call(env, code), duration)
      rescue
        # TODO: log unexpected exception during request recording
        nil
      end
    end
  end
end
