[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): def _parse_logs(self):
""" Parse build logs to classify errors """ """ Parse build logs to classify errors """
BuildError = self.env['runbot.build.error'] 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'))])
builds_to_scan = self.search([('id', 'in', self.ids), ('local_result', 'in', ('ko', 'killed', 'warn')), ('build_error_link_ids', '=', False)])
ir_logs = self.env['ir.logging'].search([('level', 'in', ('ERROR', 'WARNING', 'CRITICAL')), ('type', '=', 'server'), ('build_id', 'in', builds_to_scan.ids)]) 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) return BuildError._parse_logs(ir_logs)

View File

@ -96,7 +96,7 @@ class BuildError(models.Model):
cleaned_content = cleaners._r_sub(content) cleaned_content = cleaners._r_sub(content)
vals.update({ vals.update({
'cleaned_content': cleaned_content, 'cleaned_content': cleaned_content,
'fingerprint': self._digest(cleaned_content) 'fingerprint': self._digest(cleaned_content, vals.get('module_name'))
}) })
records = super().create(vals_list) records = super().create(vals_list)
records.action_assign() 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(): 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") raise UserError("This error broke less than one day ago can only be deactivated by admin")
if 'cleaned_content' in vals: if 'cleaned_content' in vals:
vals.update({'fingerprint': self._digest(vals['cleaned_content'])}) module_name = vals.get('module_name', self.module_name)
result = super(BuildError, self).write(vals) vals.update({'fingerprint': self._digest(vals['cleaned_content'], module_name)})
result = super().write(vals)
if vals.get('parent_id'): if vals.get('parent_id'):
for build_error in self: for build_error in self:
parent = build_error.parent_id 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)]) error.error_history_ids = self.search([('fingerprint', 'in', fingerprints), ('active', '=', False), ('id', '!=', error.id or False)])
@api.model @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 to_fingerprint = f"{module_name}\n{s}"
""" return hashlib.sha256(to_fingerprint.encode()).hexdigest()
return hashlib.sha256(s.encode()).hexdigest()
@api.model @api.model
def _parse_logs(self, ir_logs): def _parse_logs(self, ir_logs):
@ -220,7 +225,7 @@ class BuildError(models.Model):
for log in ir_logs: for log in ir_logs:
if search_regs._r_search(log.message): if search_regs._r_search(log.message):
continue 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 hash_dict[fingerprint] |= log
build_errors = self.env['runbot.build.error'] build_errors = self.env['runbot.build.error']

View File

@ -59,7 +59,7 @@ class IrLogging(models.Model):
for ir_logging in self: for ir_logging in self:
ir_logging.error_id = False ir_logging.error_id = False
if ir_logging.level in ('ERROR', 'CRITICAL', 'WARNING') and ir_logging.type == 'server': 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 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]: for ir_logging in fingerprints[build_error.fingerprint]:
ir_logging.error_id = build_error.id ir_logging.error_id = build_error.id