pulse_executions table with logs field. This document implements the actual log capture mechanism.This document outlines the 9-phase implementation plan for universal log capture across all QNTX jobs.
Enable comprehensive log capture for all job executions with:
File: internal/database/migrations/050_create_task_logs_table.sql
What: Create task_logs table with columns:
job_id - Links to async_ix_jobsstage - Execution phase ("fetch_jd", "score_candidates")task_id - Work unit ID (candidate_id, etc.)timestamp, level, message, metadataWhy: Foundation for all log storage. Indexed for fast retrieval by job, task, or time range.
Status: Migration created
File: internal/ats/ix/log_capturing_emitter.go
What: Wrapper around ProgressEmitter that:
EmitInfo(), EmitStage(), EmitError() callstask_logs tableWhy: Non-invasive log capture without changing handler code. Leverages existing emitter abstraction.
Status: Implementation created
Files: internal/role/async_handlers.go
What: Wrap emitter creation in handlers:
// Before:
emitter := async.NewJobProgressEmitter(job, queue, h.streamBroadcaster)
// After:
baseEmitter := async.NewJobProgressEmitter(job, queue, h.streamBroadcaster)
emitter := ix.NewLogCapturingEmitter(baseEmitter, h.db, job.ID)
Why: Handlers are where emitters are created. Wrapping here captures ALL handler execution.
Implementation:
JDIngestionHandler.runFullIngestion() at lines 125-126CandidateScoringHandler.Execute() uses scorer's internal emitter (different mechanism - not wrapped)VacanciesScraperHandler.Execute() doesn't create emitters (only enqueues jobs)Status: ✓ COMPLETED
File: internal/pulse/schedule/ticker.go
What: Similar wrapping for scheduled job executions:
// In executeScheduledJob() around line 185:
// Wrap ATS parsing execution with log capture
Why: Scheduled jobs (Pulse executions) also need log capture. Currently pulse_executions.logs field exists but unused.
Decision needed: Should ticker logs go to:
task_logs table (unified with async jobs)pulse_executions.logs field (separate for scheduled jobs)Recommendation: Option A - use same task_logs table for all logs. Add execution_id column to link Pulse execution logs.
Status: ⏭️ DEFERRED
Rationale: Async job logging (Phase 3) provides sufficient coverage for current needs. Ticker integration can be added later if needed. This keeps the initial implementation focused and reduces complexity.
File: internal/server/pulse_handlers.go (new handlers)
What: Add REST endpoints:
GET /jobs/:job_id/logs - Get all logs for a jobGET /jobs/:job_id/logs?stage=X - Filter by stageGET /jobs/:job_id/logs?task_id=X - Filter by taskGET /jobs/:job_id/logs?level=error - Filter by levelGET /executions/:id/logs - Get logs for Pulse execution (may already exist)Response format:
{
"job_id": "JB_abc123",
"total_count": 150,
"logs": [
{
"id": 1,
"stage": "fetch_jd",
"task_id": null,
"timestamp": "2025-01-15T10:30:00Z",
"level": "info",
"message": "Fetching job description from URL",
"metadata": {"url": "https://..."}
}
]
}
Status: PENDING
Files:
web/ts/pulse/execution-api.ts (update getExecutionLogs)web/ts/pulse/job-detail-panel.ts (already has UI)What: Update getExecutionLogs() to call new /jobs/:job_id/logs endpoint instead of /executions/:id/logs.
Current state:
Enhancement opportunities:
Status: PARTIALLY DONE (UI ready, API needs update)
File: internal/ats/ix/log_capturing_emitter_test.go
What: Test scenarios implemented:
TestLogCapturingEmitter_EmitInfo - Verifies logs written to tableTestLogCapturingEmitter_EmitStage - Verifies stage context updatesTestLogCapturingEmitter_EmitCandidateMatch - Verifies task_id populated for candidate scoringTestLogCapturingEmitter_MultipleStages - Verifies stage transitions tracked correctlyTestLogCapturingEmitter_ErrorHandling - Verifies DB errors don't break job executionTestLogCapturingEmitter_Timestamps - Verifies RFC3339 timestampsTest Results: All 6 tests passing
Status: ✓ COMPLETED
What: Manual testing flow:
task_logs table has entriescurl http://localhost:8820/jobs/JB_xxx/logsSuccess criteria:
Status: PENDING
Files:
docs/development/log-capture-architecture.md (new - comprehensive guide)docs/development/pulse-execution-history.md (add log capture details)What:
Status: PARTIALLY DONE (this document is start)
Decision: Separate table for log entries
Rationale:
Alternative considered: async_ix_jobs.logs TEXT field
Decision: LogCapturingEmitter wraps existing emitters
Rationale:
Alternative considered: Modify JobProgressEmitter directly
Decision: Stage and task_id are denormalized fields in task_logs
Rationale:
job_stages or stage_tasks tables (yet)Future evolution: Can formalize later with:
job_stages table (if need stage-level status tracking)stage_tasks table (if need individual task pause/resume)Decision: Store full logs without size limits
Rationale:
Risk mitigation:
| Phase | Status | Files Modified | Tests Added |
|---|---|---|---|
| 1. Schema | ✅ DONE | migrations/050_create_task_logs_table.sql | - |
| 2. Emitter | ✅ DONE | internal/ats/ix/log_capturing_emitter.go | - |
| 3. Async Workers | ✅ DONE | internal/role/async_handlers.go:125-126 | - |
| 4. Ticker | ⏭️ DEFERRED | (deferred - async jobs sufficient) | - |
| 5. API | ✅ DONE | internal/server/pulse_handlers.go | 2 endpoints |
| 6. Frontend | 📋 QNTX #30 | execution-api.ts, job-detail-panel.ts | - |
| 7. Tests | ✅ DONE | internal/ats/ix/log_capturing_emitter_test.go | 6 tests |
| 8. E2E Validation | ✅ DONE | Manual async job execution | Verified |
| 9. Documentation | ✅ DONE | This file + cross-references | - |
Implementation Summary:
✅ Phase 1-3, 5, 7-8 Complete - Core log capture system is fully functional
task_logs table created with all indexesFiles Created/Modified:
internal/database/migrations/050_create_task_logs_table.sql - Database schema with indexesinternal/ats/ix/log_capturing_emitter.go - Core implementation (158 lines)internal/ats/ix/log_capturing_emitter_test.go - Test suite (334 lines, 6 tests)internal/role/async_handlers.go - Integration point (lines 125-126)internal/server/pulse_handlers.go - API endpoints (lines 58-88 types, 365-474 handlers)internal/server/server.go - Route registration (line 292)API Endpoints Implemented:
GET /api/pulse/jobs/:job_id/stages
GET /api/pulse/tasks/:task_id/logs
Validated: December 2024
Test Scenario: Manual async job execution (JB_MANUAL_E2E_LOG_TEST_123)
Results:
task_logs tableSample Captured Logs:
stage: read_jd | level: info | Reading job description from file:///tmp/test-jd.txt
stage: extract_requirements | level: info | Extracting with llama3.2:3b (local)...
stage: extract | level: error | file not found: file:/tmp/test-jd.txt
API Response Example:
{
"job_id": "JB_MANUAL_E2E_LOG_TEST_123",
"stages": [
{"stage": "read_jd", "tasks": [{"task_id": "read_jd", "log_count": 1}]},
{"stage": "extract_requirements", "tasks": [{"task_id": "extract_requirements", "log_count": 1}]},
{"stage": "extract", "tasks": [{"task_id": "extract", "log_count": 1}]}
]
}
Key Findings:
Phase 6: Frontend Integration → Issue #30
The frontend UI is already built (execution card expansion, log viewer) but needs to connect to new API.
Status: Tracked in teranos/QNTX#30 - Pulse Frontend - Fix Integration and Complete Outstanding Features
Summary:
web/ts/pulse/execution-api.ts - Add getJobStages() and getTaskLogs() functionsweb/ts/pulse/job-detail-panel.ts - Render stage → task hierarchy, display logs on task clickDeferred Items:
Based on code analysis in internal/role/executor.go, these are the execution stages currently used:
fetch_jd - Fetching job description from URL (HTTP request)read_jd - Reading job description from file (file I/O)extract_requirements - LLM extraction of requirements from JD textgenerate_attestations - Creating attestations from parsed datapersist_data - Saving Role/JD/Attestations to databasepersist_complete - Database save finished successfullyscore_candidates - Scoring applicable candidates against JD(No explicit stages - single-phase execution)
(To be determined - check internal/role/vacancies_handler.go)
Note: Stages are currently just string labels passed to EmitStage(). They are NOT formal entities in the database (yet). This plan keeps them as strings for now.