Class: Chronicle::ApiRoutes::Stats
- Inherits:
-
Object
- Object
- Chronicle::ApiRoutes::Stats
- 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
- #call ⇒ Object
-
#initialize(filters: {}, sort_by: nil, sort_direction: nil, page: 1, per_page: DEFAULT_PER_PAGE) ⇒ Stats
constructor
A new instance of Stats.
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
#call ⇒ Object
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 |