mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 10:11:30 +08:00
feat(companion): Redirect companion stdout and stderr
Child calls _redirect_output after env setup: each configured log path is opened append-mode and dup2'd onto fd 1/2. None/inherit keeps the inherited fd; stderr stdout shares stdout's fd. Rotation stays external. Add tests for inherit, append flags, file dup2, and stderr-to-stdout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ea2748a209
commit
2bf7e1b1fb
@ -673,7 +673,7 @@ No per-companion logic in Arbiter.
|
||||
- [x] Add `CompanionManager` skeleton.
|
||||
- [x] Spawn one companion process from the manager.
|
||||
- [x] Apply `cwd` and `env` before target.
|
||||
- [ ] Redirect `stdout` and `stderr`.
|
||||
- [x] Redirect `stdout` and `stderr`.
|
||||
- [ ] Reap exited companion processes.
|
||||
- [ ] Implement `STARTING -> RUNNING` using `startsecs`.
|
||||
- [ ] Implement `BACKOFF` with fixed `companion_restart_delay`.
|
||||
|
||||
@ -48,6 +48,7 @@ class CompanionManager:
|
||||
|
||||
try:
|
||||
self._apply_environment(proc.config)
|
||||
self._redirect_output(proc.config)
|
||||
target = self._resolve_target(proc.config.target)
|
||||
target()
|
||||
except SystemExit:
|
||||
@ -70,6 +71,33 @@ class CompanionManager:
|
||||
if config.env:
|
||||
os.environ.update(config.env)
|
||||
|
||||
@staticmethod
|
||||
def _redirect_output(config: CompanionConfig) -> None:
|
||||
"""Send the companion's stdout and stderr to its configured log files.
|
||||
|
||||
By default a companion just inherits the manager's stdout/stderr, so
|
||||
leaving these unset (or ``"inherit"``) keeps that. Give a file path and
|
||||
we append the output there instead. For stderr you can also pass
|
||||
``"stdout"`` to fold the two streams into one file.
|
||||
"""
|
||||
out = CompanionManager._open_output(config.stdout)
|
||||
if out is not None:
|
||||
os.dup2(out, 1)
|
||||
if config.stderr == "stdout":
|
||||
os.dup2(1, 2)
|
||||
else:
|
||||
err = CompanionManager._open_output(config.stderr)
|
||||
if err is not None:
|
||||
os.dup2(err, 2)
|
||||
|
||||
@staticmethod
|
||||
def _open_output(value):
|
||||
"""Open one log file for writing, or return None to leave the stream
|
||||
as-is when the companion should keep inheriting it."""
|
||||
if value in (None, "inherit"):
|
||||
return None
|
||||
return os.open(value, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_target(target: Union[Callable, str]) -> Callable:
|
||||
"""Return the zero-arg callable for a companion target.
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@ -54,6 +55,46 @@ def test_apply_environment_noop_without_cwd_env():
|
||||
chdir.assert_not_called()
|
||||
|
||||
|
||||
def test_open_output_inherit_returns_none():
|
||||
assert CompanionManager._open_output(None) is None
|
||||
assert CompanionManager._open_output("inherit") is None
|
||||
|
||||
|
||||
def test_open_output_path_opens_append():
|
||||
with mock.patch("os.open", return_value=9) as op:
|
||||
fd = CompanionManager._open_output("/var/log/rq.log")
|
||||
assert fd == 9
|
||||
flags = op.call_args.args[1]
|
||||
assert flags & os.O_APPEND and flags & os.O_CREAT
|
||||
|
||||
|
||||
def test_redirect_output_files():
|
||||
config = CompanionConfig(name="rq", target=lambda: None,
|
||||
stdout="/o.log", stderr="/e.log")
|
||||
with mock.patch("os.open", side_effect=[10, 11]), \
|
||||
mock.patch("os.dup2") as dup2:
|
||||
CompanionManager._redirect_output(config)
|
||||
dup2.assert_any_call(10, 1)
|
||||
dup2.assert_any_call(11, 2)
|
||||
|
||||
|
||||
def test_redirect_output_stderr_to_stdout():
|
||||
config = CompanionConfig(name="rq", target=lambda: None,
|
||||
stdout="/o.log", stderr="stdout")
|
||||
with mock.patch("os.open", return_value=10), \
|
||||
mock.patch("os.dup2") as dup2:
|
||||
CompanionManager._redirect_output(config)
|
||||
dup2.assert_any_call(10, 1)
|
||||
dup2.assert_any_call(1, 2)
|
||||
|
||||
|
||||
def test_redirect_output_inherit_noop():
|
||||
config = CompanionConfig(name="rq", target=lambda: None)
|
||||
with mock.patch("os.dup2") as dup2:
|
||||
CompanionManager._redirect_output(config)
|
||||
dup2.assert_not_called()
|
||||
|
||||
|
||||
def test_spawn_parent_records_pid_and_starting():
|
||||
mgr = make_manager("rq")
|
||||
proc = mgr.processes["rq"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user