Skip to content

Commit 503787b

Browse files
feat(job-orchestration): Make stderr logs visible in container logs
- Dump subprocess stderr logs to container stderr after process completion in compression_task, query/utils, and reducer, so logs appear in `kubectl logs` / `docker compose logs` even on ephemeral storage - Add container log handler (StreamHandler on fd 2) for Celery workers via after_setup_logger signal, bypassing Celery's -f redirect
1 parent b110f22 commit 503787b

6 files changed

Lines changed: 58 additions & 3 deletions

File tree

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
from celery import Celery
1+
import logging
2+
3+
from celery import Celery, signals
24

35
from job_orchestration.executor.compress import celeryconfig
6+
from job_orchestration.executor.utils import add_container_log_handler
47

58
app = Celery("compress")
69
app.config_from_object(celeryconfig)
710

11+
12+
@signals.after_setup_logger.connect
13+
def _on_after_setup_logger(logger: logging.Logger, **kwargs) -> None:
14+
add_container_log_handler(logger)
15+
16+
817
if "__main__" == __name__:
918
app.start()

components/job-orchestration/job_orchestration/executor/compress/compression_task.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pathlib
55
import shutil
66
import subprocess
7+
import sys
78
from contextlib import closing
89
from typing import Any
910

@@ -559,6 +560,11 @@ def cleanup_temporary_files():
559560
finally:
560561
cleanup_temporary_files()
561562
stderr_log_file.close()
563+
# Dump the stderr log to sys.stderr so it's visible in container logs
564+
if stderr_log_path.stat().st_size > 0:
565+
print(f"--- Contents of {stderr_log_path.name} ---", file=sys.stderr)
566+
sys.stderr.write(stderr_log_path.read_text())
567+
print(f"--- End of {stderr_log_path.name} ---", file=sys.stderr)
562568

563569
worker_output = {
564570
"total_uncompressed_size": total_uncompressed_size,
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
from celery import Celery
1+
import logging
2+
3+
from celery import Celery, signals
24

35
from job_orchestration.executor.query import celeryconfig
6+
from job_orchestration.executor.utils import add_container_log_handler
47

58
app = Celery("query")
69
app.config_from_object(celeryconfig)
710

11+
12+
@signals.after_setup_logger.connect
13+
def _on_after_setup_logger(logger: logging.Logger, **kwargs) -> None:
14+
add_container_log_handler(logger)
15+
16+
817
if "__main__" == __name__:
918
app.start()

components/job-orchestration/job_orchestration/executor/query/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ def sigterm_handler(_signo, _stack_frame):
8888
# stdout/stderr.
8989
stdout_data, _ = task_proc.communicate()
9090
return_code = task_proc.returncode
91+
92+
# Dump the stderr log to sys.stderr so it's visible in container logs
93+
if clo_log_path.stat().st_size > 0:
94+
print(f"--- Contents of {clo_log_path.name} ---", file=sys.stderr)
95+
sys.stderr.write(clo_log_path.read_text())
96+
print(f"--- End of {clo_log_path.name} ---", file=sys.stderr)
97+
9198
if 0 != return_code:
9299
task_status = QueryTaskStatus.FAILED
93100
logger.error(

components/job-orchestration/job_orchestration/executor/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
"""Utilities shared by job-orchestration executors."""
22

3+
import logging
34
import os
45
from logging import Logger
56
from pathlib import Path
67

8+
from celery.app.defaults import DEFAULT_PROCESS_LOG_FMT
79
from clp_py_utils.clp_config import ClpConfig, WorkerConfig
810
from clp_py_utils.core import read_yaml_config_file
911

1012

13+
def add_container_log_handler(logger: logging.Logger) -> None:
14+
"""Adds a StreamHandler that writes to the container's real stderr (fd 2).
15+
16+
In Celery workers, ``-f`` redirects ``sys.stdout`` and ``sys.stderr`` to
17+
the log file, so regular ``StreamHandler(sys.stderr)`` would write to the
18+
file instead of the container's stderr. This handler writes directly to
19+
file descriptor 2, ensuring output reaches ``docker compose logs`` /
20+
``kubectl logs``.
21+
"""
22+
# dup(2) so the handler owns its own fd and doesn't close the original
23+
stderr_stream = os.fdopen(os.dup(2), "w")
24+
handler = logging.StreamHandler(stderr_stream)
25+
handler.setFormatter(logging.Formatter(DEFAULT_PROCESS_LOG_FMT))
26+
logger.addHandler(handler)
27+
28+
1129
def load_clp_config_from_config_path_env_var() -> ClpConfig:
1230
"""
1331
Loads the CLP config from `CLP_CONFIG_PATH`.

components/job-orchestration/job_orchestration/reducer/reducer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,14 @@ def main(argv: list[str]) -> int:
9393
for i, reducer in enumerate(reducers):
9494
reducer.communicate()
9595
logger.info(f"reducer-{i} exited with returncode={reducer.returncode}")
96-
for reducer_log_file in reducer_log_files:
96+
for i, reducer_log_file in enumerate(reducer_log_files):
9797
reducer_log_file.close()
98+
# Dump the reducer log to sys.stderr so it's visible in container logs
99+
log_file_path = logs_dir / f"reducer-{i}.log"
100+
if log_file_path.stat().st_size > 0:
101+
print(f"--- Contents of {log_file_path.name} ---", file=sys.stderr)
102+
sys.stderr.write(log_file_path.read_text())
103+
print(f"--- End of {log_file_path.name} ---", file=sys.stderr)
98104

99105
logger.error("All reducers terminated")
100106

0 commit comments

Comments
 (0)