Path: blob/main/_plugins/gtn/metrics.rb
1677 views
# frozen_string_literal: true12require './_plugins/gtn/mod'3require 'time'45# Monkey patch Hash to make it Prometheus compatible6class Hash7def to_prometheus8if keys.length.positive?9inner = map { |k, v| "#{k}=\"#{v}\"" }.join(',')10"{#{inner}}"11else12''13end14end15end1617module Gtn18# Module for generating metrics for Prometheus19module Metrics20def self.iqr(array)21# https://stackoverflow.com/questions/8856716/calculate-interquartile-mean-from-ruby-array/8856863#885686322arr = array.sort23length = arr.size24quart = (length / 4.0).floor25fraction = 1 - ((length / 4.0) - quart)26new_arr = arr[quart..-(quart + 1)]27((fraction * (new_arr[0] + new_arr[-1])) + new_arr[1..-2].inject(:+)) / (length / 2.0)28end2930def self.bin_width(values)31# https://en.wikipedia.org/wiki/Freedman%E2%80%93Diaconis_rule322 * iqr(values) / (values.length**(1 / 3))33end3435def self.histogram(values)36values.map!(&:to_i)37width = bin_width(values)38bins = ((values.max - values.min) / width).ceil3940(0..bins).map do |bin_idx|41left = values.min + (bin_idx * width)42right = values.min + ((bin_idx + 1) * width)43count = values.select { |x| left <= x and x < right }.length44{45:value => count,46'le' => bin_idx == bins ? '+Inf' : right.to_s47}48end49end5051def self.histogram_dates(values)52day_bins = [1, 7, 28, 90, 365, 365 * 2, 365 * 3, 365 * 5, Float::INFINITY]53last_bin = 054day_bins.map do |bin, _idx|55count = values.select { |x| last_bin <= x and x < bin }.length56last_bin = bin57{58:value => count,59'le' => bin == day_bins[-1] ? '+Inf' : bin60}61end62end6364def self.segment(values, attr)65[66{67:value => values.select { |v| v.key? attr }.length,68attr.to_s => true,69},70{71:value => values.reject { |v| v.key? attr }.length,72attr.to_s => false,73}74]75end7677def self.segment_page_by_key(values, key)78possible_keys = values.map { |v| v.data[key].to_s }.sort.uniq79possible_keys.map do |k|80{81:value => values.select { |v| v.data[key] == k }.length,82key.to_s => k,83}84end85end8687def self.collect_metrics(site)88tutorials = site.pages.select { |x| x.data['layout'] == 'tutorial_hands_on' }89# TODO: materials90# materials = site.pages.select { |x| x.data['layout'] == 'tutorial_hands_on' }91first_commit = Date.parse('2015-06-29')92today = Date.today9394{95'gtn_pages_total' => {96value: segment_page_by_key(site.pages, 'layout'),97help: 'Total number of Pages',98type: 'counter'99},100'gtn_contributors_total' => {101value: segment(site.data['contributors'].values.reject { |x| x['halloffame'] == 'no' }, 'orcid'),102help: 'Total number of contributors',103type: 'counter'104},105'gtn_organisations_total' => {106value: segment(site.data['organisations'].values.reject { |x| x['halloffame'] == 'no' }, 'orcid'),107help: 'Total number of organisations',108type: 'counter'109},110'gtn_grants_total' => {111value: segment(site.data['grants'].values.reject { |x| x['halloffame'] == 'no' }, 'orcid'),112help: 'Total number of grants',113type: 'counter'114},115'gtn_tutorials_total' => {116value: tutorials.length,117help: 'Total number of Hands-on Tutorials',118type: 'counter'119},120'gtn_faqs_total' => {121value: site.pages.select { |x| x.data['layout'] == 'faq' }.length,122help: 'Total number of FAQs',123type: 'counter'124},125'gtn_project_years_total' => {126value: (today - first_commit).to_f / 365,127help: 'Total years of project lifetime',128type: 'counter'129},130'tutorial_update_age_days' => {131type: 'histogram',132help: 'How recently was every single Hands-on Tutorial touched within the GTN, ' \133'grouped by days since last edited.',134value: histogram_dates(135tutorials136.map do |page|137Time.now - Gtn::ModificationTimes.obtain_time(page['path'].gsub(%r{^/}, ''))138end139.map { |seconds| seconds / 3600.0 / 24.0 }140)141},142'tutorial_published_age_days' => {143type: 'histogram',144help: 'How recently was every single Hands-on Tutorial published within the GTN, ' \145'grouped by days since first published.',146value: histogram_dates(147tutorials148.map do |page|149Time.now - Gtn::PublicationTimes.obtain_time(page['path'])150end151.map { |seconds| seconds / 3600.0 / 24.0 }152)153},154'contributor_join_age_days' => {155type: 'histogram',156help: 'When did contributors join? Buckets of their ages by days since joined.',157value: histogram_dates(158site.data['contributors']159.reject { |x| x['halloffame'] == 'no' }160.map do |_, contributor|161(Date.today - Date.parse("#{contributor['joined']}-01")).to_i162end163)164},165}166end167168def self.generate_metrics(site)169data = collect_metrics(site)170data.map do |k, v|171out = "# HELP #{k} #{v[:help]}\n# TYPE #{k} #{v[:type]}\n"172173if v[:value].is_a?(Array)174v[:value].each do |val|175attrs = val.except(:value).to_h176out += "#{k}#{attrs.to_prometheus} #{val[:value]}\n"177end178else179attrs = v.select { |k2, _v2| k2 != :value and k2 != :help and k2 != :type }.to_h180out += "#{k}#{attrs.to_prometheus} #{v[:value]}\n"181end182183out184end.join("\n")185end186end187end188189190