Log Repixelizer job actors for ops summaries
This commit is contained in:
parent
e0025b4cbf
commit
da25c88eb8
3 changed files with 109 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue