[IMP] runbot: also use module name when fingerprinting

When build logs are scanned, only the message is used for the
fingerprint. When a message is too generic, the created build error use
the first module name it finds but links all other builds with the same
message to this build error.

It's preferable to create more build errors when they occur in different
modules. With this commit, the module name is taken into account when
computing the fingerprint.
This commit is contained in:
Christophe Monniez 2024-09-17 16:43:32 +02:00
parent 3ae3f6dff3
commit 1d98bfafa5
3 changed files with 15 additions and 11 deletions

View File

@ -1183,8 +1183,7 @@ class BuildResult(models.Model):
def _parse_logs(self):
""" Parse build logs to classify errors """
BuildError = self.env['runbot.build.error']
# only parse logs from builds in error and not already scanned
builds_to_scan = self.search([('id', 'in', self.ids), ('local_result', 'in', ('ko', 'killed', 'warn')), ('build_error_link_ids', '=', False)])
builds_to_scan = self.search([('id', 'in', self.ids), ('local_result', 'in', ('ko', 'killed', 'warn'))])
ir_logs = self.env['ir.logging'].search([('level', 'in', ('ERROR', 'WARNING', 'CRITICAL')), ('type', '=', 'server'), ('build_id', 'in', builds_to_scan.ids)])
return BuildError._parse_logs(ir_logs)

View File

@ -96,7 +96,7 @@ class BuildError(models.Model):
cleaned_content = cleaners._r_sub(content)
vals.update({
'cleaned_content': cleaned_content,
'fingerprint': self._digest(cleaned_content)
'fingerprint': self._digest(cleaned_content, vals.get('module_name'))
})
records = super().create(vals_list)
records.action_assign()
@ -112,8 +112,9 @@ class BuildError(models.Model):
if not vals['active'] and build_error.last_seen_date + relativedelta(days=1) > fields.Datetime.now():
raise UserError("This error broke less than one day ago can only be deactivated by admin")
if 'cleaned_content' in vals:
vals.update({'fingerprint': self._digest(vals['cleaned_content'])})
result = super(BuildError, self).write(vals)
module_name = vals.get('module_name', self.module_name)
vals.update({'fingerprint': self._digest(vals['cleaned_content'], module_name)})
result = super().write(vals)
if vals.get('parent_id'):
for build_error in self:
parent = build_error.parent_id
@ -202,11 +203,15 @@ class BuildError(models.Model):
error.error_history_ids = self.search([('fingerprint', 'in', fingerprints), ('active', '=', False), ('id', '!=', error.id or False)])
@api.model
def _digest(self, s):
def _digest(self, s, module_name=''):
"""compute a sha256 digest of a string combined with a module_name
:param str s: a cleaned error
:param str module_name: a module name
:return str: a sha256 digest
"""
return a hash 256 digest of the string s
"""
return hashlib.sha256(s.encode()).hexdigest()
to_fingerprint = f"{module_name}\n{s}"
return hashlib.sha256(to_fingerprint.encode()).hexdigest()
@api.model
def _parse_logs(self, ir_logs):
@ -220,7 +225,7 @@ class BuildError(models.Model):
for log in ir_logs:
if search_regs._r_search(log.message):
continue
fingerprint = self._digest(cleaning_regs._r_sub(log.message))
fingerprint = self._digest(cleaning_regs._r_sub(log.message), log.name)
hash_dict[fingerprint] |= log
build_errors = self.env['runbot.build.error']

View File

@ -59,7 +59,7 @@ class IrLogging(models.Model):
for ir_logging in self:
ir_logging.error_id = False
if ir_logging.level in ('ERROR', 'CRITICAL', 'WARNING') and ir_logging.type == 'server':
fingerprints[self.env['runbot.build.error']._digest(cleaning_regexes._r_sub(ir_logging.message))].append(ir_logging)
fingerprints[self.env['runbot.build.error']._digest(cleaning_regexes._r_sub(ir_logging.message), ir_logging.name)].append(ir_logging)
for build_error in self.env['runbot.build.error'].search([('fingerprint', 'in', list(fingerprints.keys()))], order='active asc'):
for ir_logging in fingerprints[build_error.fingerprint]:
ir_logging.error_id = build_error.id