Auto-commit local changes before build (2026-01-16 09:04:12)

This commit is contained in:
Ivo Oskamp 2026-01-16 09:04:12 +01:00
parent 83a29a7a3c
commit abb6780744
3 changed files with 120 additions and 5 deletions

View File

@ -1 +1 @@
v20260115-17-autotask-ticket-create-trackingid-lookup v20260115-19-autotask-ticket-create-debug-logging

View File

@ -1,10 +1,16 @@
import json import json
import logging
import os
import uuid
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import requests import requests
logger = logging.getLogger(__name__)
@dataclass @dataclass
class AutotaskZoneInfo: class AutotaskZoneInfo:
zone_name: str zone_name: str
@ -37,6 +43,29 @@ class AutotaskClient:
self._zone_info: Optional[AutotaskZoneInfo] = None self._zone_info: Optional[AutotaskZoneInfo] = None
self._zoneinfo_base_used: Optional[str] = None self._zoneinfo_base_used: Optional[str] = None
def _debug_enabled(self) -> bool:
"""Return True when verbose Autotask integration logging is enabled.
This is intentionally controlled via an environment variable to avoid
writing sensitive payloads to logs by default.
"""
return str(os.getenv("BACKUPCHECKS_AUTOTASK_DEBUG", "")).strip().lower() in {
"1",
"true",
"yes",
"on",
}
def _safe_json_preview(self, data: Any, max_chars: int = 1200) -> str:
"""Serialize JSON-like data for logging, truncating large payloads."""
try:
s = json.dumps(data, ensure_ascii=False, default=str)
except Exception:
s = str(data)
if len(s) > max_chars:
return s[:max_chars] + ""
return s
def _zoneinfo_bases(self) -> List[str]: def _zoneinfo_bases(self) -> List[str]:
"""Return a list of zoneInformation base URLs to try. """Return a list of zoneInformation base URLs to try.
@ -430,6 +459,7 @@ class AutotaskClient:
self, self,
tracking_identifier: str, tracking_identifier: str,
company_id: Optional[int] = None, company_id: Optional[int] = None,
corr_id: Optional[str] = None,
) -> Optional[int]: ) -> Optional[int]:
"""Lookup the most recently created ticket by tracking identifier. """Lookup the most recently created ticket by tracking identifier.
@ -461,8 +491,22 @@ class AutotaskClient:
} }
params = {"search": json.dumps(search_payload)} params = {"search": json.dumps(search_payload)}
if self._debug_enabled():
logger.info(
"[autotask][%s] Tickets/query lookup payload=%s",
corr_id or "-",
self._safe_json_preview(search_payload, max_chars=1200),
)
data = self._request("GET", "Tickets/query", params=params) data = self._request("GET", "Tickets/query", params=params)
items = self._as_items_list(data) items = self._as_items_list(data)
if self._debug_enabled():
logger.info(
"[autotask][%s] Tickets/query lookup result_count=%s keys=%s",
corr_id or "-",
len(items),
(sorted(list(items[0].keys())) if items and isinstance(items[0], dict) else None),
)
if not items: if not items:
return None return None
@ -480,8 +524,36 @@ class AutotaskClient:
if not isinstance(payload, dict) or not payload: if not isinstance(payload, dict) or not payload:
raise AutotaskError("Ticket payload is empty.") raise AutotaskError("Ticket payload is empty.")
corr_id = uuid.uuid4().hex[:10]
if self._debug_enabled():
# Avoid dumping full descriptions by default, but include key routing fields.
payload_keys = sorted(list(payload.keys()))
logger.info(
"[autotask][%s] POST /Tickets payload_keys=%s companyID=%s queueID=%s source=%s status=%s priority=%s trackingIdentifier=%s",
corr_id,
payload_keys,
payload.get("companyID") or payload.get("CompanyID") or payload.get("companyId"),
payload.get("queueID") or payload.get("QueueID") or payload.get("queueId"),
payload.get("source") or payload.get("Source") or payload.get("sourceId") or payload.get("sourceID"),
payload.get("status") or payload.get("Status") or payload.get("statusId") or payload.get("statusID"),
payload.get("priority") or payload.get("Priority"),
payload.get("trackingIdentifier") or payload.get("TrackingIdentifier"),
)
resp = self._request_raw("POST", "Tickets", json_body=payload) resp = self._request_raw("POST", "Tickets", json_body=payload)
if self._debug_enabled():
location = (resp.headers.get("Location") or resp.headers.get("location") or "").strip()
logger.info(
"[autotask][%s] POST /Tickets response http=%s content_type=%s content_length=%s location=%s",
corr_id,
resp.status_code,
(resp.headers.get("Content-Type") or resp.headers.get("content-type") or ""),
(len(resp.content or b"") if resp is not None else None),
location or None,
)
data: Any = {} data: Any = {}
if resp.content: if resp.content:
try: try:
@ -489,6 +561,25 @@ class AutotaskClient:
except Exception: except Exception:
# Some tenants return an empty body or a non-JSON body on successful POST. # Some tenants return an empty body or a non-JSON body on successful POST.
data = {} data = {}
if self._debug_enabled():
# Log a short preview of the raw body to understand tenant behaviour.
try:
body_preview = (resp.text or "")[:600]
except Exception:
body_preview = ""
logger.info(
"[autotask][%s] POST /Tickets non-JSON body preview=%s",
corr_id,
body_preview,
)
if self._debug_enabled():
logger.info(
"[autotask][%s] POST /Tickets parsed_json_type=%s json_preview=%s",
corr_id,
type(data).__name__,
self._safe_json_preview(data, max_chars=1200),
)
ticket_id: Optional[int] = None ticket_id: Optional[int] = None
@ -526,6 +617,13 @@ class AutotaskClient:
except Exception: except Exception:
ticket_id = None ticket_id = None
if self._debug_enabled():
logger.info(
"[autotask][%s] POST /Tickets extracted_ticket_id=%s",
corr_id,
ticket_id,
)
# If we have an ID, fetch the full ticket object so callers can reliably access ticketNumber etc. # If we have an ID, fetch the full ticket object so callers can reliably access ticketNumber etc.
if ticket_id is not None: if ticket_id is not None:
return self.get_ticket(ticket_id) return self.get_ticket(ticket_id)
@ -542,7 +640,19 @@ class AutotaskClient:
company_id = int(payload[ck]) company_id = int(payload[ck])
break break
looked_up_id = self._lookup_created_ticket_id(str(tracking_identifier), company_id=company_id) if self._debug_enabled():
logger.info(
"[autotask][%s] fallback lookup by TrackingIdentifier=%s companyID=%s",
corr_id,
str(tracking_identifier),
company_id,
)
looked_up_id = self._lookup_created_ticket_id(
str(tracking_identifier),
company_id=company_id,
corr_id=corr_id,
)
if looked_up_id is not None: if looked_up_id is not None:
return self.get_ticket(looked_up_id) return self.get_ticket(looked_up_id)
@ -553,7 +663,6 @@ class AutotaskClient:
raise AutotaskError( raise AutotaskError(
"Autotask did not return a ticket id. " "Autotask did not return a ticket id. "
"Ticket creation may still have succeeded; configure ticket TrackingIdentifier to be unique per run " "Ticket creation may still have succeeded. "
"to allow deterministic lookup. " f"(HTTP {resp.status_code}, Correlation={corr_id})."
f"(HTTP {resp.status_code})."
) )

View File

@ -123,6 +123,12 @@ Changes:
- Ensured the created ticket is reliably retrieved via follow-up GET /Tickets/{id} so ticketNumber/id can always be stored. - Ensured the created ticket is reliably retrieved via follow-up GET /Tickets/{id} so ticketNumber/id can always be stored.
- Eliminated false-negative ticket creation errors when Autotask returns an empty body and no Location header. - Eliminated false-negative ticket creation errors when Autotask returns an empty body and no Location header.
## v20260115-19-autotask-ticket-create-debug-logging
- Added optional verbose Autotask ticket creation logging (guarded by BACKUPCHECKS_AUTOTASK_DEBUG=1).
- Introduced per-request correlation IDs and included them in ticket creation error messages for log tracing.
- Logged POST /Tickets response characteristics (status, headers, body preview) to diagnose tenants returning incomplete create responses.
- Logged fallback Tickets/query lookup payload and result shape to pinpoint why deterministic lookup fails.
*** ***
## v0.1.21 ## v0.1.21