recolectar estadísticas usando menos recursos
This commit is contained in:
parent
ab004fae70
commit
c84462c4a8
5 changed files with 198 additions and 58 deletions
55
app/jobs/periodic_job.rb
Normal file
55
app/jobs/periodic_job.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Una tarea que se corre periódicamente
|
||||||
|
class PeriodicJob < ApplicationJob
|
||||||
|
class RunAgainException < StandardError; end
|
||||||
|
|
||||||
|
STARTING_INTERVAL = Stat::INTERVALS.first
|
||||||
|
|
||||||
|
# Tener el sitio a mano
|
||||||
|
attr_reader :site
|
||||||
|
|
||||||
|
# Descartar y notificar si pasó algo más.
|
||||||
|
#
|
||||||
|
# XXX: En realidad deberíamos seguir reintentando?
|
||||||
|
discard_on(StandardError) do |_, error|
|
||||||
|
ExceptionNotifier.notify_exception(error)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Correr indefinidamente una vez por hora.
|
||||||
|
#
|
||||||
|
# XXX: El orden importa, si el descarte viene después, nunca se va a
|
||||||
|
# reintentar.
|
||||||
|
retry_on(PeriodicJob::RunAgainException, wait: 1.try(STARTING_INTERVAL), attempts: Float::INFINITY, jitter: 0)
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Las clases que implementen esta tienen que usar este método al
|
||||||
|
# terminar.
|
||||||
|
def run_again!
|
||||||
|
raise PeriodicJob::RunAgainException, 'Reintentando'
|
||||||
|
end
|
||||||
|
|
||||||
|
# El intervalo de inicio
|
||||||
|
#
|
||||||
|
# @return [Symbol]
|
||||||
|
def starting_interval
|
||||||
|
STARTING_INTERVAL
|
||||||
|
end
|
||||||
|
|
||||||
|
# La última recolección de estadísticas o empezar desde el principio
|
||||||
|
# de los tiempos.
|
||||||
|
#
|
||||||
|
# @return [Stat]
|
||||||
|
def last_stat
|
||||||
|
@last_stat ||= site.stats.where(name: stat_name).last ||
|
||||||
|
site.stats.build(created_at: Time.new(1970, 1, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devuelve el comienzo del intervalo
|
||||||
|
#
|
||||||
|
# @return [Time]
|
||||||
|
def beginning_of_interval
|
||||||
|
@beginning_of_interval ||= last_stat.created_at.try(:"beginning_of_#{starting_interval}")
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,74 +3,65 @@
|
||||||
# Genera resúmenes de información para poder mostrar estadísticas y se
|
# Genera resúmenes de información para poder mostrar estadísticas y se
|
||||||
# corre regularmente a sí misma.
|
# corre regularmente a sí misma.
|
||||||
class StatCollectionJob < ApplicationJob
|
class StatCollectionJob < ApplicationJob
|
||||||
class CrontabException < StandardError; end
|
STAT_NAME = 'stat_collection_job'
|
||||||
|
|
||||||
# Descartar y notificar si pasó algo más.
|
def perform(site_id:, once: true)
|
||||||
#
|
@site = Site.find site_id
|
||||||
# XXX: En realidad deberíamos seguir reintentando?
|
|
||||||
discard_on(Exception) do |_, error|
|
scope.rollup('builds', **options)
|
||||||
ExceptionNotifier.notify_exception error
|
|
||||||
|
scope.rollup('space_used', **options) do |rollup|
|
||||||
|
rollup.average(:bytes)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Correr indefinidamente una vez por hora.
|
scope.rollup('build_time', **options) do |rollup|
|
||||||
#
|
rollup.average(:seconds)
|
||||||
# XXX: El orden importa, si el descarte viene después, nunca se va a
|
|
||||||
# reintentar.
|
|
||||||
retry_on(StatCollectionJob::CrontabException, wait: 1.hour, attempts: Float::INFINITY, jitter: 0)
|
|
||||||
|
|
||||||
COLUMNS = %i[uri].freeze
|
|
||||||
|
|
||||||
def perform(once: false)
|
|
||||||
Site.find_each do |site|
|
|
||||||
hostnames = [site.hostname, site.alternative_hostnames].flatten
|
|
||||||
|
|
||||||
# Usamos el primero porque luego podemos hacer un rollup recursivo
|
|
||||||
options = { interval: Stat::INTERVALS.first }
|
|
||||||
|
|
||||||
# Visitas por hostname
|
|
||||||
hostnames.each do |hostname|
|
|
||||||
AccessLog.where(host: hostname).completed_requests.non_robots.pages.group(:host).rollup('host', **options)
|
|
||||||
|
|
||||||
combined_columns(hostname, **options)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
stats_by_site(site, **options)
|
# XXX: Es correcto promediar promedios?
|
||||||
|
Stat::INTERVALS.reduce do |previous, current|
|
||||||
|
rollup(name: 'builds', interval_previous: previous, interval: current)
|
||||||
|
rollup(name: 'space_used', interval_previous: previous, interval: current, operation: :average)
|
||||||
|
rollup(name: 'build_time', interval_previous: previous, interval: current, operation: :average)
|
||||||
|
|
||||||
|
current
|
||||||
end
|
end
|
||||||
|
|
||||||
# Registrar que se hicieron todas las recolecciones
|
# Registrar que se hicieron todas las recolecciones
|
||||||
Stat.create!
|
site.stats.create! name: STAT_NAME
|
||||||
|
|
||||||
raise CrontabException unless once
|
run_again! unless once
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Combinación de columnas
|
# Genera un rollup recursivo en base al período anterior y aplica una
|
||||||
def combined_columns(hostname, **options)
|
# operación.
|
||||||
where = { host: hostname }
|
|
||||||
|
|
||||||
COLUMNS.each do |column|
|
|
||||||
AccessLog.where(host: hostname).pluck(Arel.sql("distinct #{column}")).each do |value|
|
|
||||||
where[column] = value
|
|
||||||
|
|
||||||
AccessLog.where(**where).completed_requests.non_robots.group(:host, column).rollup("host|#{column}", **options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Uso de recursos por cada sitio.
|
|
||||||
#
|
#
|
||||||
# XXX: En realidad se agrupan por el deploy_id, que siempre será el
|
# @return [NilClass]
|
||||||
# del DeployLocal.
|
def rollup(name:, interval_previous:, interval:, operation: :sum)
|
||||||
def stats_by_site(site, **options)
|
Rollup.where(name: name, interval: interval_previous)
|
||||||
site.build_stats.jekyll.group(:deploy_id).rollup('builds', **options)
|
.where_dimensions(site_id: site.id)
|
||||||
|
.group("dimensions->'site_id'")
|
||||||
site.build_stats.jekyll.group(:deploy_id).rollup('space_used', **options) do |rollup|
|
.rollup(name, interval: interval, update: true) do |rollup|
|
||||||
rollup.average(:bytes)
|
rollup.try(:operation, :value)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
site.build_stats.jekyll.group(:deploy_id).rollup('build_time', **options) do |rollup|
|
# Los registros a procesar
|
||||||
rollup.average(:seconds)
|
#
|
||||||
|
# @return [ActiveRecord::Relation]
|
||||||
|
def scope
|
||||||
|
@scope ||= site.build_stats
|
||||||
|
.jekyll
|
||||||
|
.where('created_at => ?', beginning_of_interval)
|
||||||
|
.group(:site_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Las opciones por defecto
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def options
|
||||||
|
@options ||= { interval: starting_interval, update: true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,16 +1,105 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Procesar una lista de URIs para una lista de dominios. Esto nos
|
# Procesar una lista de URIs para una lista de dominios. Esto nos
|
||||||
# permite procesar estadísticas a demanada.
|
# permite procesar estadísticas a demanda.
|
||||||
class UriCollectionJob < ApplicationJob
|
#
|
||||||
def perform(hostnames:, file:)
|
# Hay varias cosas acá que van a convertirse en métodos propios, como la
|
||||||
uris = File.read(file).split("\n")
|
# detección de URIs de un sitio (aunque la versión actual detecta todas
|
||||||
|
# las páginas y no solo las de posts como tenemos planeado, hay que
|
||||||
|
# resolver eso).
|
||||||
|
#
|
||||||
|
# Los hostnames de un sitio van a poder obtenerse a partir de
|
||||||
|
# Site#hostnames con la garantía de que son únicos.
|
||||||
|
class UriCollectionJob < PeriodicJob
|
||||||
|
# Ignoramos imágenes porque suelen ser demasiadas y no aportan a las
|
||||||
|
# estadísticas.
|
||||||
|
IMAGES = %w[.png .jpg .jpeg .gif .webp].freeze
|
||||||
|
STAT_NAME = 'uri_collection_job'
|
||||||
|
|
||||||
|
def perform(site_id:, once: true)
|
||||||
|
@site = Site.find site_id
|
||||||
|
|
||||||
hostnames.each do |hostname|
|
hostnames.each do |hostname|
|
||||||
uris.each do |uri|
|
uris.each do |uri|
|
||||||
break if File.exist? Rails.root.join('tmp', 'uri_collection_job_stop')
|
return if File.exist? Rails.root.join('tmp', 'uri_collection_job_stop')
|
||||||
|
|
||||||
AccessLog.where(host: hostname, uri: uri).completed_requests.non_robots.group(:host, :uri).rollup('host|uri', interval: 'day')
|
AccessLog.where(host: hostname, uri: uri)
|
||||||
|
.where('created_at >= ?', beginning_of_interval)
|
||||||
|
.completed_requests
|
||||||
|
.non_robots
|
||||||
|
.group(:host, :uri)
|
||||||
|
.rollup('host|uri', interval: starting_interval, update: true)
|
||||||
|
|
||||||
|
# Reducir las estadísticas calculadas aplicando un rollup sobre el
|
||||||
|
# intervalo más amplio.
|
||||||
|
Stat::INTERVALS.reduce do |previous, current|
|
||||||
|
Rollup.where(name: 'host|uri', interval: previous)
|
||||||
|
.where_dimensions(host: hostname, uri: uri)
|
||||||
|
.group("dimensions->'host'", "dimensions->'uri'")
|
||||||
|
.rollup('host|uri', interval: current, update: true) do |rollup|
|
||||||
|
rollup.sum(:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Devolver el intervalo actual
|
||||||
|
current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Recordar la última vez que se corrió la tarea
|
||||||
|
site.stats.create! name: STAT_NAME
|
||||||
|
|
||||||
|
run_again! unless once
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def stat_name
|
||||||
|
STAT_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [String]
|
||||||
|
#
|
||||||
|
# TODO: Cambiar al mergear origin-referer
|
||||||
|
def destination
|
||||||
|
@destination ||= site.deploys.find_by(type: 'DeployLocal').destination
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Cambiar al mergear origin-referer
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def hostnames
|
||||||
|
@hostnames ||= site.deploys.map do |deploy|
|
||||||
|
case deploy
|
||||||
|
when DeployLocal
|
||||||
|
site.hostname
|
||||||
|
when DeployWww
|
||||||
|
deploy.fqdn
|
||||||
|
when DeployAlternativeDomain
|
||||||
|
deploy.hostname.dup.tap do |h|
|
||||||
|
h.replace(h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}")
|
||||||
|
end
|
||||||
|
when DeployHiddenService
|
||||||
|
deploy.onion
|
||||||
|
end
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
# Recolecta todas las URIs menos imágenes
|
||||||
|
#
|
||||||
|
# @return [Array]
|
||||||
|
def uris
|
||||||
|
@uris ||= Dir.chdir destination do
|
||||||
|
(Dir.glob('**/*.html') + Dir.glob('public/**/*').reject do |p|
|
||||||
|
File.directory? p
|
||||||
|
end.reject do |p|
|
||||||
|
p = p.downcase
|
||||||
|
|
||||||
|
IMAGES.any? do |i|
|
||||||
|
p.end_with? i
|
||||||
|
end
|
||||||
|
end).map do |uri|
|
||||||
|
"/#{uri}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,6 +37,7 @@ class Site < ApplicationRecord
|
||||||
belongs_to :design
|
belongs_to :design
|
||||||
belongs_to :licencia
|
belongs_to :licencia
|
||||||
|
|
||||||
|
has_many :stats
|
||||||
has_many :log_entries, dependent: :destroy
|
has_many :log_entries, dependent: :destroy
|
||||||
has_many :deploys, dependent: :destroy
|
has_many :deploys, dependent: :destroy
|
||||||
has_many :build_stats, through: :deploys
|
has_many :build_stats, through: :deploys
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Registran cuándo fue la última recolección de datos.
|
||||||
class Stat < ApplicationRecord
|
class Stat < ApplicationRecord
|
||||||
INTERVALS = %i[year month day].freeze
|
# XXX: Los intervalos van en orden de mayor especificidad a menor
|
||||||
|
INTERVALS = %i[day month year].freeze
|
||||||
RESOURCES = %i[builds space_used build_time].freeze
|
RESOURCES = %i[builds space_used build_time].freeze
|
||||||
|
|
||||||
|
belongs_to :site
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue