"""OWS Logger Compliant Runners. This module extends the garcon runners and inspects the context for correlation IDs to create logger objects with. These loggers are then added to a thread local object so they can be accessed easily and safely like the flask.g object. """ from concurrent import futures from concurrent.futures import ThreadPoolExecutor from functools import update_wrapper from garcon import runner from garcon.task import flatten from owslogger import logger from flows import g from flows import log class Sync(runner.Sync): """Syncronous task runner. This runner executes all tasks in sequence. """ def execute(self, activity, context): """Execute the tasks in sequence. Args: activity (ActivityWorker): activity worker instance. context (dict): context params to pass to the tasks. Returns: dict: compiled responses from tasks. """ result = dict() for task in flatten(self.tasks, context): activity.heartbeat() task_context = dict(list(result.items()) + list(context.items())) task = wrap_task_with_logger(task, task_context) resp = task(task_context, activity=activity) result.update(resp or dict()) return result class Async(runner.Async): """Asyncronous task runner. This runner executes all tasks in an activity in a threadpool. """ def execute(self, activity, context): """Execute the tasks in a thread pool. Args: activity (ActivityWorker): activity worker instance. context (dict): context params to pass to the tasks. Returns: dict: compiled responses from tasks. """ result = dict() with ThreadPoolExecutor(max_workers=self.max_workers) as executor: tasks = [] for task in flatten(self.tasks, context): task = wrap_task_with_logger(task, context) tasks.append(executor.submit(task, context, activity=activity)) for future in futures.as_completed(tasks): activity.heartbeat() data = future.result() result.update(data or {}) return result def wrap_task_with_logger(task, context): """Wrap task with a logger for the task's correlation ID parameter. The tasks will have a side effect of having the flows.g thread local object populated with a owslogger compliant logger. This is done by inspecting the context parameters passed to the task callable. So by convention, all tasks must have a 'correlation_id' parameter to be able to have a compliant global logger. Args: task (callable): Garcon compatible task. context (dict): task parameters being passed to the task. Returns: callable: same task with the global logging side effect. """ def logging_task(*args, **kwargs): if 'correlation_id' in context: g.log = logger.OwsLoggingAdapter( log.get_logger(), {'correlation_id': context['correlation_id']}) else: g.log = log.get_logger() return task(*args, **kwargs) update_wrapper(logging_task, task) return logging_task