#46 Add Prometheus exporter
This commit is contained in:

committed by
Sebastian Serth

parent
39fcd255f9
commit
44b32b6f6a
@ -20,3 +20,9 @@ Style/Documentation:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
Layout/SpaceInsideHashLiteralBraces:
|
Layout/SpaceInsideHashLiteralBraces:
|
||||||
EnforcedStyle: no_space
|
EnforcedStyle: no_space
|
||||||
|
RSpec/MultipleExpectations:
|
||||||
|
Max: 2
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/**/*.rb'
|
||||||
|
- 'config/environments/*.rb'
|
||||||
|
1
Gemfile
1
Gemfile
@ -21,6 +21,7 @@ gem 'nokogiri'
|
|||||||
gem 'pagedown-bootstrap-rails'
|
gem 'pagedown-bootstrap-rails'
|
||||||
gem 'pg'
|
gem 'pg'
|
||||||
gem 'proforma', git: 'https://github.com/openHPI/proforma.git', tag: 'v0.5'
|
gem 'proforma', git: 'https://github.com/openHPI/proforma.git', tag: 'v0.5'
|
||||||
|
gem 'prometheus_exporter'
|
||||||
gem 'pry-byebug'
|
gem 'pry-byebug'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
|
@ -248,6 +248,8 @@ GEM
|
|||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
path_expander (1.1.0)
|
path_expander (1.1.0)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
|
prometheus_exporter (0.7.0)
|
||||||
|
webrick
|
||||||
pry (0.13.1)
|
pry (0.13.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
@ -469,6 +471,7 @@ GEM
|
|||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
|
webrick (1.7.0)
|
||||||
websocket (1.2.9)
|
websocket (1.2.9)
|
||||||
websocket-driver (0.7.3)
|
websocket-driver (0.7.3)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
@ -515,6 +518,7 @@ DEPENDENCIES
|
|||||||
pagedown-bootstrap-rails
|
pagedown-bootstrap-rails
|
||||||
pg
|
pg
|
||||||
proforma!
|
proforma!
|
||||||
|
prometheus_exporter
|
||||||
pry-byebug
|
pry-byebug
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
|
10
README.md
10
README.md
@ -9,7 +9,7 @@ CodeOcean
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
CodeOcean is an educational, web-based execution and development environment for practical programming exercises designed for the use in Massive Open Online Courses (MOOCs).
|
CodeOcean is an educational, web-based execution and development environment for practical programming exercises designed for the use in Massive Open Online Courses (MOOCs).
|
||||||
|
|
||||||
The programming courses offered on openHPI include practical programming exercises that are provided on a web-based code execution platform called CodeOcean. The platform has many advantages for learners and teachers alike:
|
The programming courses offered on openHPI include practical programming exercises that are provided on a web-based code execution platform called CodeOcean. The platform has many advantages for learners and teachers alike:
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ CodeOcean is mainly used in the context of MOOCs (such as those offered on openH
|
|||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
Please refer to the [Local Setup Guide](docs/LOCAL_SETUP.md) for more details.
|
Please refer to the [Local Setup Guide](docs/LOCAL_SETUP.md) for more details.
|
||||||
|
|
||||||
### Mandatory Steps
|
### Mandatory Steps
|
||||||
|
|
||||||
@ -50,3 +50,9 @@ In order to execute code submissions using Docker, source code files are written
|
|||||||
|
|
||||||
- create production configuration files (*database.production.yml*, …)
|
- create production configuration files (*database.production.yml*, …)
|
||||||
- customize *config/deploy/production.rb* if you want to deploy using [Capistrano](http://capistranorb.com/)
|
- customize *config/deploy/production.rb* if you want to deploy using [Capistrano](http://capistranorb.com/)
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
- We use a [Prometheus Exporter](https://github.com/discourse/prometheus_exporter) and a [Telegraf Client](https://github.com/jgraichen/telegraf-ruby)
|
||||||
|
- The Telegraf client collects the data from the Prometheus endpoint, adds its own datasets and forwards them to an InfluxDB
|
||||||
|
- The Prometheus Exporter must be started separately **before** running the Rails server via `bundle exec prometheus_exporter`
|
||||||
|
- The InfluxDB data can be visualized using Grafana, for example. There is also an adapted [dashboard](docs/grafana/prometheus_exporter_grafana_dashboard.json) for this purpose
|
17
Vagrantfile
vendored
17
Vagrantfile
vendored
@ -1,14 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
Vagrant.configure(2) do |config|
|
Vagrant.configure(2) do |config|
|
||||||
config.vm.box = "ubuntu/focal64"
|
config.vm.box = 'ubuntu/focal64'
|
||||||
config.vm.provider "virtualbox" do |v|
|
config.vm.provider 'virtualbox' do |v|
|
||||||
v.memory = 4096
|
v.memory = 4096
|
||||||
v.cpus = 4
|
v.cpus = 4
|
||||||
end
|
end
|
||||||
config.vm.network "forwarded_port", host_ip: "127.0.0.1", host: 3000, guest: 3000
|
config.vm.network 'forwarded_port',
|
||||||
config.vm.synced_folder ".", "/home/vagrant/codeocean"
|
host_ip: ENV['LISTEN_ADDRESS'] || '127.0.0.1',
|
||||||
config.vm.synced_folder "../dockercontainerpool", "/home/vagrant/dockercontainerpool"
|
host: 3000,
|
||||||
config.vm.provision "shell", path: "provision/provision.vagrant.sh", privileged: false
|
guest: 3000
|
||||||
|
config.vm.synced_folder '.', '/home/vagrant/codeocean'
|
||||||
|
config.vm.synced_folder '../dockercontainerpool', '/home/vagrant/dockercontainerpool'
|
||||||
|
config.vm.provision 'shell', path: 'provision/provision.vagrant.sh', privileged: false
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApplicationRecord < ActiveRecord::Base
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Comment < ApplicationRecord
|
class Comment < ApplicationRecord
|
||||||
# inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
|
# inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
|
||||||
include Creation
|
include Creation
|
||||||
@ -8,4 +10,12 @@ class Comment < ApplicationRecord
|
|||||||
belongs_to :file, class_name: 'CodeOcean::File'
|
belongs_to :file, class_name: 'CodeOcean::File'
|
||||||
belongs_to :user, polymorphic: true
|
belongs_to :user, polymorphic: true
|
||||||
# after_save :trigger_rfc_action_cable_from_comment
|
# after_save :trigger_rfc_action_cable_from_comment
|
||||||
|
|
||||||
|
def request_for_comment
|
||||||
|
RequestForComment.find_by(submission_id: file.context.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def only_comment_for_rfc?
|
||||||
|
request_for_comment.comments.one?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,29 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class RequestForComment < ApplicationRecord
|
class RequestForComment < ApplicationRecord
|
||||||
include Creation
|
include Creation
|
||||||
include ActionCableHelper
|
include ActionCableHelper
|
||||||
|
|
||||||
|
# SOLVED: The author explicitly marked the RfC as solved.
|
||||||
|
# SOFT_SOLVED: The author did not mark the RfC as solved but reached the maximum score in the corresponding exercise at any time.
|
||||||
|
# ONGOING: The author did not mark the RfC as solved and did not reach the maximum score in the corresponding exercise yet.
|
||||||
|
STATE = [SOLVED = :solved, SOFT_SOLVED = :soft_solved, ONGOING = :unsolved].freeze
|
||||||
|
|
||||||
belongs_to :submission
|
belongs_to :submission
|
||||||
belongs_to :exercise
|
belongs_to :exercise
|
||||||
belongs_to :file, class_name: 'CodeOcean::File'
|
belongs_to :file, class_name: 'CodeOcean::File'
|
||||||
|
|
||||||
has_many :comments, through: :submission
|
has_many :comments, through: :submission
|
||||||
has_many :subscriptions
|
has_many :subscriptions, dependent: :destroy
|
||||||
|
|
||||||
scope :unsolved, -> { where(solved: [false, nil]) }
|
scope :unsolved, -> { where(solved: [false, nil]) }
|
||||||
scope :in_range, -> (from, to) { where(created_at: from..to) }
|
scope :in_range, ->(from, to) { where(created_at: from..to) }
|
||||||
|
scope :with_comments, -> { select { |rfc| rfc.comments.any? } }
|
||||||
|
|
||||||
# after_save :trigger_rfc_action_cable
|
# after_save :trigger_rfc_action_cable
|
||||||
|
|
||||||
def self.last_per_user(n = 5)
|
|
||||||
from("(#{row_number_user_sql}) as request_for_comments")
|
|
||||||
.where("row_number <= ?", n)
|
|
||||||
.group('request_for_comments.id, request_for_comments.user_id, request_for_comments.user_type,
|
|
||||||
request_for_comments.exercise_id, request_for_comments.file_id, request_for_comments.question,
|
|
||||||
request_for_comments.created_at, request_for_comments.updated_at, request_for_comments.solved,
|
|
||||||
request_for_comments.full_score_reached, request_for_comments.submission_id, request_for_comments.row_number')
|
|
||||||
# ugly, but necessary
|
|
||||||
end
|
|
||||||
|
|
||||||
# not used right now, finds the last submission for the respective user and exercise.
|
# not used right now, finds the last submission for the respective user and exercise.
|
||||||
# might be helpful to check whether the exercise has been solved in the meantime.
|
# might be helpful to check whether the exercise has been solved in the meantime.
|
||||||
def last_submission
|
def last_submission
|
||||||
@ -46,27 +44,64 @@ class RequestForComment < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def comments_count
|
def comments_count
|
||||||
submission.files.map { |file| file.comments.size}.sum
|
submission.files.map { |file| file.comments.size }.sum
|
||||||
end
|
end
|
||||||
|
|
||||||
def commenters
|
def commenters
|
||||||
comments.map(&:user).uniq
|
comments.map(&:user).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.with_last_activity
|
def comments?
|
||||||
self.joins('join "submissions" s on s.id = request_for_comments.submission_id
|
comments.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"RFC-#{id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_state
|
||||||
|
state(solved, full_score_reached)
|
||||||
|
end
|
||||||
|
|
||||||
|
def old_state
|
||||||
|
state(solved_before_last_save, full_score_reached_before_last_save)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def state(solved, full_score_reached)
|
||||||
|
if solved
|
||||||
|
SOLVED
|
||||||
|
elsif full_score_reached
|
||||||
|
SOFT_SOLVED
|
||||||
|
else
|
||||||
|
ONGOING
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def with_last_activity
|
||||||
|
joins('join "submissions" s on s.id = request_for_comments.submission_id
|
||||||
left outer join "files" f on f.context_id = s.id
|
left outer join "files" f on f.context_id = s.id
|
||||||
left outer join "comments" c on c.file_id = f.id')
|
left outer join "comments" c on c.file_id = f.id')
|
||||||
.group('request_for_comments.id')
|
.group('request_for_comments.id')
|
||||||
.select('request_for_comments.*, max(c.updated_at) as last_comment')
|
.select('request_for_comments.*, max(c.updated_at) as last_comment')
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s
|
def last_per_user(count = 5)
|
||||||
"RFC-" + self.id.to_s
|
from("(#{row_number_user_sql}) as request_for_comments")
|
||||||
end
|
.where('row_number <= ?', count)
|
||||||
|
.group('request_for_comments.id, request_for_comments.user_id, request_for_comments.user_type,
|
||||||
|
request_for_comments.exercise_id, request_for_comments.file_id, request_for_comments.question,
|
||||||
|
request_for_comments.created_at, request_for_comments.updated_at, request_for_comments.solved,
|
||||||
|
request_for_comments.full_score_reached, request_for_comments.submission_id, request_for_comments.row_number')
|
||||||
|
# ugly, but necessary
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def self.row_number_user_sql
|
|
||||||
select("id, user_id, user_type, exercise_id, file_id, question, created_at, updated_at, solved, full_score_reached, submission_id, row_number() OVER (PARTITION BY user_id, user_type ORDER BY created_at DESC) as row_number").to_sql
|
def row_number_user_sql
|
||||||
|
select('id, user_id, user_type, exercise_id, file_id, question, created_at, updated_at, solved, full_score_reached, submission_id, row_number() OVER (PARTITION BY user_id, user_type ORDER BY created_at DESC) as row_number').to_sql
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,3 +7,5 @@ test:
|
|||||||
enabled: false
|
enabled: false
|
||||||
codeocean_events:
|
codeocean_events:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
prometheus_exporter:
|
||||||
|
enabled: false
|
||||||
|
@ -19,9 +19,15 @@ development:
|
|||||||
codeharbor:
|
codeharbor:
|
||||||
enabled: true
|
enabled: true
|
||||||
url: https://codeharbor.openhpi.de
|
url: https://codeharbor.openhpi.de
|
||||||
|
prometheus_exporter:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
production:
|
production:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
prometheus_exporter:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
test:
|
test:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
prometheus_exporter:
|
||||||
|
enabled: false
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Verifies that versions and hashed value of the package contents in the project's package.json
|
# Verifies that versions and hashed value of the package contents in the project's package.json
|
||||||
config.webpacker.check_yarn_integrity = true
|
config.webpacker.check_yarn_integrity = true
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
config.web_console.whitelisted_ips = '192.168.0.0/16'
|
config.web_console.whitelisted_ips = '192.168.0.0/16'
|
||||||
|
|
||||||
# In the development environment your application's code is reloaded on
|
# In the development environment your application's code is reloaded on
|
||||||
# every request. This slows down response time but is perfect for development
|
# every request. This slows down response time but is perfect for development
|
||||||
# since you don't have to restart the web server when you make code changes.
|
# since you don't have to restart the web server when you make code changes.
|
||||||
config.cache_classes = false
|
config.cache_classes = false
|
||||||
|
|
||||||
# Do not eager load code on boot.
|
# Eager load code for prometheus exporter
|
||||||
config.eager_load = false
|
config.eager_load = true
|
||||||
|
|
||||||
# Show full error reports.
|
# Show full error reports.
|
||||||
config.consider_all_requests_local = true
|
config.consider_all_requests_local = true
|
||||||
|
|
||||||
# Enable/disable caching. By default caching is disabled.
|
# Enable/disable caching. By default caching is disabled.
|
||||||
# Run rails dev:cache to toggle caching.
|
# Run rails dev:cache to toggle caching.
|
||||||
if Rails.root.join('tmp', 'caching-dev.txt').exist?
|
if Rails.root.join('tmp/caching-dev.txt').exist?
|
||||||
config.action_controller.perform_caching = true
|
config.action_controller.perform_caching = true
|
||||||
|
|
||||||
config.cache_store = :memory_store
|
config.cache_store = :memory_store
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Verifies that versions and hashed value of the package contents in the project's package.json
|
# Verifies that versions and hashed value of the package contents in the project's package.json
|
||||||
config.webpacker.check_yarn_integrity = false
|
config.webpacker.check_yarn_integrity = false
|
||||||
@ -10,6 +12,7 @@ Rails.application.configure do
|
|||||||
# your application in memory, allowing both threaded web servers
|
# your application in memory, allowing both threaded web servers
|
||||||
# and those relying on copy on write to perform better.
|
# and those relying on copy on write to perform better.
|
||||||
# Rake tasks automatically ignore this option for performance.
|
# Rake tasks automatically ignore this option for performance.
|
||||||
|
# Eager load is also required for the prometheus exporter
|
||||||
config.eager_load = true
|
config.eager_load = true
|
||||||
|
|
||||||
# Full error reports are disabled and caching is turned on.
|
# Full error reports are disabled and caching is turned on.
|
||||||
@ -90,8 +93,8 @@ Rails.application.configure do
|
|||||||
# require 'syslog/logger'
|
# require 'syslog/logger'
|
||||||
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
|
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
|
||||||
|
|
||||||
if ENV["RAILS_LOG_TO_STDOUT"].present?
|
if ENV['RAILS_LOG_TO_STDOUT'].present?
|
||||||
logger = ActiveSupport::Logger.new(STDOUT)
|
logger = ActiveSupport::Logger.new($stdout)
|
||||||
logger.formatter = config.log_formatter
|
logger.formatter = config.log_formatter
|
||||||
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
@ -11,9 +13,10 @@ Rails.application.configure do
|
|||||||
# your application in memory, allowing both threaded web servers
|
# your application in memory, allowing both threaded web servers
|
||||||
# and those relying on copy on write to perform better.
|
# and those relying on copy on write to perform better.
|
||||||
# Rake tasks automatically ignore this option for performance.
|
# Rake tasks automatically ignore this option for performance.
|
||||||
config.eager_load = false
|
# Eager load code for prometheus exporter
|
||||||
|
config.eager_load = true
|
||||||
|
|
||||||
#enable web console in staging
|
# enable web console in staging
|
||||||
config.web_console.development_only = false
|
config.web_console.development_only = false
|
||||||
|
|
||||||
# Show full error reports and disable caching.
|
# Show full error reports and disable caching.
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
@ -10,7 +12,8 @@ Rails.application.configure do
|
|||||||
# Do not eager load code on boot. This avoids loading your whole application
|
# Do not eager load code on boot. This avoids loading your whole application
|
||||||
# just for the purpose of running a single test. If you are using a tool that
|
# just for the purpose of running a single test. If you are using a tool that
|
||||||
# preloads Rails for running tests, you may have to set it to true.
|
# preloads Rails for running tests, you may have to set it to true.
|
||||||
config.eager_load = false
|
# Eager load code for prometheus exporter
|
||||||
|
config.eager_load = true
|
||||||
|
|
||||||
# Configure public file server for tests with Cache-Control for performance.
|
# Configure public file server for tests with Cache-Control for performance.
|
||||||
config.public_file_server.enabled = true
|
config.public_file_server.enabled = true
|
||||||
@ -47,7 +50,7 @@ Rails.application.configure do
|
|||||||
# Raises error for missing translations
|
# Raises error for missing translations
|
||||||
# config.action_view.raise_on_missing_translations = true
|
# config.action_view.raise_on_missing_translations = true
|
||||||
|
|
||||||
#config.logger = Logger.new(STDOUT)
|
# config.logger = Logger.new($stdout)
|
||||||
# Set log level
|
# Set log level
|
||||||
#config.log_level = :DEBUG
|
# config.log_level = :DEBUG
|
||||||
end
|
end
|
||||||
|
10
config/initializers/prometheus.rb
Normal file
10
config/initializers/prometheus.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
return unless CodeOcean::Config.new(:code_ocean).read[:prometheus_exporter][:enabled]
|
||||||
|
return if Rake.application.top_level_tasks.to_s.include?('db:')
|
||||||
|
|
||||||
|
# Add metric callbacks to all models
|
||||||
|
ApplicationRecord.include Prometheus::Record
|
||||||
|
|
||||||
|
# Initialize the counters according to the db
|
||||||
|
Prometheus::Controller.initialize_metrics
|
@ -1,6 +1,6 @@
|
|||||||
describe 'Exercise' do
|
describe 'Exercise' do
|
||||||
it "outputs 'Hello World" do
|
it "outputs 'Hello World" do
|
||||||
expect(STDOUT).to receive(:puts).with('Hello World')
|
expect($stdout).to receive(:puts).with('Hello World')
|
||||||
require './exercise'
|
require './exercise'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -47,6 +47,14 @@ The default credentials for the administrator are:
|
|||||||
- email: `admin@example.org`
|
- email: `admin@example.org`
|
||||||
- password: `admin`
|
- password: `admin`
|
||||||
|
|
||||||
|
For exporting metrics, start the prometeus exporter by running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec prometheus_exporter
|
||||||
|
```
|
||||||
|
|
||||||
|
in the CodeOcean folder before starting CodeOcean.
|
||||||
|
|
||||||
## Execution Environments
|
## Execution Environments
|
||||||
|
|
||||||
Every exercise is executed in an execution environment which is based on a docker image. In order to install a new image, have a look at the container of the openHPI team on [DockerHub](https://hub.docker.com/u/openhpi). For example you can add an [image for ruby](https://hub.docker.com/layers/openhpi/co_execenv_ruby/latest/images/sha256-70f597320567678bf8d0146d93fb1bd98457abe61c3b642e832d4e4fbe7f4526) by executing `docker pull openhpi/co_execenv_ruby:latest`.
|
Every exercise is executed in an execution environment which is based on a docker image. In order to install a new image, have a look at the container of the openHPI team on [DockerHub](https://hub.docker.com/u/openhpi). For example you can add an [image for ruby](https://hub.docker.com/layers/openhpi/co_execenv_ruby/latest/images/sha256-70f597320567678bf8d0146d93fb1bd98457abe61c3b642e832d4e4fbe7f4526) by executing `docker pull openhpi/co_execenv_ruby:latest`.
|
||||||
|
1693
docs/grafana/prometheus_exporter_grafana_dashboard.json
Normal file
1693
docs/grafana/prometheus_exporter_grafana_dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
3
lib/prometheus.rb
Normal file
3
lib/prometheus.rb
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Prometheus; end
|
100
lib/prometheus/controller.rb
Normal file
100
lib/prometheus/controller.rb
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'prometheus_exporter/client'
|
||||||
|
|
||||||
|
module Prometheus
|
||||||
|
module Controller
|
||||||
|
# TODO: currently active users - as Event
|
||||||
|
# TODO: active_in_last_hour
|
||||||
|
# TODO: autosaves_per_minute
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def initialize_metrics
|
||||||
|
register_metrics
|
||||||
|
initialize_instance_count
|
||||||
|
initialize_rfc_metrics
|
||||||
|
end
|
||||||
|
|
||||||
|
def register_metrics
|
||||||
|
prometheus = PrometheusExporter::Client.default
|
||||||
|
|
||||||
|
@instance_count = prometheus.register(:gauge, :instance_count, help: 'Instance count')
|
||||||
|
# counts solved, soft_solved, ongoing
|
||||||
|
@rfc_count = prometheus.register(:gauge, :rfc_count, help: 'Count of RfCs in each state')
|
||||||
|
# counts commented
|
||||||
|
@rfc_commented_count = prometheus.register(:gauge, :rfc_commented_count, help: 'Count of commented RfCs')
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_instance_count
|
||||||
|
ApplicationRecord.descendants.reject(&:abstract_class).each do |each|
|
||||||
|
@instance_count.observe(each.count, class: each.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_rfc_metrics
|
||||||
|
# Initialize rfc metric
|
||||||
|
@rfc_count.observe(RequestForComment.unsolved.where(full_score_reached: false).count,
|
||||||
|
state: RequestForComment::ONGOING)
|
||||||
|
@rfc_count.observe(RequestForComment.unsolved.where(full_score_reached: true).count,
|
||||||
|
state: RequestForComment::SOFT_SOLVED)
|
||||||
|
@rfc_count.observe(RequestForComment.where(solved: true).count,
|
||||||
|
state: RequestForComment::SOLVED)
|
||||||
|
|
||||||
|
# count of rfcs with comments
|
||||||
|
@rfc_commented_count.observe(RequestForComment.with_comments.count)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_notification(object)
|
||||||
|
Rails.logger.debug("Prometheus metric updated for #{object.class.name}")
|
||||||
|
|
||||||
|
case object
|
||||||
|
when RequestForComment
|
||||||
|
update_rfc(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_notification(object)
|
||||||
|
@instance_count.increment(class: object.class.name)
|
||||||
|
Rails.logger.debug("Prometheus instance count increased for #{object.class.name}")
|
||||||
|
|
||||||
|
case object
|
||||||
|
when RequestForComment
|
||||||
|
create_rfc(object)
|
||||||
|
when Comment
|
||||||
|
create_comment(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_notification(object)
|
||||||
|
@instance_count.decrement(class: object.class.name)
|
||||||
|
Rails.logger.debug("Prometheus instance count decreased for #{object.class.name}")
|
||||||
|
|
||||||
|
case object
|
||||||
|
when Comment
|
||||||
|
destroy_comment(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_rfc(rfc)
|
||||||
|
@rfc_count.increment(state: rfc.current_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_rfc(rfc)
|
||||||
|
@rfc_count.decrement(state: rfc.old_state)
|
||||||
|
# If the metrics are scraped when the execution is exactly at the place of this comment,
|
||||||
|
# the old state is already decremented while the new state is not yet incremented and
|
||||||
|
# the metric is therefore inconsistent. As this is only a temporarily off by one error
|
||||||
|
# in the metric and a solution (e.g. a mutex) would be complex, this is acceptable.
|
||||||
|
@rfc_count.increment(state: rfc.current_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_comment(comment)
|
||||||
|
@rfc_commented_count.increment if comment.only_comment_for_rfc?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_comment(comment)
|
||||||
|
@rfc_commented_count.decrement unless comment.request_for_comment.comments?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
lib/prometheus/record.rb
Normal file
27
lib/prometheus/record.rb
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Prometheus
|
||||||
|
module Record
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
after_create_commit :create_notification
|
||||||
|
after_destroy_commit :destroy_notification
|
||||||
|
after_update_commit :update_notification
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_notification
|
||||||
|
Prometheus::Controller.create_notification self
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_notification
|
||||||
|
Prometheus::Controller.destroy_notification self
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_notification
|
||||||
|
Prometheus::Controller.update_notification self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,5 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :rfc, class: RequestForComment do
|
factory :rfc, class: 'RequestForComment' do
|
||||||
association :user, factory: :external_user
|
association :user, factory: :external_user
|
||||||
association :submission
|
association :submission
|
||||||
association :exercise, factory: :dummy
|
association :exercise, factory: :dummy
|
||||||
@ -7,5 +9,12 @@ FactoryBot.define do
|
|||||||
sequence :question do |n|
|
sequence :question do |n|
|
||||||
"test question #{n}"
|
"test question #{n}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :rfc_with_comment, class: 'RequestForComment' do
|
||||||
|
after(:create) do |rfc|
|
||||||
|
rfc.file = rfc.submission.files.first
|
||||||
|
Comment.create(file: rfc.file, user: rfc.user, text: "comment for rfc #{rfc.question}")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
101
spec/features/prometheus/controller_spec.rb
Normal file
101
spec/features/prometheus/controller_spec.rb
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Prometheus::Controller do
|
||||||
|
def stub_metrics
|
||||||
|
%i[increment decrement observe].each do |method|
|
||||||
|
%i[@instance_count @rfc_count @rfc_commented_count].each do |metric|
|
||||||
|
allow(described_class.instance_variable_get(metric)).to receive(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
ApplicationRecord.include Prometheus::Record
|
||||||
|
described_class.initialize_metrics
|
||||||
|
stub_metrics
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'instance count' do
|
||||||
|
it 'initializes the metrics with the current database entries' do
|
||||||
|
FactoryBot.create_list(:proxy_exercise, 3)
|
||||||
|
described_class.register_metrics
|
||||||
|
stub_metrics
|
||||||
|
described_class.initialize_instance_count
|
||||||
|
expect(described_class.instance_variable_get(:@instance_count)).to(have_received(:observe).with(ProxyExercise.count, class: ProxyExercise.name).once)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets notified when an object is created' do
|
||||||
|
allow(described_class).to receive(:create_notification)
|
||||||
|
proxy_exercise = FactoryBot.create(:proxy_exercise)
|
||||||
|
expect(described_class).to have_received(:create_notification).with(proxy_exercise).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets notified when an object is destroyed' do
|
||||||
|
allow(described_class).to receive(:destroy_notification)
|
||||||
|
proxy_exercise = FactoryBot.create(:proxy_exercise).destroy
|
||||||
|
expect(described_class).to have_received(:destroy_notification).with(proxy_exercise).once
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'increments gauge when creating a new instance' do
|
||||||
|
FactoryBot.create(:proxy_exercise)
|
||||||
|
expect(described_class.instance_variable_get(:@instance_count)).to(
|
||||||
|
have_received(:increment).with(class: ProxyExercise.name).once
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'decrements gauge when deleting an object' do
|
||||||
|
FactoryBot.create(:proxy_exercise).destroy
|
||||||
|
expect(described_class.instance_variable_get(:@instance_count)).to(
|
||||||
|
have_received(:decrement).with(class: ProxyExercise.name).once
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'rfc count' do
|
||||||
|
context 'when initializing an rfc' do
|
||||||
|
it 'updates rfc count when creating an ongoing rfc' do
|
||||||
|
FactoryBot.create(:rfc)
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_count)).to(
|
||||||
|
have_received(:increment).with(state: RequestForComment::ONGOING).once
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when changing the state of an rfc' do
|
||||||
|
let(:rfc) { FactoryBot.create(:rfc) }
|
||||||
|
|
||||||
|
it 'updates rfc count when soft-solving an rfc' do
|
||||||
|
rfc.full_score_reached = true
|
||||||
|
rfc.save
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_count)).to(have_received(:increment).with(state: RequestForComment::SOFT_SOLVED).once)
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_count)).to(have_received(:decrement).with(state: RequestForComment::ONGOING).once)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates rfc count when solving an rfc' do
|
||||||
|
rfc.solved = true
|
||||||
|
rfc.save
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_count)).to(have_received(:increment).with(state: RequestForComment::SOLVED).once)
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_count)).to(have_received(:decrement).with(state: RequestForComment::ONGOING).once)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when commenting an rfc' do
|
||||||
|
it 'updates comment metric when commenting an rfc' do
|
||||||
|
FactoryBot.create(:rfc_with_comment)
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_commented_count)).to have_received(:increment)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update comment metric when commenting an rfc that already has a comment' do
|
||||||
|
rfc = FactoryBot.create(:rfc_with_comment)
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_commented_count)).to have_received(:increment).once
|
||||||
|
|
||||||
|
Comment.create(file: rfc.file, user: rfc.user, text: "comment a for rfc #{rfc.question}")
|
||||||
|
Comment.create(file: rfc.file, user: rfc.user, text: "comment b for rfc #{rfc.question}")
|
||||||
|
# instance count has only been updated for the creation of the commented rfc and not for additional comments
|
||||||
|
expect(described_class.instance_variable_get(:@rfc_commented_count)).to have_received(:increment).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
spec/models/request_for_comment_spec.rb
Normal file
19
spec/models/request_for_comment_spec.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe RequestForComment do
|
||||||
|
let!(:rfc) { FactoryBot.create(:rfc) }
|
||||||
|
|
||||||
|
describe 'scope with_comments' do
|
||||||
|
let!(:rfc2) { FactoryBot.create(:rfc_with_comment) }
|
||||||
|
|
||||||
|
it 'includes all RfCs with comments' do
|
||||||
|
expect(described_class.with_comments).to include(rfc2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include any RfC without a comment' do
|
||||||
|
expect(described_class.with_comments).not_to include(rfc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,7 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
||||||
ENV['RAILS_ENV'] ||= 'test'
|
ENV['RAILS_ENV'] ||= 'test'
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
require File.expand_path('../../config/environment', __FILE__)
|
require 'support/prometheus_client_stub'
|
||||||
|
require File.expand_path('../config/environment', __dir__)
|
||||||
require 'rspec/rails'
|
require 'rspec/rails'
|
||||||
require 'pundit/rspec'
|
require 'pundit/rspec'
|
||||||
|
|
||||||
@ -12,7 +15,7 @@ require 'pundit/rspec'
|
|||||||
# run twice. It is recommended that you do not name files matching this glob to
|
# run twice. It is recommended that you do not name files matching this glob to
|
||||||
# end with _spec.rb. You can configure this pattern with with the --pattern
|
# end with _spec.rb. You can configure this pattern with with the --pattern
|
||||||
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
||||||
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
|
Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f }
|
||||||
|
|
||||||
# Checks for pending migrations before tests are run.
|
# Checks for pending migrations before tests are run.
|
||||||
# If you are not using ActiveRecord, you can remove this line.
|
# If you are not using ActiveRecord, you can remove this line.
|
||||||
|
15
spec/support/prometheus_client_stub.rb
Normal file
15
spec/support/prometheus_client_stub.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'prometheus_exporter/client'
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
module Prometheus
|
||||||
|
# A stub to disable server functionality in the specs and stub all registered metrics
|
||||||
|
module StubClient
|
||||||
|
def send(str)
|
||||||
|
# Do nothing
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
PrometheusExporter::Client.prepend StubClient
|
||||||
|
end
|
Reference in New Issue
Block a user