Skip to content

Add Prometheus metrics endpoint with HTTP Basic Auth - #437

Open
vutratenko wants to merge 7 commits into
vol1ura:masterfrom
vutratenko:feature/prometheus-metrics
Open

Add Prometheus metrics endpoint with HTTP Basic Auth#437
vutratenko wants to merge 7 commits into
vol1ura:masterfrom
vutratenko:feature/prometheus-metrics

Conversation

@vutratenko

Copy link
Copy Markdown
Contributor

Summary

  • New endpoint GET /metrics with Prometheus Text Exposition Format
  • HTTP Basic Auth using PROMETHEUS_USERNAME/PROMETHEUS_PASSWORD env vars
  • Returns 503 if credentials not configured
  • All metrics cached with 5-minute TTL using Rails.cache
  • Comprehensive test coverage with RSpec
  • Documentation added to docs/prometheus_metrics.md

Metrics Implemented

Event Metrics

  • s95_events_total{country,active}
  • s95_activities_total{event,country,published}
  • s95_activity_results_total{event,activity_date}
  • s95_activity_volunteers_total{event,activity_date}
  • s95_activity_first_runs_total{event,activity_date}
  • s95_activity_personal_bests_total{event,activity_date}

Athlete Metrics

  • s95_athletes_total{event,country}
  • s95_athletes_with_user_total{event}
  • s95_athletes_with_gender_total{event,gender}
  • s95_athletes_with_external_code_total{source}
  • s95_athletes_going_to_event_total{event}

Result Metrics

  • s95_activity_average_time_seconds{event,activity_date}
  • s95_activity_median_time_seconds{event,activity_date}
  • s95_activity_best_time_seconds{event,activity_date,gender}
  • s95_activity_pb_ratio{event,activity_date}
  • s95_activity_first_run_ratio{event,activity_date}
  • s95_activity_correct{event,activity_date}

Volunteer Metrics

  • s95_volunteers_total{event,activity_date}
  • s95_volunteers_by_role_total{event,activity_date,role}
  • s95_unique_volunteers_total{event,window}
  • s95_volunteer_roles_covered_total{event,activity_date}
  • s95_volunteer_position_coverage_ratio{event,activity_date}
  • s95_volunteer_bus_factor{event}

Location Health Metrics

  • s95_active_community_total{event,window}
  • s95_unique_athletes_total{event,window}
  • s95_returning_athletes_total{event,window}
  • s95_sleeping_athletes_total{event,window}
  • s95_location_health_score{event}

Data Quality Metrics

  • s95_activity_has_results{event,activity_date}
  • s95_activity_published{event,activity_date}
  • s95_incorrect_results_total{event}
  • s95_doubled_results_total{event,activity_date}
  • s95_uninformed_results_total{event}
  • s95_uninformed_volunteers_total{event}

Configuration

Set these environment variables to enable the metrics endpoint:

  • PROMETHEUS_USERNAME
  • PROMETHEUS_PASSWORD

Testing

Run tests with: bundle exec rspec

Documentation

See docs/prometheus_metrics.md for detailed metric descriptions, labels, and Prometheus configuration examples.

- New endpoint GET /metrics with Prometheus Text Exposition Format
- HTTP Basic Auth using PROMETHEUS_USERNAME/PROMETHEUS_PASSWORD env vars
- Returns 503 if credentials not configured
- All metrics cached with 5-minute TTL using Rails.cache
- Implemented services:
  * Metrics::S95Collector - main metrics aggregator
  * Metrics::LocationHealthCalculator - health score calculations
  * Metrics::VolunteerBusFactorCalculator - bus factor calculations
- Metrics implemented:
  * Event metrics (s95_events_total, s95_activities_total, etc.)
  * Athlete metrics (s95_athletes_total, s95_athletes_with_user_total, etc.)
  * Result metrics (s95_activity_average_time_seconds, s95_activity_median_time_seconds, etc.)
  * Volunteer metrics (s95_volunteers_total, s95_volunteer_bus_factor, etc.)
  * Location health metrics (s95_location_health_score, active_community, etc.)
  * Data quality metrics (s95_activity_correct, s95_incorrect_results_total, etc.)
- Comprehensive test coverage with RSpec
- Documentation added to docs/prometheus_metrics.md
Comment thread app/controllers/metrics_controller.rb Fixed
Comment thread docs/prometheus_metrics.md Outdated
Comment thread app/controllers/metrics_controller.rb Outdated
Comment thread Gemfile Outdated

@vol1ura vol1ura left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А где будет инстанс прометеуса?

vutratenko and others added 2 commits July 9, 2026 13:15
Remove the unused prometheus-client gem and initializer so CI no longer fails due to Gemfile/Gemfile.lock mismatch in deployment mode.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use nested module style, sum, explicit parentheses, and trailing comma to satisfy CI lint checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vutratenko

Copy link
Copy Markdown
Contributor Author

А где будет инстанс прометеуса?

Вариантов масса - пока была мысль забрать своим, построить дашборды и показать как это работает. А там либо рядом поднять в каком-то виде, или на отдельной VDS. В том же клауд.ру бесплатную взять - её хватит

Switch metrics auth to Authorization header token, remove unnecessary CSRF skip and docs file, and fix LocationHealthCalculator indentation so lint-ruby passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vutratenko

Copy link
Copy Markdown
Contributor Author

Поправил по ревью:

  • убрал skip_before_action :verify_authenticity_token — для GET он не нужен
  • auth перевёл на заголовок Authorization с токеном из PROMETHEUS_TOKEN (по аналогии с API::Parkzhrun::ApplicationController)
  • удалил docs/prometheus_metrics.md
  • починил отступы в LocationHealthCalculator для lint-ruby

