mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Add static job logs view and enhance logging functionality with improved UI and error handling
This commit is contained in:
+19
-1
@@ -8,6 +8,7 @@ import re
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, cast
|
||||
|
||||
@@ -2311,7 +2312,24 @@ def job_logs_partial(job_id: str) -> str:
|
||||
job = _service().get_job(job_id)
|
||||
if not job:
|
||||
abort(404)
|
||||
return render_template("partials/logs.html", job=job)
|
||||
return render_template("partials/logs.html", job=job, static_view=False)
|
||||
|
||||
|
||||
@web_bp.get("/jobs/<job_id>/logs/static")
|
||||
def job_logs_static(job_id: str) -> str:
|
||||
job = _service().get_job(job_id)
|
||||
if not job:
|
||||
abort(404)
|
||||
log_lines = [
|
||||
f"{datetime.fromtimestamp(entry.timestamp).strftime('%Y-%m-%d %H:%M:%S')} [{entry.level.upper()}] {entry.message}"
|
||||
for entry in job.logs
|
||||
]
|
||||
return render_template(
|
||||
"job_logs_static.html",
|
||||
job=job,
|
||||
log_text="\n".join(log_lines),
|
||||
static_view=True,
|
||||
)
|
||||
|
||||
|
||||
@api_bp.get("/jobs/<job_id>")
|
||||
|
||||
+41
-1
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
@@ -24,6 +26,35 @@ def _create_set_event() -> threading.Event:
|
||||
STATE_VERSION = 5
|
||||
|
||||
|
||||
_JOB_LOGGER = logging.getLogger("abogen.jobs")
|
||||
if not _JOB_LOGGER.handlers:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S"))
|
||||
_JOB_LOGGER.addHandler(handler)
|
||||
_JOB_LOGGER.propagate = False
|
||||
_JOB_LOGGER.setLevel(logging.DEBUG)
|
||||
|
||||
_JOB_LEVEL_MAP: Dict[str, int] = {
|
||||
"critical": logging.CRITICAL,
|
||||
"error": logging.ERROR,
|
||||
"warning": logging.WARNING,
|
||||
"info": logging.INFO,
|
||||
"success": logging.INFO,
|
||||
"debug": logging.DEBUG,
|
||||
"trace": logging.DEBUG,
|
||||
}
|
||||
|
||||
|
||||
def _emit_job_log(job_id: str, level: str, message: str) -> None:
|
||||
normalized = (level or "info").lower()
|
||||
log_level = _JOB_LEVEL_MAP.get(normalized, logging.INFO)
|
||||
try:
|
||||
_JOB_LOGGER.log(log_level, "[job %s] %s", job_id, message)
|
||||
except Exception:
|
||||
# Logging failures should never disrupt job processing.
|
||||
pass
|
||||
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
@@ -105,7 +136,9 @@ class Job:
|
||||
applied_speaker_config: Optional[str] = None
|
||||
|
||||
def add_log(self, message: str, level: str = "info") -> None:
|
||||
self.logs.append(JobLog(timestamp=time.time(), message=message, level=level))
|
||||
entry = JobLog(timestamp=time.time(), message=message, level=level)
|
||||
self.logs.append(entry)
|
||||
_emit_job_log(self.id, level, message)
|
||||
|
||||
def as_dict(self) -> Dict[str, object]:
|
||||
return {
|
||||
@@ -476,6 +509,7 @@ class ConversionService:
|
||||
new_job.applied_speaker_config = job.applied_speaker_config
|
||||
new_job.add_log(f"Retry created from job {job.id}", level="info")
|
||||
job.add_log(f"Retry scheduled as job {new_job.id}", level="info")
|
||||
self._remove_job_locked(job_id)
|
||||
return new_job
|
||||
|
||||
def delete(self, job_id: str) -> bool:
|
||||
@@ -608,6 +642,12 @@ class ConversionService:
|
||||
job.queue_position = index
|
||||
self._persist_state()
|
||||
|
||||
def _remove_job_locked(self, job_id: str) -> None:
|
||||
self._jobs.pop(job_id, None)
|
||||
if job_id in self._queue:
|
||||
self._queue.remove(job_id)
|
||||
self._update_queue_positions_locked()
|
||||
|
||||
# Persistence ------------------------------------------------------
|
||||
def _serialize_job(self, job: Job) -> Dict[str, Any]:
|
||||
result_audio = str(job.result.audio_path) if job.result.audio_path else None
|
||||
|
||||
@@ -478,6 +478,23 @@ body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card__title-row .card__title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card__title-row .button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
@@ -539,6 +556,36 @@ body {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.log-copy {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.log-copy > summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.log-copy__textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 240px;
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.32);
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
color: inherit;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.log-copy__textarea:focus {
|
||||
outline: 2px solid rgba(56, 189, 248, 0.55);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Logs · {{ job.original_filename }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="card logs-static">
|
||||
{% include "partials/logs.html" %}
|
||||
{% if job.logs %}
|
||||
<details class="log-copy">
|
||||
<summary>Copy raw log output</summary>
|
||||
<textarea class="log-copy__textarea" readonly>{{- log_text -}}</textarea>
|
||||
</details>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,12 @@
|
||||
<div class="card__title">Live log</div>
|
||||
{% set is_static = static_view if static_view is defined and static_view else False %}
|
||||
<div class="card__title-row">
|
||||
<div class="card__title">Live log</div>
|
||||
{% if not is_static %}
|
||||
<a class="button button--ghost button--small" href="{{ url_for('web.job_logs_static', job_id=job.id) }}" target="_blank" rel="noopener">Open static view</a>
|
||||
{% else %}
|
||||
<a class="button button--ghost button--small" href="{{ url_for('web.job_detail', job_id=job.id) }}">Back to job</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if job.logs %}
|
||||
<ul class="log-list">
|
||||
{% for entry in job.logs|reverse %}
|
||||
|
||||
Reference in New Issue
Block a user