浏览代码

Merge pull request #2720 from seguins/2227-improvements-logs

Add flags on logs
Aanand Prasad 9 年之前
父节点
当前提交
0b3561a7d5

+ 16 - 7
compose/cli/log_printer.py

@@ -13,11 +13,18 @@ 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,
+                 log_args=None):
+        log_args = log_args or {}
         self.containers = containers
         self.output = utils.get_output_stream(output)
         self.monochrome = monochrome
         self.cascade_stop = cascade_stop
+        self.log_args = log_args
 
     def run(self):
         if not self.containers:
@@ -41,7 +48,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.log_args)
 
 
 def build_log_prefix(container, prefix_width):
@@ -64,28 +71,30 @@ 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, log_args):
     """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 log_args.get('follow'):
+        yield color_func(wait_on_exit(container))
 
 
-def build_log_generator(container, prefix, color_func):
+def build_log_generator(container, prefix, color_func, log_args):
     # 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, **log_args)
         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 log_args.get('follow'):
+        yield color_func(wait_on_exit(container))
 
 
 def wait_on_exit(container):

+ 21 - 5
compose/cli/main.py

@@ -380,13 +380,28 @@ class TopLevelCommand(DocoptCommand):
         Usage: logs [options] [SERVICE...]
 
         Options:
-            --no-color  Produce monochrome output.
+            --no-color          Produce monochrome output.
+            -f, --follow        Follow log output.
+            -t, --timestamps    Show timestamps.
+            --tail="all"        Number of lines to show from the end of the logs
+                                for each container.
         """
         containers = project.containers(service_names=options['SERVICE'], stopped=True)
 
         monochrome = options['--no-color']
+        tail = options['--tail']
+        if tail is not None:
+            if tail.isdigit():
+                tail = int(tail)
+            elif tail != 'all':
+                raise UserError("tail flag must be all or a number")
+        log_args = {
+            'follow': options['--follow'],
+            'tail': tail,
+            'timestamps': options['--timestamps']
+        }
         print("Attaching to", list_containers(containers))
-        LogPrinter(containers, monochrome=monochrome).run()
+        LogPrinter(containers, monochrome=monochrome, log_args=log_args).run()
 
     def pause(self, project, options):
         """
@@ -712,7 +727,8 @@ class TopLevelCommand(DocoptCommand):
 
             if detached:
                 return
-            log_printer = build_log_printer(to_attach, service_names, monochrome, cascade_stop)
+            log_args = {'follow': True}
+            log_printer = build_log_printer(to_attach, service_names, monochrome, cascade_stop, log_args)
             print("Attaching to", list_containers(log_printer.containers))
             log_printer.run()
 
@@ -810,13 +826,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, log_args):
     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, log_args=log_args)
 
 
 @contextlib.contextmanager

+ 5 - 1
docs/reference/logs.md

@@ -15,7 +15,11 @@ parent = "smn_compose_cli"
 Usage: logs [options] [SERVICE...]
 
 Options:
---no-color  Produce monochrome output.
+--no-color          Produce monochrome output.
+-f, --follow        Follow log output
+-t, --timestamps    Show timestamps
+--tail              Number of lines to show from the end of the logs
+                    for each container.
 ```
 
 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

+ 38 - 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):
@@ -1159,6 +1161,42 @@ 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_logs_timestamps(self):
+        self.base_dir = 'tests/fixtures/echo-services'
+        self.dispatch(['up', '-d'], None)
+
+        result = self.dispatch(['logs', '-f', '-t'], None)
+
+        self.assertRegexpMatches(result.stdout, '(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})')
+
+    def test_logs_tail(self):
+        self.base_dir = 'tests/fixtures/logs-tail-composefile'
+        self.dispatch(['up'], None)
+
+        result = self.dispatch(['logs', '--tail', '2'], None)
+
+        assert result.stdout.count('\n') == 3
+
     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"

+ 3 - 0
tests/fixtures/logs-tail-composefile/docker-compose.yml

@@ -0,0 +1,3 @@
+simple:
+  image: busybox:latest
+  command: sh -c "echo a && echo b && echo c && echo d"

+ 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, log_args={'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_stream(self, output_stream, mock_container):
+        LogPrinter([mock_container], output=output_stream).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, {'follow': 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, {'follow': True})
         self.assertEqual(log_printer.containers, containers)