Add tests for reactive log printing.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
65797558f8
commit
44c1747127
6 changed files with 218 additions and 133 deletions
|
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from itertools import cycle
|
||||
from threading import Thread
|
||||
|
||||
|
|
@ -15,9 +16,6 @@ from compose.cli.signals import ShutdownException
|
|||
from compose.utils import split_buffer
|
||||
|
||||
|
||||
STOP = object()
|
||||
|
||||
|
||||
class LogPresenter(object):
|
||||
|
||||
def __init__(self, prefix_width, color_func):
|
||||
|
|
@ -79,51 +77,74 @@ class LogPrinter(object):
|
|||
queue = Queue()
|
||||
thread_args = queue, self.log_args
|
||||
thread_map = build_thread_map(self.containers, self.presenters, thread_args)
|
||||
start_producer_thread(
|
||||
start_producer_thread((
|
||||
thread_map,
|
||||
self.event_stream,
|
||||
self.presenters,
|
||||
thread_args)
|
||||
thread_args))
|
||||
|
||||
for line in consume_queue(queue, self.cascade_stop):
|
||||
remove_stopped_threads(thread_map)
|
||||
|
||||
if not line:
|
||||
if not thread_map:
|
||||
return
|
||||
continue
|
||||
|
||||
self.output.write(line)
|
||||
self.output.flush()
|
||||
|
||||
# TODO: this needs more logic
|
||||
# TODO: does consume_queue need to yield Nones to get to this point?
|
||||
if not thread_map:
|
||||
return
|
||||
|
||||
def remove_stopped_threads(thread_map):
|
||||
for container_id, tailer_thread in list(thread_map.items()):
|
||||
if not tailer_thread.is_alive():
|
||||
thread_map.pop(container_id, None)
|
||||
|
||||
|
||||
def build_thread(container, presenter, queue, log_args):
|
||||
tailer = Thread(
|
||||
target=tail_container_logs,
|
||||
args=(container, presenter, queue, log_args))
|
||||
tailer.daemon = True
|
||||
tailer.start()
|
||||
return tailer
|
||||
|
||||
|
||||
def build_thread_map(initial_containers, presenters, thread_args):
|
||||
def build_thread(container):
|
||||
tailer = Thread(
|
||||
target=tail_container_logs,
|
||||
args=(container, presenters.next()) + thread_args)
|
||||
tailer.daemon = True
|
||||
tailer.start()
|
||||
return tailer
|
||||
|
||||
return {
|
||||
container.id: build_thread(container)
|
||||
container.id: build_thread(container, presenters.next(), *thread_args)
|
||||
for container in initial_containers
|
||||
}
|
||||
|
||||
|
||||
class QueueItem(namedtuple('_QueueItem', 'item is_stop exc')):
|
||||
|
||||
@classmethod
|
||||
def new(cls, item):
|
||||
return cls(item, None, None)
|
||||
|
||||
@classmethod
|
||||
def exception(cls, exc):
|
||||
return cls(None, None, exc)
|
||||
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
return cls(None, True, None)
|
||||
|
||||
|
||||
def tail_container_logs(container, presenter, queue, log_args):
|
||||
generator = get_log_generator(container)
|
||||
|
||||
try:
|
||||
for item in generator(container, log_args):
|
||||
queue.put((item, None))
|
||||
|
||||
if log_args.get('follow'):
|
||||
yield presenter.color_func(wait_on_exit(container))
|
||||
|
||||
queue.put((STOP, None))
|
||||
|
||||
queue.put(QueueItem.new(presenter.present(container, item)))
|
||||
except Exception as e:
|
||||
queue.put((None, e))
|
||||
queue.put(QueueItem.exception(e))
|
||||
return
|
||||
|
||||
if log_args.get('follow'):
|
||||
queue.put(QueueItem.new(presenter.color_func(wait_on_exit(container))))
|
||||
queue.put(QueueItem.stop())
|
||||
|
||||
|
||||
def get_log_generator(container):
|
||||
|
|
@ -156,37 +177,48 @@ def wait_on_exit(container):
|
|||
return "%s exited with code %s\n" % (container.name, exit_code)
|
||||
|
||||
|
||||
def start_producer_thread(thread_map, event_stream, presenters, thread_args):
|
||||
queue, log_args = thread_args
|
||||
|
||||
def watch_events():
|
||||
for event in event_stream:
|
||||
# TODO: handle start and stop events
|
||||
pass
|
||||
|
||||
producer = Thread(target=watch_events)
|
||||
def start_producer_thread(thread_args):
|
||||
producer = Thread(target=watch_events, args=thread_args)
|
||||
producer.daemon = True
|
||||
producer.start()
|
||||
|
||||
|
||||
def watch_events(thread_map, event_stream, presenters, thread_args):
|
||||
for event in event_stream:
|
||||
if event['action'] != 'start':
|
||||
continue
|
||||
|
||||
if event['id'] in thread_map:
|
||||
if thread_map[event['id']].is_alive():
|
||||
continue
|
||||
# Container was stopped and started, we need a new thread
|
||||
thread_map.pop(event['id'], None)
|
||||
|
||||
thread_map[event['id']] = build_thread(
|
||||
event['container'],
|
||||
presenters.next(),
|
||||
*thread_args)
|
||||
|
||||
|
||||
def consume_queue(queue, cascade_stop):
|
||||
"""Consume the queue by reading lines off of it and yielding them."""
|
||||
while True:
|
||||
try:
|
||||
item, exception = queue.get(timeout=0.1)
|
||||
item = queue.get(timeout=0.1)
|
||||
except Empty:
|
||||
pass
|
||||
yield None
|
||||
continue
|
||||
# See https://github.com/docker/compose/issues/189
|
||||
except thread.error:
|
||||
raise ShutdownException()
|
||||
|
||||
if exception:
|
||||
raise exception
|
||||
if item.exc:
|
||||
raise item.exc
|
||||
|
||||
if item is STOP:
|
||||
if item.is_stop:
|
||||
if cascade_stop:
|
||||
raise StopIteration
|
||||
else:
|
||||
continue
|
||||
|
||||
yield item
|
||||
yield item.item
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from .docopt_command import NoSuchCommand
|
|||
from .errors import UserError
|
||||
from .formatter import ConsoleWarningFormatter
|
||||
from .formatter import Formatter
|
||||
from .log_printer import build_log_presenters
|
||||
from .log_printer import LogPrinter
|
||||
from .utils import get_version_info
|
||||
from .utils import yesno
|
||||
|
|
@ -277,6 +278,7 @@ class TopLevelCommand(object):
|
|||
|
||||
def json_format_event(event):
|
||||
event['time'] = event['time'].isoformat()
|
||||
event.pop('container')
|
||||
return json.dumps(event)
|
||||
|
||||
for event in self.project.events():
|
||||
|
|
@ -374,7 +376,6 @@ class TopLevelCommand(object):
|
|||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
|
||||
monochrome = options['--no-color']
|
||||
tail = options['--tail']
|
||||
if tail is not None:
|
||||
if tail.isdigit():
|
||||
|
|
@ -387,7 +388,11 @@ class TopLevelCommand(object):
|
|||
'timestamps': options['--timestamps']
|
||||
}
|
||||
print("Attaching to", list_containers(containers))
|
||||
LogPrinter(containers, monochrome=monochrome, log_args=log_args).run()
|
||||
log_printer_from_project(
|
||||
project,
|
||||
containers,
|
||||
options['--no-color'],
|
||||
log_args).run()
|
||||
|
||||
def pause(self, options):
|
||||
"""
|
||||
|
|
@ -693,7 +698,6 @@ class TopLevelCommand(object):
|
|||
when attached or when containers are already
|
||||
running. (default: 10)
|
||||
"""
|
||||
monochrome = options['--no-color']
|
||||
start_deps = not options['--no-deps']
|
||||
cascade_stop = options['--abort-on-container-exit']
|
||||
service_names = options['SERVICE']
|
||||
|
|
@ -704,7 +708,10 @@ class TopLevelCommand(object):
|
|||
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
||||
|
||||
with up_shutdown_context(self.project, service_names, timeout, detached):
|
||||
to_attach = self.project.up(
|
||||
# start the event stream first so we don't lose any events
|
||||
event_stream = project.events()
|
||||
|
||||
to_attach = project.up(
|
||||
service_names=service_names,
|
||||
start_deps=start_deps,
|
||||
strategy=convergence_strategy_from_opts(options),
|
||||
|
|
@ -714,8 +721,14 @@ class TopLevelCommand(object):
|
|||
|
||||
if detached:
|
||||
return
|
||||
log_args = {'follow': True}
|
||||
log_printer = build_log_printer(to_attach, service_names, monochrome, cascade_stop, log_args)
|
||||
|
||||
log_printer = log_printer_from_project(
|
||||
project,
|
||||
filter_containers_to_service_names(to_attach, service_names),
|
||||
options['--no-color'],
|
||||
{'follow': True},
|
||||
cascade_stop,
|
||||
event_stream=event_stream)
|
||||
print("Attaching to", list_containers(log_printer.containers))
|
||||
log_printer.run()
|
||||
|
||||
|
|
@ -827,13 +840,30 @@ def run_one_off_container(container_options, project, service, options):
|
|||
sys.exit(exit_code)
|
||||
|
||||
|
||||
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, log_args=log_args)
|
||||
def log_printer_from_project(
|
||||
project,
|
||||
containers,
|
||||
monochrome,
|
||||
log_args,
|
||||
cascade_stop=False,
|
||||
event_stream=None,
|
||||
):
|
||||
return LogPrinter(
|
||||
containers,
|
||||
build_log_presenters(project.service_names, monochrome),
|
||||
event_stream or project.events(),
|
||||
cascade_stop=cascade_stop,
|
||||
log_args=log_args)
|
||||
|
||||
|
||||
def filter_containers_to_service_names(containers, service_names):
|
||||
if not service_names:
|
||||
return containers
|
||||
|
||||
return [
|
||||
container
|
||||
for container in containers if container.service in service_names
|
||||
]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ class Project(object):
|
|||
continue
|
||||
|
||||
# TODO: get labels from the API v1.22 , see github issue 2618
|
||||
# TODO: this can fail if the conatiner is removed, wrap in try/except
|
||||
container = Container.from_id(self.client, event['id'])
|
||||
if container.service not in service_names:
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue