forked from errbit/errbit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
problem.rb
296 lines (252 loc) · 9.07 KB
/
problem.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# Represents a single Problem. The problem may have been
# reported as various Errs, but the user has grouped the
# Errs together as belonging to the same problem.
# rubocop:disable Metrics/ClassLength. At some point we need to break up this class, but I think it doesn't have to be right now.
class Problem
include Mongoid::Document
include Mongoid::Timestamps
CACHED_NOTICE_ATTRIBUTES = {
messages: :message,
hosts: :host,
user_agents: :user_agent_string
}.freeze
field :last_notice_at, type: ActiveSupport::TimeWithZone, default: proc { Time.zone.now }
field :first_notice_at, type: ActiveSupport::TimeWithZone, default: proc { Time.zone.now }
field :resolved, type: Boolean, default: false
field :resolved_at, type: Time
field :issue_link, type: String
field :issue_type, type: String
# Cached fields
field :app_name, type: String
field :notices_count, type: Integer, default: 0
field :message
field :environment
field :error_class
field :where
field :user_agents, type: Hash, default: {}
field :messages, type: Hash, default: {}
field :hosts, type: Hash, default: {}
field :comments_count, type: Integer, default: 0
index app_id: 1
index app_name: 1
index message: 1
index last_notice_at: 1
index first_notice_at: 1
index resolved_at: 1
index notices_count: 1
index({
error_class: "text",
where: "text",
message: "text",
app_name: "text",
environment: "text"
}, default_language: "english")
belongs_to :app
has_many :errs, inverse_of: :problem, dependent: :destroy
has_many :comments, inverse_of: :err, dependent: :destroy
validates :environment, presence: true
validates :last_notice_at, :first_notice_at, presence: true
before_create :cache_app_attributes
scope :resolved, -> { where(resolved: true) }
scope :unresolved, -> { where(resolved: false) }
scope :ordered, -> { order_by(:last_notice_at.desc) }
scope :for_apps, ->(apps) { where(:app_id.in => apps.all.map(&:id)) }
scope :search, ->(value) { where('$text' => { '$search' => value }) }
def self.all_else_unresolved(fetch_all)
if fetch_all
all
else
where(resolved: false)
end
end
def self.in_env(env)
env.present? ? where(environment: env) : scoped
end
def self.cache_notice(id, notice)
# increment notice count
message_digest = Digest::MD5.hexdigest(notice.message)
host_digest = Digest::MD5.hexdigest(notice.host)
user_agent_digest = Digest::MD5.hexdigest(notice.user_agent_string)
Problem.where('_id' => id).find_one_and_update({
'$set' => {
'environment' => notice.environment_name,
'error_class' => notice.error_class,
'last_notice_at' => notice.created_at.utc,
'message' => notice.message,
'resolved' => false,
'resolved_at' => nil,
'where' => notice.where,
"messages.#{message_digest}.value" => notice.message,
"hosts.#{host_digest}.value" => notice.host,
"user_agents.#{user_agent_digest}.value" => notice.user_agent_string
},
'$inc' => {
'notices_count' => 1,
"messages.#{message_digest}.count" => 1,
"hosts.#{host_digest}.count" => 1,
"user_agents.#{user_agent_digest}.count" => 1
}
}, return_document: :after)
end
def uncache_notice(notice)
last_notice = notices.last
atomically do |doc|
doc.set(
'environment' => last_notice.environment_name,
'error_class' => last_notice.error_class,
'last_notice_at' => last_notice.created_at,
'message' => last_notice.message,
'where' => last_notice.where,
'notices_count' => notices_count.to_i > 1 ? notices_count - 1 : 0
)
CACHED_NOTICE_ATTRIBUTES.each do |k, v|
digest = Digest::MD5.hexdigest(notice.send(v))
field = "#{k}.#{digest}"
if (doc[k].try(:[], digest).try(:[], :count)).to_i > 1
doc.inc("#{field}.count" => -1)
else
doc.unset(field)
end
end
end
end
def recache
CACHED_NOTICE_ATTRIBUTES.each do |k, v|
# clear all cached attributes
send("#{k}=", {})
# find only notices related to this problem
Notice.collection.find.aggregate([
{ "$match" => { err_id: { "$in" => err_ids } } },
{ "$group" => { _id: "$#{v}", count: { "$sum" => 1 } } }
]).each do |agg|
send(k)[Digest::MD5.hexdigest(agg[:_id] || 'N/A')] = {
'value' => agg[:_id] || 'N/A',
'count' => agg[:count]
}
end
end
first_notice = notices.order_by([:created_at, :asc]).first
last_notice = notices.order_by([:created_at, :desc]).first
self.notices_count = notices.count
self.first_notice_at = first_notice.created_at if first_notice
self.message = first_notice.message if first_notice
self.where = first_notice.where if first_notice
self.last_notice_at = last_notice.created_at if last_notice
save
end
def url
Rails.application.routes.url_helpers.app_problem_url(
app,
self,
protocol: Errbit::Config.protocol,
host: Errbit::Config.host,
port: Errbit::Config.port
)
end
def notices
Notice.for_errs(errs).ordered
end
def resolve!
self.update_attributes!(resolved: true, resolved_at: Time.zone.now)
end
def unresolve!
self.update_attributes!(resolved: false, resolved_at: nil)
end
def unresolved?
!resolved?
end
def self.merge!(*problems)
ProblemMerge.new(problems).merge
end
def merged?
errs.length > 1
end
def unmerge!
attrs = { error_class: error_class, environment: environment }
problem_errs = errs.to_a
# associate and return all the problems
new_problems = [self]
# create new problems for each err that needs one
(problem_errs[1..-1] || []).each do |err|
new_problems << app.problems.create(attrs)
err.update_attribute(:problem, new_problems.last)
end
# recache each new problem
new_problems.each(&:recache)
new_problems
end
def grouped_notice_counts(since, group_by = 'day')
key_op = [['year', '$year'], ['day', '$dayOfYear'], ['hour', '$hour']]
key_op = key_op.take(1 + key_op.find_index { |key, _op| group_by == key })
project_date_fields = Hash[*key_op.collect { |key, op| [key, { op => "$created_at" }] }.flatten]
group_id_fields = Hash[*key_op.collect { |key, _op| [key, "$#{key}"] }.flatten]
pipeline = [
{
"$match" => {
"err_id" => { '$in' => errs.map(&:id) },
"created_at" => { "$gt" => since }
}
},
{ "$project" => project_date_fields },
{ "$group" => { "_id" => group_id_fields, "count" => { "$sum" => 1 } } },
{ "$sort" => { "_id" => 1 } }
]
Notice.collection.aggregate(pipeline).find.to_a
end
def zero_filled_grouped_noticed_counts(since, group_by = 'day')
non_zero_filled = grouped_notice_counts(since, group_by)
buckets = group_by == 'day' ? 14 : 24
ruby_time_method = group_by == 'day' ? :yday : :hour
# rubocop:disable Performance/TimesMap
bucket_times = buckets.times.map { |ii| (since + ii.send(group_by)).send(ruby_time_method) }
# rubocop:enable Performance/TimesMap
bucket_times.to_a.map do |bucket_time|
count = if (data_for_day = non_zero_filled.detect { |item| item.dig('_id', group_by) == bucket_time })
data_for_day['count']
else
0
end
{ bucket_time => count }
end
end
def grouped_notice_count_relative_percentages(since, group_by = 'day')
zero_filled = zero_filled_grouped_noticed_counts(since, group_by).map { |h| h.values.first }
max = zero_filled.max
zero_filled.map do |number|
max.zero? ? 0 : number.to_f / max.to_f * 100.0
end
end
def self.ordered_by(sort, order)
case sort
when "app" then order_by(["app_name", order])
when "environment" then order_by(["environment", order])
when "message" then order_by(["message", order])
when "last_notice_at" then order_by(["last_notice_at", order])
when "count" then order_by(["notices_count", order])
else fail("\"#{sort}\" is not a recognized sort")
end
end
def cache_app_attributes
self.app_name = app.name if app
end
def issue_type
# Return issue_type if configured, but fall back to detecting app's issue tracker
attributes['issue_type'] ||=
(app.issue_tracker_configured? && app.issue_tracker.type_tracker) || nil
end
private
def attribute_count_decrease(name, value)
counter = send(name)
index = attribute_index(value)
if counter[index] && counter[index]['count'] > 1
counter[index]['count'] -= 1
else
counter.delete(index)
end
counter
end
def attribute_index(value)
Digest::MD5.hexdigest(value.to_s)
end
end
# rubocop:enable Metrics/ClassLength