Skip to content

Commit 04a455c

Browse files
authored
Integrate loguru (#4991)
1 parent 4494101 commit 04a455c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+850
-272
lines changed

application/backend/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ data/
1717
**/openapi.json
1818
.idea
1919
.DS_Store
20+
logs/

application/backend/app/api/dependencies.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ def get_data_dir(request: Request) -> Path:
133133
return request.app.state.settings.data_dir
134134

135135

136+
def get_job_dir(request: Request) -> Path:
137+
"""Provides the job log directory path from settings."""
138+
return request.app.state.settings.job_dir
139+
140+
136141
def get_event_bus(request: Request) -> EventBus:
137142
"""Provides an EventBus instance."""
138143
return request.app.state.event_bus

application/backend/app/api/routers/jobs.py

Lines changed: 102 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
import asyncio
55
from collections.abc import AsyncGenerator
6+
from pathlib import Path
67
from typing import Annotated
8+
from uuid import UUID
79

8-
from fastapi import APIRouter, Body, Depends, HTTPException, Request, status
9-
from starlette.responses import StreamingResponse
10+
import aiofiles
11+
from fastapi import APIRouter, Body, Depends, HTTPException, status
12+
from sse_starlette.sse import EventSourceResponse, ServerSentEvent
1013

11-
from app.api.dependencies import get_job_queue, get_project_service
14+
from app.api.dependencies import get_data_dir, get_job_dir, get_job_queue, get_project_service
1215
from app.api.validators import JobID
1316
from app.core.jobs.control_plane import CancellationResult, JobQueue
1417
from app.core.jobs.models import JobStatus
@@ -33,6 +36,8 @@
3336
async def submit_job(
3437
job_request: Annotated[JobRequest, Body()],
3538
job_queue: Annotated[JobQueue, Depends(get_job_queue)],
39+
job_dir: Annotated[Path, Depends(get_job_dir)],
40+
data_dir: Annotated[Path, Depends(get_data_dir)],
3641
project_service: Annotated[ProjectService, Depends(get_project_service)],
3742
) -> JobView:
3843
"""
@@ -41,6 +46,8 @@ async def submit_job(
4146
Args:
4247
job_request (JobRequest): The Job request payload.
4348
job_queue (JobQueue): The job queue instance responsible for managing job submissions and tracking job statuses.
49+
job_dir (Path): The directory where job log files are stored.
50+
data_dir (Path): The base directory for project data storage.
4451
project_service (ProjectService): The service to interact with project data.
4552
4653
Returns:
@@ -53,6 +60,8 @@ async def submit_job(
5360
case JobType.TRAIN:
5461
job = TrainingJob(
5562
project_id=job_request.project_id,
63+
log_dir=job_dir,
64+
data_dir=data_dir,
5665
params=TrainingParams(
5766
model_architecture_id=job_request.parameters.model_architecture_id,
5867
parent_model_revision_id=job_request.parameters.parent_model_revision_id,
@@ -163,49 +172,106 @@ async def cancel_job(job_id: JobID, job_queue: Annotated[JobQueue, Depends(get_j
163172

164173
@router.get("/{job_id}/status")
165174
async def stream_job_status(
166-
job_id: JobID, request: Request, job_queue: Annotated[JobQueue, Depends(get_job_queue)]
167-
) -> StreamingResponse:
175+
job_id: JobID, job_queue: Annotated[JobQueue, Depends(get_job_queue)]
176+
) -> EventSourceResponse:
168177
"""
169178
Stream real-time status updates for a specific job.
170179
171180
This endpoint streams job status updates using Server-Sent Events (SSE).
172-
It sends periodic updates until the client disconnects or the job reaches
173-
terminal state.
181+
It sends periodic updates until the job reaches terminal state.
174182
175183
Args:
176184
job_id (JobID): The unique identifier of the job.
177-
request (Request): The HTTP request object to monitor client connection status.
178-
job_queue (JobQueue): The job queue instance responsible for managing job submissions and tracking job statuses.
185+
job_queue (JobQueue): The job queue instance responsible for tracking job statuses.
179186
180187
Returns:
181-
StreamingResponse: A streaming response with job status updates.
188+
EventSourceResponse: A streaming response with job status updates.
182189
"""
183190
if not job_queue.get(job_id):
184191
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")
185192

186-
async def gen_job_updates() -> AsyncGenerator[str]:
187-
"""Generate job status updates."""
188-
last = None
189-
while True:
190-
if await request.is_disconnected():
191-
break
192-
j = job_queue.get(job_id)
193-
if not j:
194-
break
195-
snap = JobView.of(j).model_dump_json()
196-
if snap != last:
197-
yield f"{snap}\n"
198-
last = snap
199-
if j.status >= JobStatus.DONE:
200-
break
201-
await asyncio.sleep(0.1)
202-
203-
return StreamingResponse(
204-
gen_job_updates(),
205-
media_type="text/event-stream",
206-
headers={
207-
"Content-Type": "text/event-stream",
208-
"Connection": "keep-alive",
209-
"Cache-Control": "no-cache",
210-
},
211-
)
193+
return EventSourceResponse(__gen_job_updates(job_id, job_queue))
194+
195+
196+
@router.get("/{job_id}/logs")
197+
async def stream_job_logs(
198+
job_id: JobID,
199+
job_dir: Annotated[Path, Depends(get_job_dir)],
200+
job_queue: Annotated[JobQueue, Depends(get_job_queue)],
201+
) -> EventSourceResponse:
202+
"""
203+
Stream real-time log output for a specific job.
204+
205+
This endpoint streams job logs using Server-Sent Events (SSE). It reads
206+
the job's log file and yields new lines as they are written, allowing clients
207+
to follow the job's progress in real-time. The stream continues until the
208+
client disconnects or an error occurs.
209+
210+
Args:
211+
job_id (JobID): The unique identifier of the job.
212+
job_dir (Path): The directory where job log files are stored.
213+
job_queue (JobQueue): The job queue instance for tracking job statuses.
214+
215+
Returns:
216+
EventSourceResponse: A streaming response with log entries sent as SSE events.
217+
218+
Raises:
219+
HTTPException: If the job is not found (404), the log file doesn't exist (404),
220+
or the job has already completed (409).
221+
"""
222+
job = job_queue.get(job_id)
223+
if not job:
224+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")
225+
226+
if job.status >= JobStatus.DONE:
227+
raise HTTPException(
228+
status_code=status.HTTP_409_CONFLICT,
229+
detail="Job has already completed; logs are no longer available for streaming",
230+
)
231+
232+
log_path = job_dir / job.log_file
233+
234+
if not log_path.exists():
235+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Log file not found")
236+
237+
return EventSourceResponse(__gen_log_stream(job_id, log_path, job_queue))
238+
239+
240+
async def __gen_job_updates(job_id: UUID, job_queue: JobQueue) -> AsyncGenerator[ServerSentEvent]:
241+
"""Generate job status updates."""
242+
last = None
243+
while True:
244+
j = job_queue.get(job_id)
245+
if not j:
246+
break
247+
snap = JobView.of(j).model_dump_json()
248+
if snap != last:
249+
yield ServerSentEvent(data=snap)
250+
last = snap
251+
if j.status >= JobStatus.DONE:
252+
break
253+
await asyncio.sleep(0.1)
254+
255+
256+
async def __gen_log_stream(job_id: UUID, log_path: Path, job_queue: JobQueue) -> AsyncGenerator[ServerSentEvent]:
257+
"""Asynchronously follow a log file and yield new lines as SSE events."""
258+
try:
259+
async with aiofiles.open(log_path) as f:
260+
async for line in f:
261+
yield ServerSentEvent(data=line.rstrip("\n"))
262+
263+
while True:
264+
j = job_queue.get(job_id)
265+
if not j:
266+
break
267+
line = await f.readline()
268+
if not line:
269+
await asyncio.sleep(0.3)
270+
continue
271+
yield ServerSentEvent(data=line.rstrip("\n"))
272+
if j.status >= JobStatus.DONE:
273+
break
274+
except asyncio.CancelledError:
275+
raise
276+
except Exception as e:
277+
yield ServerSentEvent(data=f"Error reading log file: {e}")

application/backend/app/api/routers/model_architectures.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33

44
"""Endpoints for managing model architectures"""
55

6-
import logging
7-
86
from fastapi import APIRouter, status
97

108
from app.schemas import ModelArchitectures
119
from app.schemas.model_architecture import ModelArchitecture
1210
from app.supported_models import SupportedModels
1311

14-
logger = logging.getLogger(__name__)
1512
router = APIRouter(prefix="/api/model_architectures", tags=["Model Architectures"])
1613

1714

application/backend/app/api/routers/pipelines.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
"""Endpoints for managing pipelines"""
55

6-
import logging
76
from typing import Annotated
87
from uuid import UUID
98

@@ -17,7 +16,6 @@
1716
from app.schemas.pipeline import DataCollectionPolicyAdapter, PipelineStatus, PipelineView
1817
from app.services import PipelineMetricsService, PipelineService, ResourceNotFoundError
1918

20-
logger = logging.getLogger(__name__)
2119
router = APIRouter(prefix="/api/projects/{project_id}/pipeline", tags=["Pipelines"])
2220

2321
UPDATE_PIPELINE_BODY_DESCRIPTION = """

application/backend/app/api/routers/projects.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
"""Endpoints for managing projects"""
55

6-
import logging
76
from typing import Annotated
87
from uuid import UUID
98

@@ -25,7 +24,6 @@
2524
from app.services.label_service import DuplicateLabelsError
2625
from app.supported_models.hyperparameters import Hyperparameters
2726

28-
logger = logging.getLogger(__name__)
2927
router = APIRouter(prefix="/api/projects", tags=["Projects"])
3028

3129
CREATE_PROJECT_BODY_DESCRIPTION = """

application/backend/app/api/routers/sinks.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
"""Endpoints for managing pipeline sinks"""
55

6-
import logging
76
from typing import Annotated
87

98
import yaml
@@ -24,7 +23,6 @@
2423
SinkService,
2524
)
2625

27-
logger = logging.getLogger(__name__)
2826
router = APIRouter(prefix="/api/sinks", tags=["Sinks"])
2927

3028
CREATE_SINK_BODY_DESCRIPTION = """

application/backend/app/api/routers/sources.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
"""Endpoints for managing pipeline sources"""
55

6-
import logging
76
from typing import Annotated
87

98
import yaml
@@ -24,7 +23,6 @@
2423
SourceUpdateService,
2524
)
2625

27-
logger = logging.getLogger(__name__)
2826
router = APIRouter(prefix="/api/sources", tags=["Sources"])
2927

3028
CREATE_SOURCE_BODY_DESCRIPTION = """

application/backend/app/api/routers/system.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33

44
"""System API Endpoints"""
55

6-
import logging
76
from typing import Annotated
87

98
from fastapi import APIRouter, Depends
109

1110
from app.api.dependencies import get_system_service
1211
from app.services import SystemService
1312

14-
logger = logging.getLogger(__name__)
1513
router = APIRouter(prefix="/api")
1614

1715

application/backend/app/api/routers/webrtc.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@
33

44
"""WebRTC API Endpoints"""
55

6-
import logging
76
from typing import Annotated
87

98
from fastapi import APIRouter, Depends, status
109
from fastapi.exceptions import HTTPException
10+
from loguru import logger
1111

1212
from app.api.dependencies import get_webrtc_manager as get_webrtc
1313
from app.schemas.webrtc import Answer, InputData, Offer
1414
from app.webrtc.manager import WebRTCManager
1515

16-
logger = logging.getLogger(__name__)
1716
router = APIRouter(prefix="/api/webrtc", tags=["WebRTC"])
1817

1918

@@ -30,7 +29,7 @@ async def create_webrtc_offer(offer: Offer, webrtc_manager: Annotated[WebRTCMana
3029
try:
3130
return await webrtc_manager.handle_offer(offer)
3231
except Exception as e:
33-
logger.error("Error processing WebRTC offer: %s", e)
32+
logger.exception("Error processing WebRTC offer")
3433
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
3534

3635

0 commit comments

Comments
 (0)