[IMP] maintenance_service_http_monitoring: rework maintenance.request creation
Previously, a single ``maintenance.request`` was created per equipment,
regardless of how many services were down on that equipment. The name
was ``[HTTP KO] {equipment.name}`` and deduplication relied on a
name+date+equipment search that was fragile (manual clear of the field
would lose the reference to an existing open request).
This commit reworks the whole creation logic:
- **1 request per KO service** instead of 1 per equipment. Each failing
``service.instance`` gets its own ``maintenance.request``, allowing
fine-grained tracking and independent resolution.
- **Request name** is now ``[HTTP KO] {service_url}``, making it
immediately identifiable without opening the record.
- **Description** includes the error detail: ``HTTP {status_code}`` for
HTTP errors, or a human-readable network error label when
``last_http_status_code == -1`` (timeout / DNS / SSL failures).
- **Deduplication** is now based solely on whether an open (non-done)
``maintenance.request`` already exists on the ``service.instance``
via the new ``http_maintenance_request`` field. No date boundary —
as long as the request is open, no new one is created.
- **``http_maintenance_request``** field moved from
``maintenance.equipment`` to ``service.instance``, where it belongs
given the 1-request-per-service model. It is exposed as an optional
hidden column in the service instance list view.
- ``_build_ko_services_description()`` is removed (no longer needed).
- ``create_http_maintenance_request()`` now receives a single
``service.instance`` recordset instead of a list.
Tests updated accordingly (14 tests total):
- Tests 2, 4, 9, 10, 12, 13 now assert on
``service_instance.http_maintenance_request``.
- Test 2 also verifies the request name contains the service URL and
the description contains the HTTP status code.
- New test 14 asserts that two KO services on the same equipment
produce two distinct requests with the correct names.
This commit is contained in:
@@ -98,10 +98,11 @@ On service instances, you can see:
|
|||||||
## Automatic Maintenance Requests
|
## Automatic Maintenance Requests
|
||||||
|
|
||||||
When a service fails HTTP checks:
|
When a service fails HTTP checks:
|
||||||
- A corrective maintenance request is created with prefix "[HTTP KO]"
|
- A corrective maintenance request is created per failing service, named
|
||||||
- The request is linked to the equipment
|
``[HTTP KO] {service_url}``
|
||||||
- Only one request per equipment per day is created
|
- The request description includes the error detail: the HTTP status code,
|
||||||
- The request description lists all failing services
|
or a network error label (timeout / DNS / SSL) when no HTTP response was received
|
||||||
|
- No duplicate is created as long as an open request already exists for that service
|
||||||
- A **double-check** is performed before creating the request: the service is retested
|
- A **double-check** is performed before creating the request: the service is retested
|
||||||
after 2 seconds. A maintenance request is only created if the service fails **both**
|
after 2 seconds. A maintenance request is only created if the service fails **both**
|
||||||
checks, reducing noise from transient HTTP errors
|
checks, reducing noise from transient HTTP errors
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ class MaintenanceEquipment(models.Model):
|
|||||||
readonly=True,
|
readonly=True,
|
||||||
help="Computed from start + configured duration",
|
help="Computed from start + configured duration",
|
||||||
)
|
)
|
||||||
http_maintenance_request = fields.Many2one(
|
|
||||||
"maintenance.request",
|
|
||||||
string="HTTP Maintenance Request",
|
|
||||||
readonly=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_activate_maintenance_mode(self):
|
def action_activate_maintenance_mode(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
@@ -72,31 +67,30 @@ class MaintenanceEquipment(models.Model):
|
|||||||
)
|
)
|
||||||
expired.action_deactivate_maintenance_mode()
|
expired.action_deactivate_maintenance_mode()
|
||||||
|
|
||||||
def create_http_maintenance_request(self, ko_services):
|
def create_http_maintenance_request(self, ko_service):
|
||||||
|
"""
|
||||||
|
Create or return the open maintenance.request for a single KO service.
|
||||||
|
|
||||||
|
Deduplication: if ko_service already has an open (non-done) request,
|
||||||
|
return it without creating a new one.
|
||||||
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
today = fields.Date.context_today(self)
|
existing = ko_service.http_maintenance_request
|
||||||
name = f"[HTTP KO] {self.name}"
|
if existing and not existing.stage_id.done:
|
||||||
domain = [
|
|
||||||
("name", "=", name),
|
|
||||||
("equipment_id", "=", self.id),
|
|
||||||
("maintenance_type", "=", "corrective"),
|
|
||||||
("create_date", ">=", f"{today} 00:00:00"),
|
|
||||||
("create_date", "<=", f"{today} 23:59:59"),
|
|
||||||
]
|
|
||||||
existing = self.env["maintenance.request"].search(domain, limit=1)
|
|
||||||
# Check if a task with same name already exist for the day, if it’s the case : skip
|
|
||||||
if existing:
|
|
||||||
self.http_maintenance_request = existing.id
|
|
||||||
return existing
|
return existing
|
||||||
request = self.http_maintenance_request
|
status_code = ko_service.last_http_status_code
|
||||||
if request and not request.stage_id.done:
|
if status_code == -1:
|
||||||
return request
|
error_detail = "Erreur réseau (timeout / DNS / SSL)"
|
||||||
|
else:
|
||||||
|
error_detail = f"HTTP {status_code}"
|
||||||
|
name = f"[HTTP KO] {ko_service.service_url}"
|
||||||
|
description = f"Service KO: {ko_service.service_url}\n{error_detail}"
|
||||||
vals = {
|
vals = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"equipment_id": self.id,
|
"equipment_id": self.id,
|
||||||
"priority": "2",
|
"priority": "2",
|
||||||
"maintenance_type": "corrective",
|
"maintenance_type": "corrective",
|
||||||
"description": self._build_ko_services_description(ko_services),
|
"description": description,
|
||||||
}
|
}
|
||||||
if self.employee_id:
|
if self.employee_id:
|
||||||
vals["employee_id"] = self.employee_id.id
|
vals["employee_id"] = self.employee_id.id
|
||||||
@@ -109,8 +103,8 @@ class MaintenanceEquipment(models.Model):
|
|||||||
if team:
|
if team:
|
||||||
vals["maintenance_team_id"] = team.id
|
vals["maintenance_team_id"] = team.id
|
||||||
request = self.env["maintenance.request"].create(vals)
|
request = self.env["maintenance.request"].create(vals)
|
||||||
self.http_maintenance_request = request.id
|
ko_service.http_maintenance_request = request.id
|
||||||
self._notify_webhook(request, ko_services)
|
self._notify_webhook(request, ko_service)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def _notify_webhook(self, request, ko_service):
|
def _notify_webhook(self, request, ko_service):
|
||||||
@@ -156,7 +150,3 @@ class MaintenanceEquipment(models.Model):
|
|||||||
request.id,
|
request.id,
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _build_ko_services_description(self, ko_services):
|
|
||||||
lines = [f"Service KO: {s.service_url or s.name}" for s in ko_services]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ class ServiceInstance(models.Model):
|
|||||||
readonly=True,
|
readonly=True,
|
||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
|
http_maintenance_request = fields.Many2one(
|
||||||
|
"maintenance.request",
|
||||||
|
string="HTTP Maintenance Request",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
def check_http_status(self):
|
def check_http_status(self):
|
||||||
"""
|
"""
|
||||||
@@ -98,4 +103,4 @@ class ServiceInstance(models.Model):
|
|||||||
ko_confirmed = ko_after_pass1.check_http_status()
|
ko_confirmed = ko_after_pass1.check_http_status()
|
||||||
|
|
||||||
for service in ko_confirmed:
|
for service in ko_confirmed:
|
||||||
service.equipment_id.create_http_maintenance_request([service])
|
service.equipment_id.create_http_maintenance_request(service)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class TestHttpMonitoring(TransactionCase):
|
|||||||
self.assertIsNotNone(self.service_instance.last_http_check_date)
|
self.assertIsNotNone(self.service_instance.last_http_check_date)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Test 2 -- Two KO passes -> maintenance.request created
|
# Test 2 -- Two KO passes -> maintenance.request created on the service
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def test_http_500_creates_maintenance_request(self):
|
def test_http_500_creates_maintenance_request(self):
|
||||||
with (
|
with (
|
||||||
@@ -69,11 +69,13 @@ class TestHttpMonitoring(TransactionCase):
|
|||||||
self.assertFalse(self.service_instance.http_status_ok)
|
self.assertFalse(self.service_instance.http_status_ok)
|
||||||
self.assertEqual(self.service_instance.last_http_status_code, 500)
|
self.assertEqual(self.service_instance.last_http_status_code, 500)
|
||||||
|
|
||||||
request = self.equipment.http_maintenance_request
|
request = self.service_instance.http_maintenance_request
|
||||||
self.assertTrue(request)
|
self.assertTrue(request)
|
||||||
self.assertEqual(request.name, f"[HTTP KO] {self.equipment.name}")
|
self.assertEqual(request.name, f"[HTTP KO] {self.service_instance.service_url}")
|
||||||
self.assertEqual(request.priority, "2")
|
self.assertEqual(request.priority, "2")
|
||||||
self.assertEqual(request.maintenance_type, "corrective")
|
self.assertEqual(request.maintenance_type, "corrective")
|
||||||
|
self.assertIn("HTTP 500", request.description)
|
||||||
|
self.assertIn(self.service_instance.service_url, request.description)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Test 3 -- Network error -> KO with code -1
|
# Test 3 -- Network error -> KO with code -1
|
||||||
@@ -99,7 +101,7 @@ class TestHttpMonitoring(TransactionCase):
|
|||||||
mock_requests.exceptions.RequestException = Exception
|
mock_requests.exceptions.RequestException = Exception
|
||||||
self.env["service.instance"].cron_check_http_services()
|
self.env["service.instance"].cron_check_http_services()
|
||||||
|
|
||||||
request_1 = self.equipment.http_maintenance_request
|
request_1 = self.service_instance.http_maintenance_request
|
||||||
self.assertTrue(request_1)
|
self.assertTrue(request_1)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
@@ -110,7 +112,7 @@ class TestHttpMonitoring(TransactionCase):
|
|||||||
mock_requests.exceptions.RequestException = Exception
|
mock_requests.exceptions.RequestException = Exception
|
||||||
self.env["service.instance"].cron_check_http_services()
|
self.env["service.instance"].cron_check_http_services()
|
||||||
|
|
||||||
self.assertEqual(self.equipment.http_maintenance_request, request_1)
|
self.assertEqual(self.service_instance.http_maintenance_request, request_1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.env["maintenance.request"].search_count(
|
self.env["maintenance.request"].search_count(
|
||||||
[("equipment_id", "=", self.equipment.id)]
|
[("equipment_id", "=", self.equipment.id)]
|
||||||
@@ -247,11 +249,9 @@ class TestHttpMonitoring(TransactionCase):
|
|||||||
mock_requests.exceptions.RequestException = Exception
|
mock_requests.exceptions.RequestException = Exception
|
||||||
self.env["service.instance"].cron_check_http_services()
|
self.env["service.instance"].cron_check_http_services()
|
||||||
|
|
||||||
# Final status reflects pass 2 result
|
|
||||||
self.assertTrue(self.service_instance.http_status_ok)
|
self.assertTrue(self.service_instance.http_status_ok)
|
||||||
self.assertEqual(self.service_instance.last_http_status_code, 200)
|
self.assertEqual(self.service_instance.last_http_status_code, 200)
|
||||||
# No maintenance.request must have been created
|
self.assertFalse(self.service_instance.http_maintenance_request)
|
||||||
self.assertFalse(self.equipment.http_maintenance_request)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.env["maintenance.request"].search_count(
|
self.env["maintenance.request"].search_count(
|
||||||
[("equipment_id", "=", self.equipment.id)]
|
[("equipment_id", "=", self.equipment.id)]
|
||||||
@@ -271,12 +271,44 @@ class TestHttpMonitoring(TransactionCase):
|
|||||||
mock_requests.exceptions.RequestException = Exception
|
mock_requests.exceptions.RequestException = Exception
|
||||||
self.env["service.instance"].cron_check_http_services()
|
self.env["service.instance"].cron_check_http_services()
|
||||||
|
|
||||||
# sleep must have been called between the two passes
|
|
||||||
mock_sleep.assert_called_once_with(2)
|
mock_sleep.assert_called_once_with(2)
|
||||||
# requests.get must have been called twice (pass 1 + pass 2)
|
|
||||||
self.assertEqual(mock_requests.get.call_count, 2)
|
self.assertEqual(mock_requests.get.call_count, 2)
|
||||||
# Final status is KO
|
|
||||||
self.assertFalse(self.service_instance.http_status_ok)
|
self.assertFalse(self.service_instance.http_status_ok)
|
||||||
self.assertEqual(self.service_instance.last_http_status_code, 503)
|
self.assertEqual(self.service_instance.last_http_status_code, 503)
|
||||||
# maintenance.request created
|
self.assertTrue(self.service_instance.http_maintenance_request)
|
||||||
self.assertTrue(self.equipment.http_maintenance_request)
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 14 -- 2 KO services on same equipment -> 2 distinct requests
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_two_ko_services_same_equipment_create_two_requests(self):
|
||||||
|
service2 = self.env["service"].create({"name": "Test Service 2"})
|
||||||
|
service_instance2 = self.env["service.instance"].create(
|
||||||
|
{
|
||||||
|
"equipment_id": self.equipment.id,
|
||||||
|
"service_id": service2.id,
|
||||||
|
"service_url": "https://other.example.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(SERVICE_INSTANCE_REQUESTS) as mock_requests,
|
||||||
|
patch(SERVICE_INSTANCE_SLEEP),
|
||||||
|
):
|
||||||
|
mock_requests.get.return_value = _mock_response(500)
|
||||||
|
mock_requests.exceptions.RequestException = Exception
|
||||||
|
self.env["service.instance"].cron_check_http_services()
|
||||||
|
|
||||||
|
req1 = self.service_instance.http_maintenance_request
|
||||||
|
req2 = service_instance2.http_maintenance_request
|
||||||
|
|
||||||
|
self.assertTrue(req1)
|
||||||
|
self.assertTrue(req2)
|
||||||
|
self.assertNotEqual(req1, req2)
|
||||||
|
self.assertEqual(req1.name, f"[HTTP KO] {self.service_instance.service_url}")
|
||||||
|
self.assertEqual(req2.name, f"[HTTP KO] {service_instance2.service_url}")
|
||||||
|
self.assertEqual(
|
||||||
|
self.env["maintenance.request"].search_count(
|
||||||
|
[("equipment_id", "=", self.equipment.id)]
|
||||||
|
),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
<field name="maintenance_mode" />
|
<field name="maintenance_mode" />
|
||||||
<field name="maintenance_mode_start" />
|
<field name="maintenance_mode_start" />
|
||||||
<field name="maintenance_mode_end" />
|
<field name="maintenance_mode_end" />
|
||||||
<field name="http_maintenance_request" readonly="1" />
|
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<field name="last_http_check_date" />
|
<field name="last_http_check_date" />
|
||||||
<field name="last_http_status_code" />
|
<field name="last_http_status_code" />
|
||||||
<field name="http_status_ok" />
|
<field name="http_status_ok" />
|
||||||
|
<field name="http_maintenance_request" optional="hide" />
|
||||||
</field>
|
</field>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
Reference in New Issue
Block a user