Class: Chronicle::ApiRoutes::Stats

Inherits:
Object
  • Object
show all
Defined in:
app/services/chronicle/api_routes/stats.rb

Constant Summary collapse

SORTABLE_COLUMNS =
%w[
  avg_response_time_ms
  p95_response_time_ms
  p99_response_time_ms
  error_rate_percentage
  total_requests
  unique_users
  requests_per_hour
].freeze
DEFAULT_SORT_BY =
'avg_response_time_ms'.freeze
DEFAULT_SORT_DIRECTION =
'desc'.freeze
DEFAULT_PER_PAGE =
25
MAX_PER_PAGE =
100
SORT_COLUMN_ALIAS =

Aliases that PostgreSQL exposes after the GROUP BY+SELECT; safe to interpolate because they are taken from the SORTABLE_COLUMNS whitelist above.

{
  'avg_response_time_ms' => 'avg_response_time_ms',
  'p95_response_time_ms' => 'p95_response_time_ms',
  'p99_response_time_ms' => 'p99_response_time_ms',
  'error_rate_percentage' => 'error_rate_percentage',
  'total_requests' => 'total_requests',
  'unique_users' => 'unique_users',
  # duration is constant per request so ordering by total_requests is equivalent
  'requests_per_hour' => 'total_requests',
}.freeze
AGGREGATE_SELECT =
[
  'api_endpoint AS path',
  'http_method',
  'COUNT(*) AS total_requests',
  'COUNT(DISTINCT user_id) AS unique_users',
  'ROUND(AVG(response_time_ms)::numeric, 2) AS avg_response_time_ms',
  'ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms)::numeric, 2) AS p95_response_time_ms',
  'ROUND(PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY response_time_ms)::numeric, 2) AS p99_response_time_ms',
  'ROUND(100.0 * COUNT(CASE WHEN http_status_code BETWEEN 400 AND 599 THEN 1 END) / COUNT(*), 2)
 AS error_rate_percentage',
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(filters: {}, sort_by: nil, sort_direction: nil, page: 1, per_page: DEFAULT_PER_PAGE) ⇒ Stats

Returns a new instance of Stats.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'app/services/chronicle/api_routes/stats.rb', line 44

def initialize(filters: {}, sort_by: nil, sort_direction: nil, page: 1, per_page: DEFAULT_PER_PAGE)
  filters         = filters.to_unsafe_h.symbolize_keys if filters.respond_to?(:to_unsafe_h)
  @filters        = filters.symbolize_keys
  @sort_by        = SORTABLE_COLUMNS.include?(sort_by.to_s) ? sort_by.to_s : DEFAULT_SORT_BY
  @sort_direction = if %w[asc
                          desc].include?(sort_direction.to_s.downcase)
                      sort_direction.to_s.downcase
                    else
                      DEFAULT_SORT_DIRECTION
                    end
  @page           = [page.to_i, 1].max
  @per_page = per_page.to_i.clamp(1, MAX_PER_PAGE)
  @start_time, @end_time = normalize_date_range
end

Instance Method Details

#callObject



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'app/services/chronicle/api_routes/stats.rb', line 59

def call
  base    = filtered_scope
  total   = base.group(:api_endpoint, :http_method).count.size
  records = stats_scope(base)
            .order(Arel.sql("#{SORT_COLUMN_ALIAS[@sort_by]} #{@sort_direction.upcase} NULLS LAST"))
            .limit(@per_page)
            .offset((@page - 1) * @per_page)

  duration_hours = [(@end_time - @start_time) / 3600.0, 1].max

  {
    data: records.map { |row| serialize(row, duration_hours) },
    pagination: {
      total_count: total,
      page: @page,
      per_page: @per_page,
      total_pages: (total.to_f / @per_page).ceil,
    },
  }
end