Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add LogOperations to execution hooks #661

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f4f5cb7
Added log operations.
klywang Aug 2, 2022
3999fe3
Added LogOperations to __init__
klywang Aug 2, 2022
8e4d989
Added tests for LogOperations.
klywang Aug 2, 2022
dfcf5ab
Added project definitions for log hook tests.
klywang Aug 2, 2022
c312cdb
Merge branch 'master' into feature/hooks-LogOperation
klywang Sep 21, 2022
5f12826
refactor: LogHook rework the getting/setting up of loggers
b-butler Oct 7, 2022
477ff48
Merge branch 'master' into feature/hooks-LogOperation
b-butler Oct 7, 2022
3951e74
doc: Fix pre-commit failure.
b-butler Oct 7, 2022
b360a2f
test: TestHooksInvalidOption.call_subcmd outputs error every time.
b-butler Oct 10, 2022
ee84251
fix: stringify job before passing to logging.getLogger()
b-butler Oct 10, 2022
f990244
refactor: Modify names of log functions for Log hook
b-butler Oct 10, 2022
d52d350
doc: Improve documentation of LogOperations hook
b-butler Oct 10, 2022
7826466
feat: Add helper method for operation level hooks
b-butler Oct 10, 2022
5fca1ac
refactor: Modify LogOperations base log filename
b-butler Oct 10, 2022
1e97e2b
refactor: Remove __call__ method of LogOperation
b-butler Oct 10, 2022
543d373
Merge branch 'master' into feature/hooks-LogOperation
b-butler Oct 10, 2022
8b619de
fix: LogOperation.install_operation_hooks
b-butler Oct 10, 2022
66d2e6c
test: Update tests to recent LogOperation updates
b-butler Oct 10, 2022
79b34e0
test: Fix faulty test's assumption on underlying project
b-butler Oct 11, 2022
26a63e2
ci: Update CircleCI
b-butler Oct 11, 2022
d5b81dd
Merge branch 'master' into feature/hooks-LogOperation
b-butler Oct 13, 2022
740b785
Merge branch 'master' into feature/hooks-LogOperation
bdice Jan 17, 2023
40f6dad
Added more detailed docstring.
klywang Jan 24, 2023
7eb6de9
Merge branch 'master' into feature/hooks-LogOperation
klywang Jan 24, 2023
1291617
Updated hooks test setup with new signac-flow API
klywang Jan 24, 2023
7e306cc
Update flow/hooks/log_operations.py
klywang Jan 27, 2023
44d9f2c
Merge branch 'master' into feature/hooks-LogOperation
klywang Feb 14, 2023
3633ffa
Changed scamples to be consistent with new decorator evalutaion order.
klywang Feb 14, 2023
1646cdc
Finished docstring in example.
klywang Feb 14, 2023
467668e
Fixed example in LogOperations.
klywang Feb 14, 2023
1f27e50
Updated examples.
klywang Feb 26, 2023
294ea87
Merge branch 'master' into feature/hooks-LogOperation
klywang Feb 26, 2023
7f0ddad
Updated example so tha trun opertions was not included. Did not work …
klywang Feb 26, 2023
105099a
Merge branch 'master' into feature/hooks-LogOperation
klywang Mar 1, 2023
3e69beb
Merge branch 'main' into feature/hooks-LogOperation
b-butler Apr 3, 2023
c329258
Merge branch 'main' into feature/hooks-LogOperation
klywang Jun 19, 2023
0452d88
Apply suggestions from code review
klywang Jul 7, 2023
fb3bb26
Build(deps-dev): Bump pre-commit from 3.3.2 to 3.3.3 (#756)
dependabot[bot] Jul 1, 2023
ea627fe
Build(deps): Bump ruamel-yaml from 0.17.31 to 0.17.32 (#755)
dependabot[bot] Jul 3, 2023
670dd25
Build(deps): Bump pytest from 7.3.1 to 7.4.0 (#754)
dependabot[bot] Jul 3, 2023
e19256b
[pre-commit.ci] pre-commit autoupdate (#757)
pre-commit-ci[bot] Jul 4, 2023
2a4c110
Merge branch 'main' into feature/hooks-LogOperation
klywang Jul 7, 2023
1debcb9
Modified log strings for consistency.
klywang Jul 7, 2023
7b91827
Comment clarity and shorter default log name.
klywang Jul 7, 2023
8b448fc
Update job workspace to job directory.
klywang Jul 7, 2023
1f3229f
Updated changelog.
klywang Jul 7, 2023
a6579e2
Merge branch 'main' into feature/hooks-LogOperation
b-butler Oct 13, 2023
672726b
test: Refactor hook test into single template
b-butler Oct 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion flow/hooks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
# This software is licensed under the BSD 3-Clause License.
"""Operation hooks."""
from .hooks import _Hooks
from .log_operations import LogOperations

__all__ = ["_Hooks"]
__all__ = ["_Hooks", "LogOperations"]
2 changes: 1 addition & 1 deletion flow/hooks/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ class _Hooks:
def start_hook(operation_name, job):
print(f"Starting operation {operation_name} on job {job.id}.")

@FlowProject.operation
@FlowProject.operation_hooks.on_start(start_hook)
@FlowProject.operation
def foo(job):
pass

Expand Down
144 changes: 144 additions & 0 deletions flow/hooks/log_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Copyright (c) 2023 The Regents of the University of Michigan
# All rights reserved.
# This software is licensed under the BSD 3-Clause License.
"""Builtin execution hook for basic logging."""
import logging


class LogOperations:
""":class:`~.LogOperations` logs information about the execution of operations to a log file.

This hook can provide information on the start, successful completion, and/or
erroring of one or more operations in a `flow.FlowProject` instance. The logs are stored in a
file given by the parameter ``fn_logfile``. This file will be appended to if it already exists.

The default formating for the log provides the time, job id, log level, and log message.

.. note::

All logging is performed at the INFO level. To ensure outputs are captured in log files,
use the `--debug` flag when running or submitting jobs, or specify
`submit_options=--debug` in your directives (example shown below).


Examples
--------
The following example will install :class:`~.LogOperations` at the operation level.
Where the log will be stored in a file name `foo.log` in the job workspace.
klywang marked this conversation as resolved.
Show resolved Hide resolved

.. code-block:: python
from flow import FlowProject
from flow.hooks import LogOperations


class Project(FlowProject):
pass


def install_operation_log_hook(operation_name, project_cls):
log = LogOperation(f"{operation_name}.log")
return lambda op: log.install_operation_hooks(op, project_cls)


@install_operation_log_hook("foo", Project)
@Project.operation(directives={
"submit_options": "--debug" # Always submit operation foo with the --debug flag
b-butler marked this conversation as resolved.
Show resolved Hide resolved
})
b-butler marked this conversation as resolved.
Show resolved Hide resolved
def foo(job):
pass


The code block below provides an example of how install :class:`~.LogOperations` to an
instance of :class:`~.FlowProject`

.. code-block:: python
from flow import FlowProject
from flow.hooks import LogOperations # Import build
klywang marked this conversation as resolved.
Show resolved Hide resolved
b-butler marked this conversation as resolved.
Show resolved Hide resolved


class Project(FlowProject):
pass


# Project operation definitions


if __name__ == "__main__":
project = Project()
project = LogOperations().install_project_hooks(project)
project.main()

Parameters
----------
fn_logfile : str
The name of the log file in the job workspace. Default is "operations.log".
"""

def __init__(self, fn_logfile="operations.log"):
self._fn_logfile = fn_logfile
# getLogger keep its own cache. This reduces the time spent setting up loggers
# by only doing it once.
self._loggers = {}

def on_start(self, operation, job):
"""Log the start of execution of an operation on a job."""
self._get_logger(job).info(f"Operation '{operation}' started.")

def on_success(self, operation, job):
"""Log the successful completion of a given job(s) operation pair."""
self._get_logger(job).info(
f"Operation '{operation}' finished without exception."
)

def on_exception(self, operation, error, job):
"""Log the raising of an error in the execution of a given job(s) operation pair."""
self._get_logger(job).info(
b-butler marked this conversation as resolved.
Show resolved Hide resolved
f"Operation '{operation}' failed with error '{error}'."
)

def _get_logger(self, job):
if job not in self._loggers:
self._loggers[job] = self._setup_logger(job)
return self._loggers[job]
klywang marked this conversation as resolved.
Show resolved Hide resolved

def _setup_logger(self, job):
logger = logging.getLogger(str(job))
fh = logging.FileHandler(job.fn(self._fn_logfile))
fh.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
fh.setFormatter(formatter)
logger.addHandler(fh)
return logger

def install_operation_hooks(self, op, project_cls=None):
"""Decorate operation to log execution.

Parameters
----------
op : function or type
An operation function to log or a subclass of `flow.FlowProject` if ``project_cls`` is
``None``.
project_cls : type
A subclass of `flow.FlowProject`.
"""
if project_cls is None:
return lambda func: self.install_operation_hooks(func, op)
project_cls.operation_hooks.on_start(self.on_start)(op)
project_cls.operation_hooks.on_success(self.on_success)(op)
project_cls.operation_hooks.on_exception(self.on_exception)(op)
return op

def install_project_hooks(self, project):
"""Install log operation to all operations in a signac-flow project.

Parameters
----------
project : flow.FlowProject
The project to install hooks on.
"""
project.project_hooks.on_start.append(self.on_start)
project.project_hooks.on_success.append(self.on_success)
project.project_hooks.on_exception.append(self.on_exception)
return project
36 changes: 36 additions & 0 deletions tests/define_hooks_logging_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from define_hooks_logging_project import (
HOOKS_ERROR_MESSAGE,
HOOKS_RUNTIME_MESSAGE,
set_logger,
)

import flow
from flow.hooks import LogOperations


class _HooksLogOperationsProject(flow.FlowProject):
pass


@_HooksLogOperationsProject.operation
def base(job):
logger = set_logger(job)

logger.info(HOOKS_RUNTIME_MESSAGE)

if job.sp.raise_exception:
raise RuntimeError(HOOKS_ERROR_MESSAGE)


@_HooksLogOperationsProject.operation(cmd=True)
def base_cmd(job):
if job.sp.raise_exception:
return "exit 42"
else:
return "touch base_cmd.txt"


if __name__ == "__main__":
LogOperations("operations.log").install_project_hooks(
_HooksLogOperationsProject()
).main()
48 changes: 48 additions & 0 deletions tests/define_hooks_logging_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging

from define_hooks_test_project import HOOKS_ERROR_MESSAGE

from flow import FlowProject
from flow.hooks import LogOperations

HOOKS_RUNTIME_MESSAGE = "Runtime message."


class _HooksLogOperationsProject(FlowProject):
pass


operation_log = LogOperations("operations.log")


def set_logger(job):
logger = logging.getLogger(str(job))
sh = logging.StreamHandler()
logger.addHandler(sh)
return logger


@operation_log.install_operation_hooks(_HooksLogOperationsProject)
@_HooksLogOperationsProject.operation
def base(job):
logger = set_logger(job)

logger.info(HOOKS_RUNTIME_MESSAGE)

if job.sp.raise_exception:
raise RuntimeError(HOOKS_ERROR_MESSAGE)


@_HooksLogOperationsProject.operation_hooks.on_start(operation_log.on_start)
@_HooksLogOperationsProject.operation_hooks.on_success(operation_log.on_success)
@_HooksLogOperationsProject.operation_hooks.on_exception(operation_log.on_exception)
@_HooksLogOperationsProject.operation(cmd=True)
def base_cmd(job):
if job.sp.raise_exception:
return "exit 42"
else:
return "touch base_cmd.txt"


if __name__ == "__main__":
_HooksLogOperationsProject().main()
Loading