mirror of
				https://github.com/docker/compose.git
				synced 2025-10-31 19:24:21 +01:00 
			
		
		
		
	Merge pull request #2720 from seguins/2227-improvements-logs
Add flags on logs
This commit is contained in:
		
						commit
						0b3561a7d5
					
				| @ -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): | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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,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 | ||||
|  | ||||
| @ -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
									
								
								tests/fixtures/logs-composefile/docker-compose.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								tests/fixtures/logs-composefile/docker-compose.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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
									
								
								tests/fixtures/logs-tail-composefile/docker-compose.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								tests/fixtures/logs-tail-composefile/docker-compose.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| simple: | ||||
|   image: busybox:latest | ||||
|   command: sh -c "echo a && echo b && echo c && echo d" | ||||
| @ -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 | ||||
|  | ||||
| @ -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) | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user