From 82632098a33cb57b7dc8412b13c5f17aacb162a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Seguin?= Date: Thu, 21 Jan 2016 13:16:04 +0100 Subject: [PATCH] =?UTF-8?q?Add=20-f,=20--follow=20flag=20as=20option=20on?= =?UTF-8?q?=20logs.=20Closes=20#2187=20Signed-off-by:=20St=C3=A9phane=20Se?= =?UTF-8?q?guin=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose/cli/log_printer.py | 19 ++++++++++------ compose/cli/main.py | 13 ++++++----- docs/reference/logs.md | 1 + requirements.txt | 2 +- tests/acceptance/cli_test.py | 22 +++++++++++++++++++ .../logs-composefile/docker-compose.yml | 6 +++++ tests/unit/cli/log_printer_test.py | 14 ++++++++++-- tests/unit/cli/main_test.py | 4 ++-- 8 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/logs-composefile/docker-compose.yml diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index 85fef794f..6fd5ca5dd 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -13,11 +13,13 @@ from compose.utils import split_buffer class LogPrinter(object): """Print logs from many containers to a single output stream.""" - def __init__(self, containers, output=sys.stdout, monochrome=False, cascade_stop=False): + def __init__(self, containers, output=sys.stdout, monochrome=False, + cascade_stop=False, follow=False): self.containers = containers self.output = utils.get_output_stream(output) self.monochrome = monochrome self.cascade_stop = cascade_stop + self.follow = follow def run(self): if not self.containers: @@ -41,7 +43,7 @@ class LogPrinter(object): for color_func, container in zip(color_funcs, self.containers): generator_func = get_log_generator(container) prefix = color_func(build_log_prefix(container, prefix_width)) - yield generator_func(container, prefix, color_func) + yield generator_func(container, prefix, color_func, self.follow) def build_log_prefix(container, prefix_width): @@ -64,28 +66,31 @@ def get_log_generator(container): return build_no_log_generator -def build_no_log_generator(container, prefix, color_func): +def build_no_log_generator(container, prefix, color_func, follow): """Return a generator that prints a warning about logs and waits for container to exit. """ yield "{} WARNING: no logs are available with the '{}' log driver\n".format( prefix, container.log_driver) - yield color_func(wait_on_exit(container)) + if follow: + yield color_func(wait_on_exit(container)) -def build_log_generator(container, prefix, color_func): +def build_log_generator(container, prefix, color_func, follow): # if the container doesn't have a log_stream we need to attach to container # before log printer starts running if container.log_stream is None: - stream = container.attach(stdout=True, stderr=True, stream=True, logs=True) + stream = container.logs(stdout=True, stderr=True, stream=True, + follow=follow) line_generator = split_buffer(stream) else: line_generator = split_buffer(container.log_stream) for line in line_generator: yield prefix + line - yield color_func(wait_on_exit(container)) + if follow: + yield color_func(wait_on_exit(container)) def wait_on_exit(container): diff --git a/compose/cli/main.py b/compose/cli/main.py index 6a04f9f00..1e6d0a55b 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -328,13 +328,15 @@ class TopLevelCommand(DocoptCommand): Usage: logs [options] [SERVICE...] Options: - --no-color Produce monochrome output. + --no-color Produce monochrome output. + -f, --follow Follow log output """ containers = project.containers(service_names=options['SERVICE'], stopped=True) monochrome = options['--no-color'] + follow = options['--follow'] print("Attaching to", list_containers(containers)) - LogPrinter(containers, monochrome=monochrome).run() + LogPrinter(containers, monochrome=monochrome, follow=follow).run() def pause(self, project, options): """ @@ -660,7 +662,8 @@ class TopLevelCommand(DocoptCommand): if detached: return - log_printer = build_log_printer(to_attach, service_names, monochrome, cascade_stop) + log_printer = build_log_printer(to_attach, service_names, monochrome, cascade_stop, + follow=True) print("Attaching to", list_containers(log_printer.containers)) log_printer.run() @@ -758,13 +761,13 @@ def run_one_off_container(container_options, project, service, options): sys.exit(exit_code) -def build_log_printer(containers, service_names, monochrome, cascade_stop): +def build_log_printer(containers, service_names, monochrome, cascade_stop, follow): if service_names: containers = [ container for container in containers if container.service in service_names ] - return LogPrinter(containers, monochrome=monochrome, cascade_stop=cascade_stop) + return LogPrinter(containers, monochrome=monochrome, cascade_stop=cascade_stop, follow=follow) @contextlib.contextmanager diff --git a/docs/reference/logs.md b/docs/reference/logs.md index 5b241ea70..4f0d57304 100644 --- a/docs/reference/logs.md +++ b/docs/reference/logs.md @@ -16,6 +16,7 @@ Usage: logs [options] [SERVICE...] Options: --no-color Produce monochrome output. +-f, --follow Follow log output ``` Displays log output from services. diff --git a/requirements.txt b/requirements.txt index e25386d24..b31840c85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ PyYAML==3.11 cached-property==1.2.0 -docker-py==1.7.2 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4 +git+https://github.com/docker/docker-py.git@81d8caaf36159bf1accd86eab2e157bf8dd071a9#egg=docker-py jsonschema==2.5.1 requests==2.7.0 six==1.7.3 diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 02f828727..60a5693bd 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -398,6 +398,8 @@ class CLITestCase(DockerClientTestCase): assert 'simple_1 | simple' in result.stdout assert 'another_1 | another' in result.stdout + assert 'simple_1 exited with code 0' in result.stdout + assert 'another_1 exited with code 0' in result.stdout @v2_only() def test_up(self): @@ -1141,6 +1143,26 @@ class CLITestCase(DockerClientTestCase): def test_logs_invalid_service_name(self): self.dispatch(['logs', 'madeupname'], returncode=1) + def test_logs_follow(self): + self.base_dir = 'tests/fixtures/echo-services' + self.dispatch(['up', '-d'], None) + + result = self.dispatch(['logs', '-f']) + + assert result.stdout.count('\n') == 5 + assert 'simple' in result.stdout + assert 'another' in result.stdout + assert 'exited with code 0' in result.stdout + + def test_logs_unfollow(self): + self.base_dir = 'tests/fixtures/logs-composefile' + self.dispatch(['up', '-d'], None) + + result = self.dispatch(['logs']) + + assert result.stdout.count('\n') >= 1 + assert 'exited with code 0' not in result.stdout + def test_kill(self): self.dispatch(['up', '-d'], None) service = self.project.get_service('simple') diff --git a/tests/fixtures/logs-composefile/docker-compose.yml b/tests/fixtures/logs-composefile/docker-compose.yml new file mode 100644 index 000000000..0af9d805c --- /dev/null +++ b/tests/fixtures/logs-composefile/docker-compose.yml @@ -0,0 +1,6 @@ +simple: + image: busybox:latest + command: sh -c "echo hello && sleep 200" +another: + image: busybox:latest + command: sh -c "echo test" diff --git a/tests/unit/cli/log_printer_test.py b/tests/unit/cli/log_printer_test.py index 5b04226cf..bed0eae83 100644 --- a/tests/unit/cli/log_printer_test.py +++ b/tests/unit/cli/log_printer_test.py @@ -17,7 +17,7 @@ def build_mock_container(reader): name_without_project='web_1', has_api_logs=True, log_stream=None, - attach=reader, + logs=reader, wait=mock.Mock(return_value=0), ) @@ -39,7 +39,7 @@ def mock_container(): class TestLogPrinter(object): def test_single_container(self, output_stream, mock_container): - LogPrinter([mock_container], output=output_stream).run() + LogPrinter([mock_container], output=output_stream, follow=True).run() output = output_stream.getvalue() assert 'hello' in output @@ -47,6 +47,15 @@ class TestLogPrinter(object): # Call count is 2 lines + "container exited line" assert output_stream.flush.call_count == 3 + def test_single_container_without_follow(self, output_stream, mock_container): + LogPrinter([mock_container], output=output_stream, follow=False).run() + + output = output_stream.getvalue() + assert 'hello' in output + assert 'world' in output + # Call count is 2 lines + assert output_stream.flush.call_count == 2 + def test_monochrome(self, output_stream, mock_container): LogPrinter([mock_container], output=output_stream, monochrome=True).run() assert '\033[' not in output_stream.getvalue() @@ -86,3 +95,4 @@ class TestLogPrinter(object): output = output_stream.getvalue() assert "WARNING: no logs are available with the 'none' log driver\n" in output + assert "exited with code" not in output diff --git a/tests/unit/cli/main_test.py b/tests/unit/cli/main_test.py index fd6c50028..bddb9f178 100644 --- a/tests/unit/cli/main_test.py +++ b/tests/unit/cli/main_test.py @@ -33,7 +33,7 @@ class CLIMainTestCase(unittest.TestCase): mock_container('another', 1), ] service_names = ['web', 'db'] - log_printer = build_log_printer(containers, service_names, True, False) + log_printer = build_log_printer(containers, service_names, True, False, True) self.assertEqual(log_printer.containers, containers[:3]) def test_build_log_printer_all_services(self): @@ -43,7 +43,7 @@ class CLIMainTestCase(unittest.TestCase): mock_container('other', 1), ] service_names = [] - log_printer = build_log_printer(containers, service_names, True, False) + log_printer = build_log_printer(containers, service_names, True, False, True) self.assertEqual(log_printer.containers, containers)