По вопросу про инстанс Prometheus: в этом PR только endpoint /metrics в приложении. Сам Prometheus — внешний scraper (например, уже существующий monitoring stack), который ходит на приложение по расписанию. Пример scrape config:

scrape_configs:
  - job_name: sat_9am_5km
    metrics_path: /metrics
    authorization:
      credentials: <PROMETHEUS_TOKEN>
    static_configs:
      - targets: ['app.example.com']

Если нужно, могу отдельным PR добавить deploy-конфиг для Prometheus.

@vutratenko
vutratenko force-pushed the feature/prometheus-metrics branch from 3ccb1af to 91c241c Compare July 9, 2026 17:16

@vol1ura vol1ura left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сейчас возникли очень большие сомнения по поводу производительности, можно положить сайт такими запросами

Особо подробно пока не смотрел, возможно что-то можно оптимизировать

private

def calculate_activity_frequency_factor
six_months_ago = Date.current - 6.months

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

можно без переменной, сразу 6.months.ago.to_date

def calculate_activity_frequency_factor
six_months_ago = Date.current - 6.months
recent_activities = Activity.published.where(event: @event, date: six_months_ago..).count
(recent_activities.to_f / 26.0 * 100).clamp(0, 100)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

26.0 - magic number

factors << calculate_activity_frequency_factor
factors << calculate_volunteer_consistency_factor
factors << calculate_athlete_retention_factor
factors << calculate_results_quality_factor

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

предлагаю все эти методы называть без префикса calculate_

recent_volunteers = Volunteer.joins(:activity)
.where(activity: { event: @event, published: true, date: (Date.current - 6.months).. })
.distinct.count(:athlete_id)
total_volunteers = Volunteer.joins(:activity)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут и выше можно сократить за счёт скоупа
Volunteer.published.where(activity: { event: @event }).distinct.count(:athlete_id)

и аналогично с результатами Result.published...


def calculate_athlete_retention_factor
six_months_ago = Date.current - 6.months
total_athletes = @event.athletes.count

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

это число участников, поставивших это мероприятие домашним забегом - точно именно это имелось в виду?

total = recent_results.count
return 100 if total.zero?

incorrect = recent_results.count { |result| !result.correct? }

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем это? Мне кажется, не особо актуально. Забег не получится опубликовать, если есть некорректный результат. Единственное, кто-то может в опубликованном забеге сделать правку, но это редкость (сейчас в проде ни одного некорректного результата нет).

Тут разве что может оказаться полезен алерт (возможно)... Но тогда наверно нет смысла за 3 месяца смотреть, достаточно 3 недель (или сколько там сейчас разрешено редактировать старые + 1).

Comment thread app/controllers/metrics_controller.rb Outdated

def metrics_data
Rails.cache.fetch('s95_metrics', expires_in: Metrics::S95Collector::TTL) do
Metrics::S95Collector.call

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Попробовал локально в дев консоли запустить на продовой базе - тут гигантское количество sql запросов, у меня на маке минут 10-15 выполнялось, я не дождался и прервал (с выводом запросов в консоль, т.е. на проде будет конечно быстрее, тем не менее - с ростом базы время будет только расти). Если кеш пустой, серия таких запросов может положить сайт

Ещё было видно, что где-то есть N+1 по атлетам

Comment thread config/routes.rb Outdated

get 'up', to: 'rails/health#show'

get '/metrics', to: 'metrics#show'

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

вроде / не нужен - как в 'up'


module Metrics
class VolunteerBusFactorCalculator < ApplicationService
ROLES = %w[

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут имя у константы я бы сделал более говорящим, тут же не все возможные роли Volunteer.roles.keys - только те из них,которые имеют значение для проведения


next if volunteers.zero?

volunteers * 1.5

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.5 - magic value

Comment thread spec/requests/metrics_spec.rb Outdated
ENV['PROMETHEUS_TOKEN'] = 'test-token'
end

it 'returns 503 Service Unavailable' do

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

кмк правильнее отдавать 4xx код, например, 401, 403 или 404(типа не существует такого для внешнего мира)

@vol1ura vol1ura added the enhancement New feature or request label Jul 9, 2026
Comment thread app/services/metrics/s95_collector.rb Fixed
@vutratenko

Copy link
Copy Markdown
Contributor Author

@vol1ura cпасибо за ревью и разумные замечания по перфомансу)

Что поменял:

  • /metrics больше не собирает метрики на cache miss и не ходит в БД при scrape, а только отдаёт последний snapshot из Rails.cache.
  • Если snapshot ещё не собран, endpoint всё равно отвечает 200 и отдаёт служебные метрики snapshot_ready/generated_at/age.
  • Добавил отдельный metrics:refresh для обновления snapshot.
  • Убрал тяжёлые и спорные метрики: location health, bus factor, median/best time, active community и глобальные athlete scans.
  • В collector оставил только лёгкие aggregate/group/count запросы; для results/volunteers ограничил окно последними 12 неделями.
  • Auth теперь принимает Authorization: Bearer , совместимо с Prometheus и vmagent/VictoriaMetrics. Raw token тоже оставил для обратной совместимости.
  • Поправил route на get 'metrics' и 404 при отсутствующем PROMETHEUS_TOKEN.
  • Исправил CodeQL warning по escaping label values.

@vol1ura
vol1ura force-pushed the master branch 5 times, most recently from 47b9265 to 0f18e53 Compare August 1, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants