Merge pull request #1082 from Retrospring/app-metrics

Export question and comment metrics via Prometheus
This commit is contained in:
Georg Gadinger 2023-02-16 22:10:57 +01:00 committed by GitHub
commit 2de6ed9bf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 274 additions and 6 deletions

View File

@ -117,3 +117,5 @@ gem "openssl", "~> 3.1"
# mail 2.8.0 breaks sendmail usage: https://github.com/mikel/mail/issues/1538
gem "mail", "~> 2.7.1"
gem "prometheus-client", "~> 4.0"

View File

@ -279,6 +279,7 @@ GEM
pg (1.4.5)
pghero (3.1.0)
activerecord (>= 6)
prometheus-client (4.0.0)
public_suffix (4.0.7)
puma (6.1.0)
nio4r (~> 2.0)
@ -522,6 +523,7 @@ DEPENDENCIES
openssl (~> 3.1)
pg
pghero
prometheus-client (~> 4.0)
puma
pundit (~> 2.3)
questiongenerator (~> 1.1)

View File

@ -5,7 +5,7 @@ class InboxController < ApplicationController
after_action :mark_inbox_entries_as_read, only: %i[show]
def show # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
def show # rubocop:disable Metrics/MethodLength
find_author
find_inbox_entries
@ -37,6 +37,7 @@ class InboxController < ApplicationController
user: current_user)
inbox = Inbox.create!(user: current_user, question_id: question.id, new: true)
increment_metric
respond_to do |format|
format.turbo_stream do
@ -85,4 +86,14 @@ class InboxController < ApplicationController
# using .dup to not modify @inbox -- useful in tests
@inbox&.dup&.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
end
def increment_metric
Retrospring::Metrics::QUESTIONS_ASKED.increment(
labels: {
anonymous: true,
followers: false,
generated: true,
}
)
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require "prometheus/client/formats/text"
class MetricsController < ActionController::API
include ActionController::MimeResponds
def show
fetch_sidekiq_metrics
render plain: metrics
end
private
SIDEKIQ_STATS_METHODS = %i[
processed
failed
scheduled_size
retry_size
dead_size
processes_size
].freeze
def fetch_sidekiq_metrics
stats = Sidekiq::Stats.new
SIDEKIQ_STATS_METHODS.each do |key|
Retrospring::Metrics::SIDEKIQ[key].set stats.public_send(key)
end
stats.queues.each do |queue, value|
Retrospring::Metrics::SIDEKIQ[:queue_enqueued].set value, labels: { queue: }
end
end
def metrics
Prometheus::Client::Formats::Text.marshal(Retrospring::Metrics::PROMETHEUS)
end
end

View File

@ -115,6 +115,8 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
raise Errors::AnsweringSelfBlockedOther if self.blocking?(question.user)
# rubocop:enable Style/RedundantSelf
Retrospring::Metrics::QUESTIONS_ANSWERED.increment
Answer.create!(content:, user: self, question:)
end
@ -128,6 +130,8 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
raise Errors::CommentingOtherBlockedSelf if answer.user.blocking?(self)
# rubocop:enable Style/RedundantSelf
Retrospring::Metrics::COMMENTS_CREATED.increment
Comment.create!(user: self, answer:, content:)
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
return if Rails.env.test? # no need for the direct file store in testing
require "prometheus/client/data_stores/direct_file_store"
Rails.application.config.before_configuration do
dir = Rails.root.join("tmp/prometheus_metrics")
FileUtils.mkdir_p dir
Prometheus::Client.config.data_store = Prometheus::Client::DataStores::DirectFileStore.new(dir:)
end
Rails.application.config.after_initialize do
# ensure the version metric is populated
Retrospring::Metrics::VERSION_INFO
end

View File

@ -49,6 +49,10 @@ Rails.application.routes.draw do
get "/linkfilter", to: "link_filter#index", as: :linkfilter
get "/manifest.json", to: "manifests#show", as: :webapp_manifest
constraints(Constraints::LocalNetwork) do
get "/metrics", to: "metrics#show"
end
# Devise routes
devise_for :users, path: "user", skip: %i[sessions registrations]
as :user do

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Constraints
module LocalNetwork
module_function
SUBNETS = %w[
10.0.0.0/8
127.0.0.0/8
172.16.0.0/12
192.168.0.0/16
].map { IPAddr.new(_1) }.freeze
def matches?(request)
SUBNETS.find do |net|
net.include? request.remote_ip
rescue
false
end
end
end
end

View File

@ -0,0 +1,79 @@
# frozen_string_literal: true
module Retrospring
module Metrics
PROMETHEUS = Prometheus::Client.registry
# avoid re-registering metrics to make autoreloader happy during dev:
class << self
%i[counter gauge histogram summary].each do |meth|
define_method meth do |name, *args, **kwargs|
PROMETHEUS.public_send(meth, name, *args, **kwargs)
rescue Prometheus::Client::Registry::AlreadyRegisteredError
raise unless Rails.env.development?
PROMETHEUS.unregister name
retry
end
end
end
VERSION_INFO = gauge(
:retrospring_version_info,
docstring: "Information about the currently running version",
labels: [:version],
preset_labels: {
version: Retrospring::Version.to_s,
}
).tap { _1.set 1 }
QUESTIONS_ASKED = counter(
:retrospring_questions_asked_total,
docstring: "How many questions got asked",
labels: %i[anonymous followers generated]
)
QUESTIONS_ANSWERED = counter(
:retrospring_questions_answered_total,
docstring: "How many questions got answered"
)
COMMENTS_CREATED = counter(
:retrospring_comments_created_total,
docstring: "How many comments got created"
)
# metrics from Sidekiq::Stats.new
SIDEKIQ = {
processed: gauge(
:sidekiq_processed,
docstring: "Number of jobs processed by Sidekiq"
),
failed: gauge(
:sidekiq_failed,
docstring: "Number of jobs that failed"
),
scheduled_size: gauge(
:sidekiq_scheduled_jobs,
docstring: "Number of jobs that are enqueued"
),
retry_size: gauge(
:sidekiq_retried_jobs,
docstring: "Number of jobs that are being retried"
),
dead_size: gauge(
:sidekiq_dead_jobs,
docstring: "Number of jobs that are dead"
),
processes_size: gauge(
:sidekiq_processes,
docstring: "Number of active Sidekiq processes"
),
queue_enqueued: gauge(
:sidekiq_queues_enqueued,
docstring: "Number of enqueued jobs per queue",
labels: %i[queue]
),
}.freeze
end
end

View File

@ -18,6 +18,7 @@ module UseCase
return if filtered?(question)
increment_asked_count
increment_metric
inbox = ::Inbox.create!(user: target_user, question:, new: true)
notify(inbox)
@ -26,8 +27,8 @@ module UseCase
status: 201,
resource: question,
extra: {
inbox:
}
inbox:,
},
}
end
@ -92,6 +93,16 @@ module UseCase
source_user.save
end
def increment_metric
Retrospring::Metrics::QUESTIONS_ASKED.increment(
labels: {
anonymous:,
followers: false,
generated: false,
}
)
end
def filtered?(question)
target_user.mute_rules.any? { |rule| rule.applies_to? question } ||
(anonymous && AnonymousBlock.where(identifier: question.author_identifier, user_id: [target_user.id, nil]).any?) ||

View File

@ -9,20 +9,21 @@ module UseCase
def call
question = ::Question.create!(
content: content,
content:,
author_is_anonymous: false,
author_identifier: author_identifier,
author_identifier:,
user: source_user,
direct: false
)
increment_asked_count
increment_metric
QuestionWorker.perform_async(source_user_id, question.id)
{
status: 201,
resource: question
resource: question,
}
end
@ -33,6 +34,16 @@ module UseCase
source_user.save
end
def increment_metric
Retrospring::Metrics::QUESTIONS_ASKED.increment(
labels: {
anonymous: false,
followers: true,
generated: false,
}
)
end
def source_user
@source_user ||= ::User.find(source_user_id)
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require "rails_helper"
describe MetricsController, type: :controller do
describe "#show" do
subject { get :show }
it "returns the metrics" do
expect(subject.body).to include "retrospring_version_info"
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require "rails_helper"
describe Constraints::LocalNetwork do
describe ".matches?" do
let(:request) { double("Rack::Request", remote_ip:) }
subject { described_class.matches?(request) }
context "with a private address from the 10.0.0.0/8 range" do
let(:remote_ip) { "10.0.2.100" }
it { is_expected.to be_truthy }
end
context "with a private address from the 127.0.0.0/8 range" do
let(:remote_ip) { "127.0.0.1" }
it { is_expected.to be_truthy }
end
context "with a private address from the 172.16.0.0/12 range" do
let(:remote_ip) { "172.31.33.7" }
it { is_expected.to be_truthy }
end
context "with a private address from the 192.168.0.0/16 range" do
let(:remote_ip) { "192.168.123.45" }
it { is_expected.to be_truthy }
end
context "with a non-private/loopback address" do
let(:remote_ip) { "193.186.6.83" }
it { is_expected.to be_falsey }
end
context "with some fantasy address" do
let(:remote_ip) { "fe80:3::1ff:fe23:4567:890a" }
it { is_expected.to be_falsey }
end
context "with an actual invalid address" do
let(:remote_ip) { "herbert" }
it { is_expected.to be_falsey }
end
end
end