Browse Source

Add -f, --follow flag as option on logs.
Closes #2187
Signed-off-by: Stéphane Seguin <[email protected]>

Stéphane Seguin 9 years ago
parent
commit
82632098a3

+ 12 - 7
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):

+ 8 - 5
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

+ 1 - 0
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.

+ 1 - 1
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

+ 22 - 0
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')

+ 6 - 0
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"

+ 12 - 2
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

+ 2 - 2
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)