Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/infrahub/core/branch/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ async def migrate_branch(branch: str, context: InfrahubContext, send_events: boo
log.info(f"Running migrations for branch '{obj.name}'")
await migration_runner.run(db=db, at=Timestamp())
except MigrationFailureError as exc:
log.error(f"Failed to run migrations for branch '{obj.name}': {exc.errors}")
log.exception(f"Failed to run migrations for branch '{obj.name}': {exc.errors}")
raise

if obj.status == BranchStatus.NEED_UPGRADE_REBASE:
Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/core/merge/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async def merge(self, *, context: InfrahubContext, proposed_change_id: str | Non
target_branch_name=self.destination_branch.name,
)
except BaseException as exc:
self.log.error("Merge failed, beginning rollback", extra={"error": str(exc)})
self.log.exception("Merge failed, beginning rollback", extra={"error": str(exc)})
await self.rollback_handler.rollback(
merge_started_at=merge_at,
pre_merge_state=pre_merge_state,
Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ async def run_query(
try:
response = await execution_method.run(query=_query, parameters=params)
except ServiceUnavailable as exc:
log.error("Database Service unavailable", error=str(exc))
log.exception("Database Service unavailable", error=str(exc))
raise DatabaseError(message="Unable to connect to the database") from exc

return response
Expand Down
4 changes: 2 additions & 2 deletions backend/infrahub/git/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ def has_conflicting_changes(self, target_branch: str, source_branch: str) -> boo
target=target_branch,
)
return True
log.error(
log.exception(
f"Unexpected error running git merge-tree for {source_branch} into {target_branch}",
repository=self.name,
source=source_branch,
Expand Down Expand Up @@ -899,7 +899,7 @@ def validate_remote_branch(self, branch_name: str) -> bool:
try:
has_conflicts = self.has_conflicting_changes(target_branch=self.default_branch, source_branch=branch_name)
except GitCommandError as exc:
log.error(
log.exception(
"Unable to determine merge conflicts for branch",
branch=branch_name,
repository=self.name,
Expand Down
34 changes: 21 additions & 13 deletions backend/infrahub/git/integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,10 +453,14 @@ async def _build_jinja2_transform_definitions(
except PydanticValidationError as exc:
for error in exc.errors():
locations = [str(error_location) for error_location in error["loc"]]
log.error(f" {'/'.join(locations)} | {error['msg']} ({error['type']})")
# Validation feedback for the user's repository config, reported one line per
# error: a traceback would repeat identically for each and adds nothing.
log.error(f" {'/'.join(locations)} | {error['msg']} ({error['type']})") # noqa: TRY400
continue
except ValidationError as exc:
log.error(exc.message)
# Same user-facing config validation feedback as above: the message is the whole
# actionable content, so no traceback.
log.error(exc.message) # noqa: TRY400
continue

closure = closure_builder.build(
Expand Down Expand Up @@ -635,10 +639,14 @@ async def _build_artifact_definitions(
except PydanticValidationError as exc:
for error in exc.errors():
locations = [str(error_location) for error_location in error["loc"]]
log.error(f" {'/'.join(locations)} | {error['msg']} ({error['type']})")
# Validation feedback for the user's repository config, reported one line per
# error: a traceback would repeat identically for each and adds nothing.
log.error(f" {'/'.join(locations)} | {error['msg']} ({error['type']})") # noqa: TRY400
continue
except ValidationError as exc:
log.error(exc.message)
# Same user-facing config validation feedback as above: the message is the whole
# actionable content, so no traceback.
log.error(exc.message) # noqa: TRY400
continue

local_artifact_defs[artdef.name] = artdef
Expand Down Expand Up @@ -807,7 +815,7 @@ async def get_repository_config(self, branch_name: str, commit: str) -> Infrahub
try:
data = yaml.safe_load(config_file_content)
except yaml.YAMLError as exc:
log.error(f"Unable to load the configuration file in YAML format {config_file_name}: {exc}")
log.exception(f"Unable to load the configuration file in YAML format {config_file_name}: {exc}")
raise RepositoryConfigurationError(
identifier=self.name,
message=f"Repository '{self.name}' has an invalid configuration file '{config_file_name}'. "
Expand All @@ -822,7 +830,7 @@ async def get_repository_config(self, branch_name: str, commit: str) -> Infrahub
log.info(f"Successfully parsed {config_file_name}")
return configuration
except PydanticValidationError as exc:
log.error(f"Unable to load the configuration file {config_file_name}, the format is not valid: {exc}")
log.exception(f"Unable to load the configuration file {config_file_name}, the format is not valid: {exc}")
raise RepositoryConfigurationError(
identifier=self.name,
message=f"Repository '{self.name}' has an invalid configuration file '{config_file_name}'. "
Expand Down Expand Up @@ -944,7 +952,7 @@ async def _build_graphql_query_definitions(
relative_path=str(commit_wt.directory),
)
except InfrahubSdkError as exc:
log.error(f"Query '{query_config.name}': {exc}")
log.exception(f"Query '{query_config.name}': {exc}")
raise

return local_queries
Expand Down Expand Up @@ -1565,7 +1573,7 @@ async def get_check_definition(
)

except Exception as exc:
log.error(
log.exception(
f"An error occurred while processing the CheckDefinition {check_class.__name__} from {file_path} : {exc} "
)
raise
Expand Down Expand Up @@ -1605,7 +1613,7 @@ async def get_python_transforms(
)

except Exception as exc:
log.error(
log.exception(
f"An error occurred while processing the PythonTransform {transform.name} from {file_path} : {exc} "
)
raise
Expand Down Expand Up @@ -1893,14 +1901,14 @@ async def execute_python_check(

except ModuleNotFoundError as exc:
error_msg = "Unable to load the check file"
log.error(error_msg)
log.exception(error_msg)
raise CheckError(
repository_name=self.name, class_name=class_name, commit=commit, location=location, message=error_msg
) from exc

except AttributeError as exc:
error_msg = f"Unable to find the class {class_name}"
log.error(error_msg)
log.exception(error_msg)
raise CheckError(
repository_name=self.name, class_name=class_name, commit=commit, location=location, message=error_msg
) from exc
Expand Down Expand Up @@ -1965,14 +1973,14 @@ async def execute_python_transform(
return await transform.run(data=data)
except ModuleNotFoundError as exc:
error_msg = f"Unable to load the transform file {location}"
log.error(error_msg)
log.exception(error_msg)
raise TransformError(
repository_name=self.name, commit=commit, location=location, message=error_msg
) from exc

except AttributeError as exc:
error_msg = f"Unable to find the class {class_name} in {location}"
log.error(error_msg)
log.exception(error_msg)
raise TransformError(
repository_name=self.name, commit=commit, location=location, message=error_msg
) from exc
Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/git/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ async def update_latest_commit(self) -> None:
try:
latest_commit = git_repo.git.rev_parse(self.ref)
except GitCommandError as err:
log.error(f"No object found for ref {self.ref} on repository {self.name}")
log.exception(f"No object found for ref {self.ref} on repository {self.name}")
raise ValueError(f"Ref {self.ref} not found.") from err
latest_commit = str(git_repo.commit(latest_commit))
synced_from_remote = await self.sync_from_remote(commit=latest_commit)
Expand Down
2 changes: 1 addition & 1 deletion backend/infrahub/git/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,7 @@ async def run_user_check(model: UserCheckData) -> ValidatorConclusion:
log_entries = check_run.log_entries
except CheckError as exc:
log.warning("The check failed to run")
log.error(exc.message)
log.exception(exc.message)
log_entries = f"FATAL Error/n:{exc.message}"

check = None
Expand Down
8 changes: 6 additions & 2 deletions backend/infrahub/graphql/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ async def _handle_http_request(
except ValueError as exc:
return JSONResponse({"errors": [exc.args[0]]}, status_code=400)
except ClientDisconnect as exc:
self.logger.error("Exception ClientDisconnect in _handle_http_request")
# A client aborting mid-request is routine, and the traceback only shows the body-read
# path, so it would be non-actionable noise on a normal operating event.
self.logger.error("Exception ClientDisconnect in _handle_http_request") # noqa: TRY400
return JSONResponse({"errors": [str(exc)]}, status_code=400)

if isinstance(operations, list):
Expand Down Expand Up @@ -532,7 +534,9 @@ async def _observe_subscription(
await websocket.send_json({"type": GQL_DATA, "id": operation_id, "payload": payload})
except Exception as error:
if not isinstance(error, GraphQLError):
self.logger.error("An exception occurred in resolvers", exc_info=error)
# Inside the handler, so the active exception is attached implicitly; the helper
# below runs outside any except block and must pass it explicitly instead.
self.logger.exception("An exception occurred in resolvers")
error = GraphQLError(str(error), original_error=error)
await websocket.send_json(
{
Expand Down
4 changes: 2 additions & 2 deletions backend/infrahub/services/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ async def run_schedule(self, schedule: Schedule) -> None:
try:
await schedule.function(self.service)
# Keep-alive: a failing recurring task must not kill the scheduler loop
except Exception as exc: # noqa: BLE001
self.service.log.error(str(exc))
except Exception as exc:
self.service.log.exception(str(exc))
for _ in range(schedule.interval):
if not self.running:
return
Expand Down
6 changes: 5 additions & 1 deletion backend/infrahub/webhook/tasks/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,11 @@ async def webhook_send(
except WebhookDeliveryError as error:
elapsed_ms = (time.monotonic() - started) * 1_000
failure = error.failure
log.error(
# Deliberately not `exception`: WebhookDeliveryError is registered for traceback
# suppression, and the filter drops the *whole* record for a registered type - attaching
# the exception here would delete this classified failure report from the run logs. The
# traceback still reaches the caller via the re-raise below.
log.error( # noqa: TRY400
get_webhook_log_formatter().delivery_failed(
status_class=failure.status_class,
message=failure.message,
Expand Down
6 changes: 4 additions & 2 deletions backend/infrahub/workers/infrahub_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,17 @@ async def _init_infrahub_client(self, client: InfrahubClient | None = None) -> I
)
)
except InitializationError as err:
self._logger.error(
# A missing configuration value, reported before a clean exit: there is nothing
# in a traceback to diagnose.
self._logger.error( # noqa: TRY400
"Infrahub client initialization failed due to missing configuration for internal_address."
)
raise typer.Exit(1) from err

try:
await client.branch.all()
except SdkError as err:
self._logger.error(f"Error in communication with Infrahub: {err.message}")
self._logger.exception(f"Error in communication with Infrahub: {err.message}")
raise typer.Exit(1) from err

return client
Expand Down
7 changes: 6 additions & 1 deletion backend/tests/adapters/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ def critical(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
"""Send a critical event."""

def exception(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
"""Send an exception event."""
"""Send an exception event.

Recorded alongside the error events because an exception event is emitted at error level -
it only adds the active exception's traceback to the record.
"""
self.error_logs.append(event)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The TRY400 (error-instead-of-exception) ruff rule is now enforced — a `log.error` reporting a caught exception is either `log.exception`, so the traceback reaches the logs, or carries an explicit justified `# noqa: TRY400`. Nine now-redundant `# noqa: BLE001` comments were removed as a result.
48 changes: 48 additions & 0 deletions dev/specs/005-ruff-try400-tracebacks/alignment-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Spec/Ask Alignment Check

**Date**: 2026-08-11 | **Feature**: `dev/specs/005-ruff-try400-tracebacks/`

## Source

The source-of-truth ask is the **inline PRD** passed to this run, composed from Jira card
**INBOX-29** (Engineering Inbox, Tech Debt) by the platform-health drain pipeline. It carries the
card's Overview, Suggested solution, the measured ground truth, the TRY400-only scope decision, a
4-item "WHAT TO BUILD", an acceptance list, and a hard-constraints block.

Two referenced URLs were **not** fetched:

- the card's provenance link (`opsmillworkspace.slack.com/...`) — an authenticated Slack
permalink, not reachable; its substance is already quoted in the card and carried into the ask.
- the card itself was read directly via the Jira API before this run, not re-fetched here.

Neither is requirement-bearing beyond what the inline ask already states, so the check runs
against the inline ask.

## Verdict

**✅ ALIGNED** — 0 remediation passes used.

## Findings

| Severity | Category | Ask reference | Spec reference | Description |
|----------|----------|---------------|----------------|-------------|
| info | expansion | WHAT TO BUILD #2 ("if `exception` would be wrong there, use a targeted `# noqa: TRY400`") | research.md §R4, FR-003 | The ask authorised per-site noqa in the abstract; the spec/research resolve it concretely into 27 conversions + 7 justified suppressions. Elaboration of an explicit instruction, not drift. |
| info | added | — | SC-007 | "Every remaining `# noqa: TRY400` carries a one-line justification" is a criterion the ask implied ("with a one-line reason") but did not list under ACCEPTANCE. Added as a verifiable gate. |
| info | added | — | research.md §R3 | The `TracebackSuppressionFilter` interaction was discovered during Phase 0, not present in the ask. It *narrows* scope at one site for a correctness reason and is documented. |
| info | changed | ask: "the 34 TRY400 violations ... EXCEPT auth/auth.py" | spec.md Context | The ask's own arithmetic (36 total, 2 in auth ⇒ 34 in scope) is preserved exactly; the spec additionally publishes the full 36-site distribution table. Presentation only. |

**No** missing requirements, **no** off-scope additions, **no** softened or dropped acceptance
criteria, **no** contradicted constraints. Specifically confirmed present in the spec:

- TRY004 out of scope, with the reason (spec "Out of Scope — TRY004", SC-003)
- `extend-select` mechanism and the TRY200-removed-rule warning (Assumptions, research.md §R1)
- no dependency-list edits (FR-002)
- `auth/auth.py` untouched, suppressed by file with a commented reason (FR-006)
- changelog fragment conditional on repo convention (FR-008)
- all four hard-constraint categories (FR-007, SC-005)
- structlog keyword-argument preservation (FR-004)
- test-assertion exposure for log records (research.md §R7)

## Action

Proceed to implementation. No phases re-run.
46 changes: 46 additions & 0 deletions dev/specs/005-ruff-try400-tracebacks/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Specification Quality Checklist: Re-enable ruff TRY400 so error logs carry tracebacks
Comment thread
saltas888 marked this conversation as resolved.

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-08-11
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

**Note on the first two items**: this is a lint/tooling feature, so its "user" is an Infrahub
developer and its subject matter is inherently the lint configuration. Naming ruff, TRY400 and
`pyproject.toml` is describing *what* the change is, not leaking a chosen implementation. The
spec still avoids prescribing per-site edits — those belong to plan/tasks.

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- Scope was narrowed from the source card (INBOX-29 named TRY004 **and** TRY400) to TRY400 only.
The spec documents this in "Out of Scope — TRY004" with the reason: TRY004's fix changes
caller-visible exception types on schema and GraphQL surfaces, which needs human design review.
This is an intentional, recorded scope reduction, not drift.
- Violation counts in the spec were measured on this branch rather than taken from the card
(card said ~56; actual is 76 = 36 TRY400 + 40 TRY004).
- FR-006's auth.py carve-out is a pipeline-permission boundary. The spec's Assumptions section
records that the merged BLE precedent edited the same file, so a reviewer can overrule it.
Loading
Loading