Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
galaxyproject
GitHub Repository: galaxyproject/training-material
Path: blob/main/_plugins/gtn/metrics.rb
1677 views
1
# frozen_string_literal: true
2
3
require './_plugins/gtn/mod'
4
require 'time'
5
6
# Monkey patch Hash to make it Prometheus compatible
7
class Hash
8
def to_prometheus
9
if keys.length.positive?
10
inner = map { |k, v| "#{k}=\"#{v}\"" }.join(',')
11
"{#{inner}}"
12
else
13
''
14
end
15
end
16
end
17
18
module Gtn
19
# Module for generating metrics for Prometheus
20
module Metrics
21
def self.iqr(array)
22
# https://stackoverflow.com/questions/8856716/calculate-interquartile-mean-from-ruby-array/8856863#8856863
23
arr = array.sort
24
length = arr.size
25
quart = (length / 4.0).floor
26
fraction = 1 - ((length / 4.0) - quart)
27
new_arr = arr[quart..-(quart + 1)]
28
((fraction * (new_arr[0] + new_arr[-1])) + new_arr[1..-2].inject(:+)) / (length / 2.0)
29
end
30
31
def self.bin_width(values)
32
# https://en.wikipedia.org/wiki/Freedman%E2%80%93Diaconis_rule
33
2 * iqr(values) / (values.length**(1 / 3))
34
end
35
36
def self.histogram(values)
37
values.map!(&:to_i)
38
width = bin_width(values)
39
bins = ((values.max - values.min) / width).ceil
40
41
(0..bins).map do |bin_idx|
42
left = values.min + (bin_idx * width)
43
right = values.min + ((bin_idx + 1) * width)
44
count = values.select { |x| left <= x and x < right }.length
45
{
46
:value => count,
47
'le' => bin_idx == bins ? '+Inf' : right.to_s
48
}
49
end
50
end
51
52
def self.histogram_dates(values)
53
day_bins = [1, 7, 28, 90, 365, 365 * 2, 365 * 3, 365 * 5, Float::INFINITY]
54
last_bin = 0
55
day_bins.map do |bin, _idx|
56
count = values.select { |x| last_bin <= x and x < bin }.length
57
last_bin = bin
58
{
59
:value => count,
60
'le' => bin == day_bins[-1] ? '+Inf' : bin
61
}
62
end
63
end
64
65
def self.segment(values, attr)
66
[
67
{
68
:value => values.select { |v| v.key? attr }.length,
69
attr.to_s => true,
70
},
71
{
72
:value => values.reject { |v| v.key? attr }.length,
73
attr.to_s => false,
74
}
75
]
76
end
77
78
def self.segment_page_by_key(values, key)
79
possible_keys = values.map { |v| v.data[key].to_s }.sort.uniq
80
possible_keys.map do |k|
81
{
82
:value => values.select { |v| v.data[key] == k }.length,
83
key.to_s => k,
84
}
85
end
86
end
87
88
def self.collect_metrics(site)
89
tutorials = site.pages.select { |x| x.data['layout'] == 'tutorial_hands_on' }
90
# TODO: materials
91
# materials = site.pages.select { |x| x.data['layout'] == 'tutorial_hands_on' }
92
first_commit = Date.parse('2015-06-29')
93
today = Date.today
94
95
{
96
'gtn_pages_total' => {
97
value: segment_page_by_key(site.pages, 'layout'),
98
help: 'Total number of Pages',
99
type: 'counter'
100
},
101
'gtn_contributors_total' => {
102
value: segment(site.data['contributors'].values.reject { |x| x['halloffame'] == 'no' }, 'orcid'),
103
help: 'Total number of contributors',
104
type: 'counter'
105
},
106
'gtn_organisations_total' => {
107
value: segment(site.data['organisations'].values.reject { |x| x['halloffame'] == 'no' }, 'orcid'),
108
help: 'Total number of organisations',
109
type: 'counter'
110
},
111
'gtn_grants_total' => {
112
value: segment(site.data['grants'].values.reject { |x| x['halloffame'] == 'no' }, 'orcid'),
113
help: 'Total number of grants',
114
type: 'counter'
115
},
116
'gtn_tutorials_total' => {
117
value: tutorials.length,
118
help: 'Total number of Hands-on Tutorials',
119
type: 'counter'
120
},
121
'gtn_faqs_total' => {
122
value: site.pages.select { |x| x.data['layout'] == 'faq' }.length,
123
help: 'Total number of FAQs',
124
type: 'counter'
125
},
126
'gtn_project_years_total' => {
127
value: (today - first_commit).to_f / 365,
128
help: 'Total years of project lifetime',
129
type: 'counter'
130
},
131
'tutorial_update_age_days' => {
132
type: 'histogram',
133
help: 'How recently was every single Hands-on Tutorial touched within the GTN, ' \
134
'grouped by days since last edited.',
135
value: histogram_dates(
136
tutorials
137
.map do |page|
138
Time.now - Gtn::ModificationTimes.obtain_time(page['path'].gsub(%r{^/}, ''))
139
end
140
.map { |seconds| seconds / 3600.0 / 24.0 }
141
)
142
},
143
'tutorial_published_age_days' => {
144
type: 'histogram',
145
help: 'How recently was every single Hands-on Tutorial published within the GTN, ' \
146
'grouped by days since first published.',
147
value: histogram_dates(
148
tutorials
149
.map do |page|
150
Time.now - Gtn::PublicationTimes.obtain_time(page['path'])
151
end
152
.map { |seconds| seconds / 3600.0 / 24.0 }
153
)
154
},
155
'contributor_join_age_days' => {
156
type: 'histogram',
157
help: 'When did contributors join? Buckets of their ages by days since joined.',
158
value: histogram_dates(
159
site.data['contributors']
160
.reject { |x| x['halloffame'] == 'no' }
161
.map do |_, contributor|
162
(Date.today - Date.parse("#{contributor['joined']}-01")).to_i
163
end
164
)
165
},
166
}
167
end
168
169
def self.generate_metrics(site)
170
data = collect_metrics(site)
171
data.map do |k, v|
172
out = "# HELP #{k} #{v[:help]}\n# TYPE #{k} #{v[:type]}\n"
173
174
if v[:value].is_a?(Array)
175
v[:value].each do |val|
176
attrs = val.except(:value).to_h
177
out += "#{k}#{attrs.to_prometheus} #{val[:value]}\n"
178
end
179
else
180
attrs = v.select { |k2, _v2| k2 != :value and k2 != :help and k2 != :type }.to_h
181
out += "#{k}#{attrs.to_prometheus} #{v[:value]}\n"
182
end
183
184
out
185
end.join("\n")
186
end
187
end
188
end
189
190