Merge pull request #1082 from Retrospring/app-metrics
Export question and comment metrics via Prometheus
This commit is contained in:
commit
2de6ed9bf9
2
Gemfile
2
Gemfile
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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?) ||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue