Log Repixelizer job actors for ops summaries

This commit is contained in:
Meta Vens 2026-05-01 16:00:54 +01:00
commit da25c88eb8
3 changed files with 109 additions and 7 deletions

View file

@ -157,6 +157,19 @@ class AccessSubject:
def has_capability(self, capability: str) -> bool:
return capability in self.capabilities
@property
def primary_identity_provider(self) -> str | None:
identities = self.claims.get("identities")
if not isinstance(identities, list):
return None
for entry in identities:
if not isinstance(entry, dict):
continue
provider = entry.get("provider")
if isinstance(provider, str) and provider.strip():
return provider.strip().lower()
return None
def owns_job(self, *, account_id: str | None, session_id: str | None) -> bool:
if account_id is not None and self.account_id is not None and account_id == self.account_id:
return True

View file

@ -249,6 +249,9 @@ class GuiJob:
account_id: str | None = None
session_id: str | None = None
access_revision: int | None = None
display_name: str | None = None
auth_mode: str | None = None
identity_provider: str | None = None
created_at: float = field(default_factory=time.time)
last_heartbeat_at: float = field(default_factory=time.time)
running_at: float | None = None
@ -281,18 +284,33 @@ class GuiJob:
self.events.append(record)
self._condition.notify_all()
def actor_log_json(self) -> str:
payload: dict[str, str] = {}
if self.account_id:
payload["accountId"] = self.account_id
if self.session_id:
payload["sessionId"] = self.session_id
if self.display_name:
payload["displayName"] = self.display_name
if self.auth_mode:
payload["authMode"] = self.auth_mode
if self.identity_provider:
payload["provider"] = self.identity_provider
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
def mark_running(self) -> None:
self.status = "running"
self.running_at = time.time()
self.touch_heartbeat()
LOGGER.info(
"repixelizer_job_started job_id=%s wait_seconds=%.3f steps=%s target_size=%s target_width=%s target_height=%s",
"repixelizer_job_started job_id=%s wait_seconds=%.3f steps=%s target_size=%s target_width=%s target_height=%s actor=%s",
self.job_id,
self.running_at - self.created_at,
self.options.get("steps"),
self.options.get("target_size"),
self.options.get("target_width"),
self.options.get("target_height"),
self.actor_log_json(),
)
self.publish("job_state", {"status": self.status})
@ -301,11 +319,12 @@ class GuiJob:
finished_at = time.time()
run_started_at = self.running_at or finished_at
LOGGER.info(
"repixelizer_job_completed job_id=%s wait_seconds=%.3f run_seconds=%.3f total_seconds=%.3f",
"repixelizer_job_completed job_id=%s wait_seconds=%.3f run_seconds=%.3f total_seconds=%.3f actor=%s",
self.job_id,
run_started_at - self.created_at,
finished_at - run_started_at,
finished_at - self.created_at,
self.actor_log_json(),
)
self.publish("job_state", {"status": self.status})
@ -315,12 +334,13 @@ class GuiJob:
finished_at = time.time()
run_started_at = self.running_at or finished_at
LOGGER.warning(
"repixelizer_job_failed job_id=%s wait_seconds=%.3f run_seconds=%.3f total_seconds=%.3f message=%r",
"repixelizer_job_failed job_id=%s wait_seconds=%.3f run_seconds=%.3f total_seconds=%.3f message=%r actor=%s",
self.job_id,
run_started_at - self.created_at,
finished_at - run_started_at,
finished_at - self.created_at,
message,
self.actor_log_json(),
)
self.publish("job_failed", {"status": self.status, "message": message})
@ -331,12 +351,13 @@ class GuiJob:
finished_at = time.time()
run_started_at = self.running_at or finished_at
LOGGER.info(
"repixelizer_job_canceled job_id=%s wait_seconds=%.3f run_seconds=%.3f total_seconds=%.3f message=%r",
"repixelizer_job_canceled job_id=%s wait_seconds=%.3f run_seconds=%.3f total_seconds=%.3f message=%r actor=%s",
self.job_id,
run_started_at - self.created_at,
finished_at - run_started_at,
finished_at - self.created_at,
message,
self.actor_log_json(),
)
self.publish("job_canceled", {"status": self.status, "message": message})
self.publish("job_state", {"status": self.status, "message": message})
@ -347,7 +368,13 @@ class GuiJob:
def request_cancel(self, reason: str) -> None:
self.cancel_reason = reason
self._cancel_event.set()
LOGGER.info("repixelizer_job_cancel_requested job_id=%s status=%s reason=%r", self.job_id, self.status, reason)
LOGGER.info(
"repixelizer_job_cancel_requested job_id=%s status=%s reason=%r actor=%s",
self.job_id,
self.status,
reason,
self.actor_log_json(),
)
@property
def cancellation_message(self) -> str:
@ -690,6 +717,9 @@ class GuiJobManager:
account_id: str | None = None,
session_id: str | None = None,
access_revision: int | None = None,
display_name: str | None = None,
auth_mode: str | None = None,
identity_provider: str | None = None,
) -> GuiJob:
self.start()
suffix = Path(filename or "input.png").suffix or ".png"
@ -701,6 +731,9 @@ class GuiJobManager:
account_id=account_id,
session_id=session_id,
access_revision=access_revision,
display_name=display_name,
auth_mode=auth_mode,
identity_provider=identity_provider,
phase_field_preview_stride=self.config.phase_field_preview_stride,
)
job.spool_path.write_bytes(raw)
@ -708,10 +741,11 @@ class GuiJobManager:
if len(self._queued_job_ids) >= self.config.queue_capacity:
self._cleanup_spool_file(job)
LOGGER.warning(
"repixelizer_queue_full waiting_count=%d queue_capacity=%d upload_bytes=%d",
"repixelizer_queue_full waiting_count=%d queue_capacity=%d upload_bytes=%d actor=%s",
len(self._queued_job_ids),
self.config.queue_capacity,
len(raw),
job.actor_log_json(),
)
raise QueueFullError(f"Queue is full. {self.config.queue_capacity} waiting jobs are already lined up.")
self.jobs[job.job_id] = job
@ -719,13 +753,14 @@ class GuiJobManager:
job.publish("job_state", {"status": job.status})
self._publish_queue_state_locked()
LOGGER.info(
"repixelizer_job_queued job_id=%s queue_position=%s queue_depth=%d waiting_count=%d queue_capacity=%d upload_bytes=%d",
"repixelizer_job_queued job_id=%s queue_position=%s queue_depth=%d waiting_count=%d queue_capacity=%d upload_bytes=%d actor=%s",
job.job_id,
self._queue_position_locked(job.job_id),
self._queue_depth_locked(),
len(self._queued_job_ids),
self.config.queue_capacity,
len(raw),
job.actor_log_json(),
)
self._condition.notify_all()
return job
@ -1201,6 +1236,9 @@ def create_app():
account_id=subject.account_id,
session_id=subject.session_id,
access_revision=subject.access_revision,
display_name=subject.display_name,
auth_mode=subject.auth_mode,
identity_provider=subject.primary_identity_provider,
)
except AccessDenied as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc

View file

@ -5,6 +5,7 @@ import io
import asyncio
import base64
import json
import logging
import os
import threading
import time
@ -1048,6 +1049,56 @@ def test_gui_job_routes_enforce_bound_subject_ownership(monkeypatch, tmp_path: P
assert "different local account or session" in str(excinfo.value.detail)
def test_gui_job_logs_authenticated_actor_metadata(monkeypatch, tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
from repixelizer.gui import create_app
monkeypatch.setenv("REPIXELIZER_HOSTED_DEMO", "1")
monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool"))
monkeypatch.setenv("GC_ACCESS_MODE", "trusted-header")
monkeypatch.setenv("GC_ACCESS_REQUIRED", "1")
app = create_app()
create_job = _route_endpoint(app, "/api/jobs", "POST")
subject = AccessSubject(
account_id="acct-1",
session_id="sess-1",
access_revision=4,
display_name="Meta",
capabilities=frozenset({"app_access", "queue_submit"}),
auth_mode="heimdall",
claims={
"identities": [
{
"provider": "discord",
"providerUserId": "123456789",
}
]
},
)
caplog.set_level(logging.INFO, logger="repixelizer.gui")
with bind_access_subject(subject):
created = asyncio.run(
create_job(
image=UploadFile(filename="tiny.png", file=io.BytesIO(_png_bytes())),
target_size=None,
target_width=None,
target_height=None,
steps=None,
seed=7,
device="auto",
strip_background=False,
skip_candidate_rerank=False,
)
)
assert created.status_code == 200
queued_messages = [record.getMessage() for record in caplog.records if "repixelizer_job_queued" in record.getMessage()]
assert queued_messages
assert 'actor={"accountId":"acct-1","authMode":"heimdall","displayName":"Meta","provider":"discord","sessionId":"sess-1"}' in queued_messages[0]
def test_gui_queue_panel_defaults_off_for_local_runs_and_can_be_forced(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app