Repository: pytorch/ignite

Issue:
Generalization & Enhancement of customized events, e.g. `CustomPeriodicEvent()`
### Issue

The current `CustomPeriodicEvent` and other classes like `EarlyStopping`, `NanOutput` (more to come) share common features like condition checking, condition updating and firing events which could/should be inherited from a base class. Also the current `CustomPeriodicEvents` is limited to iteration/epoch-number-periodicity of `Events.ITERATION_COMPLETED` and `Events.EPOCH_COMPLETED`, therefore this should be extended to all possible use-cases.
Limits of current `CustomPeriodicEvent`:
1. no base/meta classes for feature sharing
2. event-fire-condition fixed to number of iterations and epochs
3. customized events of multiple custom events are not listed in a central dictionary, e.g. `Events` for overview
4. customized events have non-customizable standardized event names (e.g. `Events.ITERATIONS_10`) instead of e.g. `Events.SAMPLES_1000_COMPLETED`, `Events.EARLY_STOPPING`, `Events.NAN_OUTPUT` etc. 

### Solution

To define base/meta classes (no. 1. of above list) consider following hierarchical breakdown:
- The Events are all fired at certain conditions (e.g. certain number of iterations is reached, loss limit is surpassed etc.), some of them periodic some of them not. So one could talk about `BaseConditionalEvents`.
- For periodic events two events per iteration would be fired, i.e. `Events.*_STARTED` & `Events.*_COMPLETED`, whereas for more specific conditions, e.g. early stopping, only a single `Events.EARLY_STOPPING` is required. Therefore a `BaseConditionalSingleEvents` and a `BaseConditionalDoubleEvents` could inherit from `BaseConditionalEvents`.
- The conditions to be checked, how they are updated also depending on if an event was fired or not and when the conditions are reset vary strongly with use-cases and should be defined as `abstractmethod`s (no. 2.).
- Also to which events these methods are attached should be provided as arguments to the users. I propose initial event arguments  compatible to the suggested enhanced `Metric` class in  #643, with reasonable default values facilitating backward compatibility (`started_event`, `step_started_event`, `step_completed_event`, `completed_event`) (no. 2).
- Generally all parameter of the `engine`'s `State` should be stored to `engine.state`. So this would theoretically include the condition's parameter of all custom event together (e.g. current minimum loss) and the number of fired custom events. Nevertheless with multiple conditional events the parameter quickly spam `engine.state`. So I propose only saving the number of fired events for each conditional event to `engine.state` and keep the other parameter in the conditional event instance. Also for debugging it is easier to have all parameter in the instance of the custom event than in one of the `engine`s it is attached to.

To cope with the other limits listed above I suggest:
3. add all customized events as `<enum Events>` to `Events` so all existing events can be viewed from everywhere
4. provide argument `name_of_event` in `__init__()` to customize the event name... because the `<enum Events>` have the (slightly confusing) argument `event_name` I suggest the `name_of_event` for distinguishing... (or just change 'event_name' to 'event'? don't know)

### Code suggestion

I provide in the following 2 modules (+1 to make them run before PR):
* `base_conditional_events.py` including the base classes
* `step_periodic_events.py` including two applications of the base classes: the `__name__=='__main__'` sections provides an output of `Events` holding all custom events
* (`logger_mixin.py` including the logger mixin class just in case someone wants to test before the PR... logger would be an issue on its own and and possibly synced with PyTorch)

I'm always happy about feedback/improvements, so let me know...

#### `base_conditional_events.py`
```python
from abc import ABCMeta, abstractmethod
from aenum import extend_enum

from ignite.engine import Events, State
from ignite._six import with_metaclass


class BaseConditionalEvents(with_metaclass(ABCMeta, object)):
    """
    Base class for all repetitive event handlers triggered when pre-defined condition(s) are reached.
    Normally, this meta class may only be inherited by other meta/base classes (e.g.
    `BaseConditionalsingle/DobuleEvents`) but not directly by event handler classes

    Note: The fired events counter should be stored and updated in `engine.state` as `event_state_attr`.

    Args:
        name_of_event (str): prefix of the event's full name, with `num_period_steps` being the suffix
        started_event (<enum, 'Events'>): trigger event to initialize any firing of conditional events. Before
            this `started_event` no event is fired at all.
        step_completed_event (<enum, 'Events'>): trigger event for firing `self.conditional_completed_event`
        completed_event (<enum, 'Events'>): trigger event to terminate any further firing of conditional events.
            After this event no more events are fired at all.

    """

    def __init__(self, name_of_event, started_event=Events.STARTED, step_completed_event=Events.ITERATION_COMPLETED,
                 completed_event=Events.EPOCH_COMPLETED):
        # Assign parameters
        self.started_event = started_event
        self.step_completed_event = step_completed_event
        self.completed_event = completed_event
        # Attribute name of `self.step_completed_event`'s iteration counter in `engine.state`
        self.step_event_state_attr = self._extract_event_state_attr(name_of_event=None, suffix=None,
                                                                    event_name=self.step_completed_event)
        # Iteration counter for fired `self.conditional_completed_events` set in `engine.state`
        if not hasattr(self, 'conditional_state_attr'):
            self.conditional_state_attr = name_of_event
        self._initialize_all_conditional_events()
        # Controls time interval (defined by `self.started_event`, `self.completed_event) in which conditional event is fired.
        self._firing_enabled = False

    @property
    def name_of_event(self):
        """Tag for print outs or help for manual debugging."""
        return self.conditional_state_attr

    @staticmethod
    def _extract_event_state_attr(name_of_event=None, event_name=None, suffix=None):
        """
        Extracts name components from `event_state_attr`, `event_name` and `suffix` and combines all to
        `engine.state`-attribute of the step events which is assigned to `self.conditional_state_attr`.
        The prefix is either from `name_of_event` (if not `None`) or is extracted from `event_name.value`.
        """
        # Either cut of `_STARTED` or `_COMPLETED` from `step_event.value` to receive `self.step_event_state_attr`
        if name_of_event is None and event_name is None:
            raise TypeError('Either or both of `name_of_event` and `event_name` must not be `None`, '
                            'but both values are `None`.')
        if name_of_event is None:
            if event_name.value.split(sep='_')[-1].upper() in ['STARTED', 'COMPLETED']:
                name_of_event = '_'.join(event_name.value.split(sep='_')[:-1]).lower()
            # or use full `self..step_started_event
            else:
                name_of_event = event_name.value.lower()
        return '_'.join([name_of_event.lower(), str(suffix)]) if suffix is not None \
            else name_of_event

    @abstractmethod
    def _initialize_all_conditional_events(self):
        """
        Generate full name of conditional events whereas the number of events can vary for different
        event handlers (i.e. custom events). This method should normally be implemented in the next
        inheriting base/meta class, e.g. `BaseConditionalSingle/DoubleEvents`.
        """

    def _initialize_conditional_event(self, name_of_event):
        """
        Any custom event must be initialized equally, meaning:
            * standardized uppercase enum `value`s:
                * single event have arbitrary name `name_of_event`
                * double events have arbitrary prefix `name_of_event` and standard suffix `_STARTED` or `_COMPLETED`
            * standardized lowercase enum `name`s identical to their `value`s
            * custom event is extended to `Events`, so any custom event created will be listed there
            * custom event is extended to `State` with key `self.conditional_state_attr`, so all `conditional_state_attr`s
              are listete there
        """
        # Create conditional event by extending `Events`
        # Event existence in `Events` has to be checked and avoided because multiple conditional events with
        # same event names and values cloud be created during model training. This can mess things up, no good!!!
        if not hasattr(Events, name_of_event.upper()) and True:
            # Create event by extending `Events`
            extend_enum(Events, name_of_event.upper(), name_of_event.lower())
        else:
            raise RuntimeError('`{}` already exists in `{}` and therefore cannot be created. '
                  'Change current `name_of_event = {}` to avoid name collision.'.format(
                getattr(Events, name_of_event.upper()), Events, name_of_event.lower()))
        # Extend `State.event_to_attr` with conditional event
        State.event_to_attr[getattr(Events, name_of_event.upper())] = self.conditional_state_attr.lower()
        return getattr(Events, name_of_event.upper())

    @abstractmethod
    def _reset(self, engine):
        """
        Resets reset counters `conditional_state_attr`(s) in `engine.state` to initial value, e.g. like this:
            .. code-block:: python

                setattr(engine.state, self.conditional_event_state_attr, 1)

        Args:
            engine: the engine to which the `self` is attached to
        """
        pass

    @abstractmethod
    def _check_condition_to_fire_conditional_completed_event(self, engine) -> bool:
        """
        Checks conditions and returns `True` to fire `self.conditional_event_completed_event` in the current step
        and returns `False` to hold back the firing.

        Args:
            engine: the engine to which the `self` is attached to
        """
        pass

    @abstractmethod
    def _update_condition_if_fired_conditional_completed_event(self, engine):
        """
        Updates the fire-event-condition parameter(s) after firing `self.conditional_completed_event`.
        E.g.

        Args:
            engine: the engine to which the `self` is attached to
        """
        pass

    @abstractmethod
    def _update_condition_if_not_fired_conditional_completed_event(self, engine):
        """
        Updates the fire-event-condition parameter(s) after NOT firing `self.conditional_completed_event` or
        `self.conditional_event`.

        Args:
            engine: the engine to which the `self` is attached to
        """
        pass
    
    def _on_started(self, engine):
        self._firing_enabled = True
        self._reset(engine)

    def _on_step_completed(self, engine):
        """Fires `self.periodic_completed_event` when `self.n_period_steps` is reached. Therefore this method
        is called with each `self.step_completed_event`."""
        if self._check_condition_to_fire_conditional_completed_event(engine) and self._firing_enabled:
            engine.fire_event(event_name=self.conditional_completed_event)
            self._update_condition_if_fired_conditional_completed_event(engine)
        else:
            self._update_condition_if_not_fired_conditional_completed_event(engine)

    def _on_completed(self, engine):
        self._firing_enabled = False

    def attach(self, engine):
        # Add event handlers
        # avoid adding the same event handler multiple times:
        if not engine.has_event_handler(handler=self._on_started, event_name=self.started_event):
            engine.add_event_handler(handler=self._on_started, event_name=self.started_event)
        else:
            raise RuntimeError('{}` event handler has already been added to engine `{}` at event `{}`. You '
                               'should not add multiple times.'.format(self._on_started,  engine, self.started_event))
        # avoid adding the same event handler multiple times:
        if not engine.has_event_handler(handler=self._on_step_completed, event_name=self.step_completed_event):
            engine.add_event_handler(self.step_completed_event, self._on_step_completed)
        else:
            raise RuntimeError('{}` event handler has already been added to engine `{}` at event `{}`. You '
                               'should not add multiple times.'.format(self._on_started, engine, self.started_event))


class BaseConditionalSingleEvents(with_metaclass(ABCMeta, BaseConditionalEvents)):
    """
    Base class for conditional event generator which fire a single event `self.conditional_completed_event`
    at each completed step if the implemented conditions are reached.
    Args:
        name_of_event (str): prefix of the event's full name, with `num_period_steps` being the suffix
        started_event (<enum, 'Events'>): trigger event to initialize any firing of conditional events. Before
            this `started_event` no event is fired at all.
        step_completed_event (<enum, 'Events'>): trigger event for firing `self.conditional_completed_event`
        completed_event (<enum, 'Events'>): trigger event to terminate any further firing of conditional events.
                                            After this event no more events are fired at all.
    """

    def _initialize_all_conditional_events(self):
        """Initialize the single-event `{NAME_OF_EVENT}`."""
        self.conditional_completed_event = self._initialize_conditional_event(name_of_event=self.conditional_state_attr)

    def attach(self, engine):
        engine.register_events(self.conditional_completed_event)
        super(BaseConditionalSingleEvents, self).attach(engine)


class BaseConditionalDoubleEvents(with_metaclass(ABCMeta, BaseConditionalEvents)):
    """
    Base class for conditional event generator which fire a two events (double event) at each started
    and completed step, namely `self.conditional_started_event` and `self.conditional_completed_event`,
     if the implemented conditions are reached.
    Args:
        name_of_event (str): prefix of the event's full name, with `num_period_steps` being the suffix
        started_event (<enum, 'Events'>): trigger event to initialize any firing of conditional events. Before
            this `started_event` no event is fired at all.
        step_started_event (<enum, 'Events'>): trigger event for firing `self.conditional_started_event`
        step_completed_event (<enum, 'Events'>): trigger event for firing `self.conditional_completed_event`
        completed_event (<enum, 'Events'>): trigger event to terminate any further firing of conditional events.
                                            After this event no more events are fired at all.
    """
    def __init__(self, name_of_event, started_event=Events.STARTED, step_started_event=Events.ITERATION_STARTED,
                 step_completed_event=Events.ITERATION_COMPLETED, completed_event=Events.EPOCH_COMPLETED):
        self.step_started_event = step_started_event
        self.conditional_started_event = None  # value initialized in `self.intialize_conditional_events()`
        self.conditional_completed_event = None  # value initialized in `self.intialize_conditional_events()`
        super(BaseConditionalDoubleEvents, self).__init__(name_of_event, started_event,
                                                          step_completed_event, completed_event)

    def _initialize_all_conditional_events(self):
        """Initialize the double-event of `{NAME_OF_EVENT}_STARTED` and `{NAME_OF_EVENT}_COMPLETED`."""
        conditional_names_of_events = [suffix.format(self.conditional_state_attr.lower())
                                               for suffix in ["{}_started", "{}_completed"]]
        conditional_event_attrs = ['conditional_started_event', 'conditional_completed_event']
        for event_attr, name_of_event in zip(conditional_event_attrs, conditional_names_of_events):
            setattr(self, event_attr, self._initialize_conditional_event(name_of_event))

    @abstractmethod
    def _check_condition_to_fire_started(self, engine) -> bool:
        """
        Checks conditions and returns `True` to fire `self.conditional_started_event` in the current step
        and returns `False` if conditions are not met.

        Args:
            engine: the engine to which the `self` is attached to
        """
        pass

    @abstractmethod
    def _update_condition_if_fired_started(self, engine):
        """
        Updates the fire-event-condition parameter(s) after firing `self.conditional_started_event`.

        Args:
            engine: the engine to which the `self` is attached to
        """
        pass

    def _update_condition_if_not_fired_started(self, engine):
        """
        Updates the fire-event-condition parameter(s) after NOT firing `self.conditional_started_event`.

        Args:
            engine: the engine to which the `self` is attached to
        """
        pass

    def _on_step_started(self, engine):
        if self._check_condition_to_fire_started(engine) and self._firing_enabled:
            engine.fire_event(event_name=self.conditional_started_event)
            self._update_condition_if_fired_started(engine)
        else:
            self._update_condition_if_not_fired_started(engine)

    def attach(self, engine):
        engine.register_events(self.conditional_started_event, self.conditional_completed_event)
        # avoid adding the same event handler multiple times:
        if not engine.has_event_handler(handler=self._on_step_started, event_name=self.step_started_event):
            engine.add_event_handler(self.step_started_event, self._on_step_started)
        else:
            raise RuntimeError('{}` event handler has already been added to engine `{}` at event `{}`. '
                               'You should not add multiple times.'.format(self._on_step_started, engine,
                                                                           self.step_started_event))
        super(BaseConditionalDoubleEvents, self).attach(engine)
```

#### `step_periodic_events.py`
```python
from ignite.engine import Events, State
from ignite_contrib.handlers import BaseConditionalDoubleEvents, BaseConditionalSingleEvents, LoggerMixin


class StepPeriodicEvents(BaseConditionalSingleEvents):
    """
    Fires the custom event `self.conditional_completed_event` on every `num_period_steps`-th occurrence
    of `step_completed_event`.

    Args:
        num_period_steps (int): number of `step_started_events` to occur between two sequential events
                                `self.conditional_started_event` or `self.conditional_completed_event`
        name_of_event (str): prefix of the event's full name, with `num_period_steps` being the suffix
        started_event (<enum, 'Events'>): trigger event to initialize any firing of conditional events. Before
                                          this event no event is fired at all.
        step_started_event (<enum, 'Events'>): trigger event for firing `self.conditional_started_event`
        step_completed_event (<enum, 'Events'>): trigger event for firing `self.conditional_completed_event`
        completed_event (<enum, 'Events'>): trigger event to terminate any further firing of conditional events.
                                            After this event no more events are fired at all.
    """
    def __init__(self, n_period_steps, name_of_event=None, started_event=Events.STARTED,
                 step_completed_event=Events.ITERATION_COMPLETED, completed_event=Events.COMPLETED):
        self.n_period_steps = n_period_steps
        # `self.conditional_event_state_attr` is also returned by @property `self.name_of_event`
        self.conditional_event_state_attr = self._extract_event_state_attr(name_of_event=name_of_event,
                                                          suffix=self.n_period_steps,
                                                          event_name=step_completed_event)
        super(StepPeriodicEvents, self).__init__(name_of_event=self.conditional_event_state_attr,
                                                 started_event=started_event,
                                                 step_completed_event=step_completed_event,
                                                 completed_event=completed_event)

    def _reset(self, engine):
        # Initialization/reset value set to `1` instead of `0` representing the number of the current iteration
        setattr(engine.state, self.conditional_event_state_attr, 1)

    def _check_condition_to_fire_conditional_completed_event(self, engine):
        return getattr(engine.state, self.step_event_state_attr) % self.n_period_steps == 0

    def _update_condition_if_fired_conditional_completed_event(self, engine):
        """Updates only the fired-conditional-event-counter of `engine.state` but not the condition."""
        setattr(engine.state, self.conditional_event_state_attr, getattr(engine.state, self.conditional_event_state_attr) + 1)

    def _update_condition_if_not_fired_conditional_completed_event(self, engine):
        pass


class StepPeriodicDoubleEvents(BaseConditionalDoubleEvents):
    """
    Fires the custom event `self.conditional_started_event` at the be beginning and the custom event
    `self.conditional_completed_event` at the end of an interval. The interval is defined by the number
    'num_period_steps` of `step_completed_event`s that have to occur for triggering a fire-event.

    Args:
        num_period_steps (int): number of `step_started_events` to occur between two sequential events
                                `self.conditional_started_event` or `self.conditional_completed_event`
        name_of_event (str): prefix of the event's full name, with `num_period_steps` being the suffix
        started_event (<enum, 'Events'>): trigger event to initialize any firing of conditional events. Before
                                          this event no event is fired at all.
        step_started_event (<enum, 'Events'>): trigger event for firing `self.conditional_started_event`
        step_completed_event (<enum, 'Events'>): trigger event for firing `self.conditional_completed_event`
        completed_event (<enum, 'Events'>): trigger event to terminate any further firing of conditional events.
                                            After this event no more events are fired at all.
    """
    def __init__(self, n_period_steps, name_of_event=None, started_event=Events.STARTED,
                 step_started_event=Events.ITERATION_STARTED, step_completed_event=Events.ITERATION_COMPLETED,
                 completed_event=Events.COMPLETED):
        self.n_period_steps = n_period_steps
        # `self.conditional_event_state_attr` is also returned by @property `self.name_of_event`
        self.conditional_event_state_attr = self._extract_event_state_attr(name_of_event=name_of_event,
                                                                     suffix=self.n_period_steps,
                                                                     event_name=step_completed_event)
        super(StepPeriodicDoubleEvents, self).__init__(name_of_event=self.conditional_event_state_attr,
                                                       started_event=started_event,
                                                       step_started_event=step_started_event,
                                                       step_completed_event=step_completed_event,
                                                       completed_event=completed_event)

    def _reset(self, engine):
        # Initialization/reset value set to `1` instead of `0` representing the number of the current iteration
        setattr(engine.state, self.conditional_event_state_attr, 1)

    def _check_condition_to_fire_started(self, engine):
        return getattr(engine.state, self.step_event_state_attr) % self.n_period_steps == 1

    def _update_condition_if_fired_started(self, engine):
        """
        Updates only the fired-conditional-event-counter of `engine.state` but not the condition. Only the
        fired `stelf.conditional_started_event`s are counted.
        """
        setattr(engine.state, self.conditional_event_state_attr, getattr(engine.state, self.conditional_event_state_attr) + 1)

    def _update_condition_if_not_fired_started(self, engine):
        pass

    def _check_condition_to_fire_conditional_completed_event(self, engine):
        return getattr(engine.state, self.step_event_state_attr) % self.n_period_steps == 0

    def _update_condition_if_fired_conditional_completed_event(self, engine):
        pass

    def _update_condition_if_not_fired_conditional_completed_event(self, engine):
        pass


if __name__ == '__main__':

    custom_periodic_event = StepPeriodicDoubleEvents(n_period_steps=10)

    named_custom_periodic_event = StepPeriodicDoubleEvents(n_period_steps=10, name_of_event='ten_iterations')

    print('\nCustomized events appear in `Events` identified by idividual (if provided) or standard name:')
    for event_name, event in Events.__members__.items():
        print('{}: {}'.format(event_name, event))
```
Run this module on its own gives the `Events` holding all current/initiated custom events:
```python
Customized events appear in `Events` identified by idividual (if provided) or standard name:
EPOCH_STARTED: Events.EPOCH_STARTED
EPOCH_COMPLETED: Events.EPOCH_COMPLETED
STARTED: Events.STARTED
COMPLETED: Events.COMPLETED
ITERATION_STARTED: Events.ITERATION_STARTED
ITERATION_COMPLETED: Events.ITERATION_COMPLETED
EXCEPTION_RAISED: Events.EXCEPTION_RAISED
ITERATION_10_STARTED: Events.ITERATION_10_STARTED
ITERATION_10_COMPLETED: Events.ITERATION_10_COMPLETED
TEN_ITERATIONS_10_STARTED: Events.TEN_ITERATIONS_10_STARTED
TEN_ITERATIONS_10_COMPLETED: Events.TEN_ITERATIONS_10_COMPLETED
```

#### `logger_mixin.py` (not part of the issue, just for running this code)
```python
from abc import ABCMeta, abstractmethod, abstractproperty
from ignite._six import with_metaclass
import logging

# ======================================================================================================================
### GLOBAL LOGGER PARAMETERS ###
# ======================================================================================================================
# Change one of these parameters to change them globally
GLOBAL_LOGGING_LEVEL = logging.INFO
GLOBAL_HANDLER_CLASS = logging.StreamHandler
GLOBAL_LOGGING_FORMAT = '[%(levelname)s:%(name)s.%(funcName)s():%(lineno)s] %(message)s'
GLOBAL_MULTIPROCESSING_LOGGING_FORMAT = '[%(processName)s/%(levelname)s:%(name)s.%(funcName)s():%(lineno)s] %(message)s'


def update_formatter_of_handler_if_none(hdlr, fmt):
    if not hdlr.formatter:
        global_formatter = logging.Formatter(fmt)
        hdlr.setFormatter(global_formatter)
    return hdlr


def replace_handler(logger, hdlr):
    """
    Replaces the last handler for regular loggers and the first hander for multiprocessing loggers in
    `logger.handlers` with `the handler provided in argument`hdlr`.
    If `hdlr` has no formatter, then a formatter with `fmt` is added.

    Args:
        hdlr (logging.Handler): handler to replace the current one
    """
    if not isinstance(hdlr, logging.Handler):
        raise TypeError('Argument `hdlr` should be of type `logging.Handler`, but given {}.'.format(
            type(hdlr)))
    if 'multiprocessing' in logger.name:
        hdlr = update_formatter_of_handler_if_none(hdlr, fmt=GLOBAL_MULTIPROCESSING_LOGGING_FORMAT)
        # `multiprocessing` is limited to one handler secured by a lock against simultaneous process calls
        logging.getLogger(name='multiprocessing').handlers[0] = hdlr
    else:
        hdlr = update_formatter_of_handler_if_none(hdlr, fmt=GLOBAL_LOGGING_FORMAT)
        to_be_replaced_handler = logger.handlers[-1]
        if to_be_replaced_handler == hdlr:
            logger.warn('The logger`s handler is being repalced by itself provided by argument `repalcing_handler`.'
                        '`MixinLogger.get_logger()` needs to be called only once this the `hdlr` to'
                        'change the local handler permanently.')

        logger.removeHandler(hdlr=to_be_replaced_handler)
        logger.addHandler(hdlr=hdlr)


def get_logger(name):
    """
    Get logger with `name` from `logging` and set it to global parameters.

    Args:
        name (str): logger name
    :return:
    """
    # If logger already exists
    if name in logging.root.manager.loggerDict.keys():
        return logging.getLogger(name=name)
    # If logger does not exist yet and needs to be created
    logger = logging.getLogger(name=name)
    logger.setLevel(level=GLOBAL_LOGGING_LEVEL)
    formatter = logging.Formatter(GLOBAL_LOGGING_FORMAT)
    handler = GLOBAL_HANDLER_CLASS()
    handler.setFormatter(fmt=formatter)
    logger.addHandler(hdlr=handler)
    return logger


# Multiprocessing logger
# For logging.DEBUG level change level in train()
def get_multiprocessing_logger(name):
    # In the name `multiprocessing` is separated on purpose by `.` as `logging` build hierachy on this seperator
    multiprocessing_name = 'multiprocessing' + '.' + name
    # If mp_logger already exists
    if multiprocessing_name in logging.root.manager.loggerDict.keys():
        return logging.getLogger(name=multiprocessing_name)
    # If mp_logger does not exist yet and needs to be created
    mp_logger = logging.getLogger(name=multiprocessing_name)
    mp_logger.setLevel(level=GLOBAL_LOGGING_LEVEL)
    mp_formatter = logging.Formatter(fmt=GLOBAL_MULTIPROCESSING_LOGGING_FORMAT)
    mp_handler = GLOBAL_HANDLER_CLASS()
    mp_handler.setFormatter(fmt=mp_formatter)
    # Override the automatically generated standard-`multiprocessing`-handler with customized handler `mp_handler`
    if len(logging.getLogger(name='multiprocessing').handlers) == 0:
        logging.getLogger(name='multiprocessing').handlers.append(mp_handler)
    else:
        logging.getLogger(name='multiprocessing').handlers[0] = mp_handler
    return mp_logger


class BaseLoggerMixin(object):
    @abstractproperty
    def _logger(self):
        pass

    @abstractmethod
    def _logger_replace_handler(self, hdlr):
        pass

    @abstractmethod
    def _logger_reformat_all_handlers(self, format_string):
        pass


class LoggerMixin(BaseLoggerMixin):
    """
    Mixin to provide any class with a standardized logger `self._logger` property with standardized logger name.
    """
    @property
    def _logger(self):
        return get_logger(name=__file__ + "." + self.__class__.__name__.__str__())

    def _logger_replace_handler(self, hdlr):
        """
        Replaces the last (normally the only) handler in `logger.handlers` by `the handler provided in argument hdlr`.
        If `hdlr` has no formatter, then a formater with `GLOBAL_LOGGING_FORMAT` or
        `GLOBAL_MULTIPROCESSING_LOGGING_FORMAT` is added.

        Args:
             hdlr (logging.Handler): a instantiated handler to replace the current handler
         """
        replace_handler(logger=self._logger, hdlr=hdlr)

    def _logger_reformat_all_handlers(self, format_string):
        for hdlr in self._logger.handlers:
            hdlr.setFormatter(fmt=logging.Formatter(fmt=format_string))


class MultiprocessingLoggerMixin(BaseLoggerMixin):
    """
    Mixin to provide any class with a `mulitprocessing` standardized logger `self._logger` property with standardized
    logger name.
    """
    @property
    def _logger(self):
        return get_multiprocessing_logger(name=__file__ + "." + self.__class__.__name__.__str__())

    def _logger_replace_handler(self, hdlr):
       replace_handler(logger=self._logger, hdlr=hdlr)

    def _logger_reformat_all_handlers(self, format_string):
        logging.getLogger(name='multiprocessing').handlers[0].setFormatter(
            fmt=logging.Formatter(fmt=format_string))


if __name__ == '__main__':

    class DummyClassForLogging(LoggerMixin, object):
        pass


    class DummyClassForMultiprocessingLogging(MultiprocessingLoggerMixin, object):
        pass

    for dummy in [DummyClassForLogging(), DummyClassForMultiprocessingLogging()]:
        dummy._logger.critical('If you can read this critical log, it`s correct.')
        dummy._logger.error('If you can read this error log, it`s correct.')
        dummy._logger.info('This is a logger test. If this info log is displayed on stdout, it`s correct.')
        dummy._logger.warn('If you can read this warning log, it`s correct.')
        dummy._logger.debug('If you can read this debug log, something is wrong!')

        # Change the logger level of the single instance to debug it
        dummy._logger.setLevel(level=logging.DEBUG)
        dummy._logger.debug('If you can read this debug log NOW after changing the logger level, it`s correct.')

        # Change the logger handler of the single instance to debug it
        filelogger_handler = logging.FileHandler(filename='please_delete_me.log')
        dummy._logger_replace_handler(hdlr=filelogger_handler)
        dummy._logger.debug('If this debug log is logged to the file, it`s correct.')
        dummy._logger.info('If this info log is also logged to the file, it`s wrong!')
        dummy._logger_replace_handler(hdlr=filelogger_handler)
        dummy._logger.debug('If this is logged together with a WARNING to the file, it`s correct.')
        # Change the logger handler back again
        dummy._logger_replace_handler(hdlr=logging.StreamHandler())
        dummy._logger.info('If this info log is stdoutted again, it`s correct.')
        # Change the format
        changed_format = '!!!CHANGED FORAMT!!! [%(levelname)s:%(name)s.%(funcName)s():%(lineno)s] %(message)s'
        dummy._logger_reformat_all_handlers(format_string=changed_format)
        dummy._logger.info('If this info log message has changed format, it`s correct.')
```

Discussion:
Bibonaut: @vfdev-5 👍 👍  Thanks, that you did the whole implementation!!  Again, having not much time at the moment due to some deadlines :/ 

I had a look at the code and it seems to solve all issues we discussed in #624. 🎊  It's very nice, that you did not have to introduce changes to the basic events from `Events` class except from subclassing from CallableEvents.

 

Patch:
diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst
index 589a645a..ffb04959 100644
--- a/docs/source/concepts.rst
+++ b/docs/source/concepts.rst
@@ -116,6 +116,38 @@ reference returned by :meth:`~ignite.engine.Engine.add_event_handler`. This can
 
     trainer.run(train_loader, max_epochs=100)
 
+
+Event handlers can be also configured to be called with a user pattern: every n-th events, once or using a custom
+event filtering function:
+
+.. code-block:: python
+
+    model = ...
+    train_loader, validation_loader, test_loader = ...
+
+    trainer = create_supervised_trainer(model, optimizer, loss)
+
+    @trainer.on(Events.ITERATION_COMPLETED(every=50))
+    def log_training_loss_every_50_iterations(engine):
+        print("{} / {} : {} - loss: {:.2f}"
+              .format(engine.state.epoch, engine.state.max_epochs, engine.state.iteration, engine.state.output))
+
+    @trainer.on(Events.EPOCH_STARTED(once=25))
+    def do_something_once_on_25_epoch(engine):
+        # do something
+
+    def custom_event_filter(engine, event):
+        if event in [1, 2, 5, 10, 50, 100]:
+            return True
+        return False
+
+    @engine.on(Events.ITERATION_STARTED(event_filter=custom_event_filter))
+    def call_on_special_event(engine):
+         # do something on 1, 2, 5, 10, 50, 100 iterations
+
+    trainer.run(train_loader, max_epochs=100)
+
+
 .. Note ::
 
    User can also register custom events with :meth:`~ignite.engine.Engine.register_events`, attach handlers and fire custom events
diff --git a/examples/contrib/cifar10/main.py b/examples/contrib/cifar10/main.py
index c4bffe83..36cd1004 100644
--- a/examples/contrib/cifar10/main.py
+++ b/examples/contrib/cifar10/main.py
@@ -143,14 +143,13 @@ def run(output_path, config):
     evaluator = create_supervised_evaluator(model, metrics=metrics, device=device, non_blocking=True)
     train_evaluator = create_supervised_evaluator(model, metrics=metrics, device=device, non_blocking=True)
 
-    def run_validation(engine, val_interval):
-        if engine.state.epoch % val_interval == 0:
-            torch.cuda.synchronize()
-            train_evaluator.run(train_labelled_loader)
-            evaluator.run(test_loader)
-
-    trainer.add_event_handler(Events.EPOCH_STARTED, run_validation, val_interval=3)
-    trainer.add_event_handler(Events.COMPLETED, run_validation, val_interval=1)
+    def run_validation(engine):
+        torch.cuda.synchronize()
+        train_evaluator.run(train_labelled_loader)
+        evaluator.run(test_loader)
+
+    trainer.add_event_handler(Events.EPOCH_STARTED(every=3), run_validation)
+    trainer.add_event_handler(Events.COMPLETED, run_validation)
 
     if rank == 0:
         if config['display_iters']:
diff --git a/examples/contrib/mnist/mnist_with_tensorboard_logger.py b/examples/contrib/mnist/mnist_with_tensorboard_logger.py
index bcc15cfb..ad362ac3 100644
--- a/examples/contrib/mnist/mnist_with_tensorboard_logger.py
+++ b/examples/contrib/mnist/mnist_with_tensorboard_logger.py
@@ -109,7 +109,7 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_dir):
                      log_handler=OutputHandler(tag="training",
                                                output_transform=lambda loss: {'batchloss': loss},
                                                metric_names='all'),
-                     event_name=Events.ITERATION_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     tb_logger.attach(train_evaluator,
                      log_handler=OutputHandler(tag="training",
@@ -125,23 +125,23 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_dir):
 
     tb_logger.attach(trainer,
                      log_handler=OptimizerParamsHandler(optimizer),
-                     event_name=Events.ITERATION_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     tb_logger.attach(trainer,
                      log_handler=WeightsScalarHandler(model),
-                     event_name=Events.ITERATION_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     tb_logger.attach(trainer,
                      log_handler=WeightsHistHandler(model),
-                     event_name=Events.EPOCH_COMPLETED)
+                     event_name=Events.EPOCH_COMPLETED(every=100))
 
     tb_logger.attach(trainer,
                      log_handler=GradsScalarHandler(model),
-                     event_name=Events.ITERATION_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     tb_logger.attach(trainer,
                      log_handler=GradsHistHandler(model),
-                     event_name=Events.EPOCH_COMPLETED)
+                     event_name=Events.EPOCH_COMPLETED(every=100))
 
     # kick everything off
     trainer.run(train_loader, max_epochs=epochs)
diff --git a/examples/contrib/mnist/mnist_with_visdom_logger.py b/examples/contrib/mnist/mnist_with_visdom_logger.py
index 94f0225a..dc27c75b 100644
--- a/examples/contrib/mnist/mnist_with_visdom_logger.py
+++ b/examples/contrib/mnist/mnist_with_visdom_logger.py
@@ -33,7 +33,6 @@ from torchvision.transforms import Compose, ToTensor, Normalize
 from ignite.engine import Events, create_supervised_trainer, create_supervised_evaluator
 from ignite.metrics import Accuracy, Loss
 from ignite.contrib.handlers.visdom_logger import *
-from ignite.contrib.handlers import CustomPeriodicEvent
 
 
 class Net(nn.Module):
@@ -78,9 +77,6 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_dir):
     criterion = nn.CrossEntropyLoss()
     trainer = create_supervised_trainer(model, optimizer, criterion, device=device)
 
-    cpe = CustomPeriodicEvent(n_iterations=100)
-    cpe.attach(trainer)
-
     metrics = {
         'accuracy': Accuracy(),
         'loss': Loss(criterion)
@@ -98,7 +94,7 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_dir):
 
     vd_logger.attach(trainer,
                      log_handler=OutputHandler(tag="training", output_transform=lambda loss: {'batchloss': loss}),
-                     event_name=cpe.Events.ITERATIONS_100_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     vd_logger.attach(train_evaluator,
                      log_handler=OutputHandler(tag="training",
@@ -114,15 +110,15 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_dir):
 
     vd_logger.attach(trainer,
                      log_handler=OptimizerParamsHandler(optimizer),
-                     event_name=cpe.Events.ITERATIONS_100_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     vd_logger.attach(trainer,
                      log_handler=WeightsScalarHandler(model),
-                     event_name=cpe.Events.ITERATIONS_100_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     vd_logger.attach(trainer,
                      log_handler=GradsScalarHandler(model),
-                     event_name=cpe.Events.ITERATIONS_100_COMPLETED)
+                     event_name=Events.ITERATION_COMPLETED(every=100))
 
     # kick everything off
     trainer.run(train_loader, max_epochs=epochs)
diff --git a/examples/gan/dcgan.py b/examples/gan/dcgan.py
index 1af5b930..2887e0cc 100644
--- a/examples/gan/dcgan.py
+++ b/examples/gan/dcgan.py
@@ -321,27 +321,26 @@ def main(dataset, dataroot,
     pbar = ProgressBar()
     pbar.attach(trainer, metric_names=monitoring_metrics)
 
-    @trainer.on(Events.ITERATION_COMPLETED)
+    @trainer.on(Events.ITERATION_COMPLETED(every=PRINT_FREQ))
     def print_logs(engine):
-        if (engine.state.iteration - 1) % PRINT_FREQ == 0:
-            fname = os.path.join(output_dir, LOGS_FNAME)
-            columns = ["iteration", ] + list(engine.state.metrics.keys())
-            values = [str(engine.state.iteration), ] + \
-                     [str(round(value, 5)) for value in engine.state.metrics.values()]
-
-            with open(fname, 'a') as f:
-                if f.tell() == 0:
-                    print('\t'.join(columns), file=f)
-                print('\t'.join(values), file=f)
-
-            message = '[{epoch}/{max_epoch}][{i}/{max_i}]'.format(epoch=engine.state.epoch,
-                                                                  max_epoch=epochs,
-                                                                  i=(engine.state.iteration % len(loader)),
-                                                                  max_i=len(loader))
-            for name, value in zip(columns, values):
-                message += ' | {name}: {value}'.format(name=name, value=value)
-
-            pbar.log_message(message)
+        fname = os.path.join(output_dir, LOGS_FNAME)
+        columns = ["iteration", ] + list(engine.state.metrics.keys())
+        values = [str(engine.state.iteration), ] + \
+                 [str(round(value, 5)) for value in engine.state.metrics.values()]
+
+        with open(fname, 'a') as f:
+            if f.tell() == 0:
+                print('\t'.join(columns), file=f)
+            print('\t'.join(values), file=f)
+
+        message = '[{epoch}/{max_epoch}][{i}/{max_i}]'.format(epoch=engine.state.epoch,
+                                                              max_epoch=epochs,
+                                                              i=(engine.state.iteration % len(loader)),
+                                                              max_i=len(loader))
+        for name, value in zip(columns, values):
+            message += ' | {name}: {value}'.format(name=name, value=value)
+
+        pbar.log_message(message)
 
     # adding handlers using `trainer.on` decorator API
     @trainer.on(Events.EPOCH_COMPLETED)
diff --git a/examples/mnist/mnist.py b/examples/mnist/mnist.py
index 00248240..7005da15 100644
--- a/examples/mnist/mnist.py
+++ b/examples/mnist/mnist.py
@@ -65,13 +65,10 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_interval):
         desc=desc.format(0)
     )
 
-    @trainer.on(Events.ITERATION_COMPLETED)
+    @trainer.on(Events.ITERATION_COMPLETED(every=log_interval))
     def log_training_loss(engine):
-        iter = (engine.state.iteration - 1) % len(train_loader) + 1
-
-        if iter % log_interval == 0:
-            pbar.desc = desc.format(engine.state.output)
-            pbar.update(log_interval)
+        pbar.desc = desc.format(engine.state.output)
+        pbar.update(log_interval)
 
     @trainer.on(Events.EPOCH_COMPLETED)
     def log_training_results(engine):
diff --git a/examples/mnist/mnist_with_tensorboardx.py b/examples/mnist/mnist_with_tensorboardx.py
index 9957b036..1a5a77ee 100644
--- a/examples/mnist/mnist_with_tensorboardx.py
+++ b/examples/mnist/mnist_with_tensorboardx.py
@@ -90,13 +90,11 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_interval, lo
                                                      'nll': Loss(F.nll_loss)},
                                             device=device)
 
-    @trainer.on(Events.ITERATION_COMPLETED)
+    @trainer.on(Events.ITERATION_COMPLETED(every=log_interval))
     def log_training_loss(engine):
-        iter = (engine.state.iteration - 1) % len(train_loader) + 1
-        if iter % log_interval == 0:
-            print("Epoch[{}] Iteration[{}/{}] Loss: {:.2f}"
-                  "".format(engine.state.epoch, iter, len(train_loader), engine.state.output))
-            writer.add_scalar("training/loss", engine.state.output, engine.state.iteration)
+        print("Epoch[{}] Iteration[{}/{}] Loss: {:.2f}"
+              "".format(engine.state.epoch, iter, len(train_loader), engine.state.output))
+        writer.add_scalar("training/loss", engine.state.output, engine.state.iteration)
 
     @trainer.on(Events.EPOCH_COMPLETED)
     def log_training_results(engine):
diff --git a/examples/mnist/mnist_with_visdom.py b/examples/mnist/mnist_with_visdom.py
index 01b3ccbc..3271f0c7 100644
--- a/examples/mnist/mnist_with_visdom.py
+++ b/examples/mnist/mnist_with_visdom.py
@@ -78,15 +78,13 @@ def run(train_batch_size, val_batch_size, epochs, lr, momentum, log_interval):
     val_avg_loss_window = create_plot_window(vis, '#Epochs', 'Loss', 'Validation Average Loss')
     val_avg_accuracy_window = create_plot_window(vis, '#Epochs', 'Accuracy', 'Validation Average Accuracy')
 
-    @trainer.on(Events.ITERATION_COMPLETED)
+    @trainer.on(Events.ITERATION_COMPLETED(every=log_interval))
     def log_training_loss(engine):
-        iter = (engine.state.iteration - 1) % len(train_loader) + 1
-        if iter % log_interval == 0:
-            print("Epoch[{}] Iteration[{}/{}] Loss: {:.2f}"
-                  "".format(engine.state.epoch, iter, len(train_loader), engine.state.output))
-            vis.line(X=np.array([engine.state.iteration]),
-                     Y=np.array([engine.state.output]),
-                     update='append', win=train_loss_window)
+        print("Epoch[{}] Iteration[{}/{}] Loss: {:.2f}"
+              "".format(engine.state.epoch, iter, len(train_loader), engine.state.output))
+        vis.line(X=np.array([engine.state.iteration]),
+                 Y=np.array([engine.state.output]),
+                 update='append', win=train_loss_window)
 
     @trainer.on(Events.EPOCH_COMPLETED)
     def log_training_results(engine):
diff --git a/examples/reinforcement_learning/actor_critic.py b/examples/reinforcement_learning/actor_critic.py
index 750ac724..f4c36e47 100644
--- a/examples/reinforcement_learning/actor_critic.py
+++ b/examples/reinforcement_learning/actor_critic.py
@@ -109,12 +109,11 @@ def main(env, args):
         engine.state.running_reward = engine.state.running_reward * 0.99 + t * 0.01
         finish_episode(model, optimizer, args.gamma, eps)
 
-    @trainer.on(EPISODE_COMPLETED)
+    @trainer.on(EPISODE_COMPLETED(every=args.log_interval))
     def log_episode(engine):
         i_episode = engine.state.epoch
-        if i_episode % args.log_interval == 0:
-            print('Episode {}\tLast length: {:5d}\tAverage length: {:.2f}'.format(
-                i_episode, engine.state.timestep, engine.state.running_reward))
+        print('Episode {}\tLast length: {:5d}\tAverage length: {:.2f}'.format(
+            i_episode, engine.state.timestep, engine.state.running_reward))
 
     @trainer.on(EPISODE_COMPLETED)
     def should_finish_training(engine):
diff --git a/examples/reinforcement_learning/reinforce.py b/examples/reinforcement_learning/reinforce.py
index 46201a3b..da2f6e7f 100644
--- a/examples/reinforcement_learning/reinforce.py
+++ b/examples/reinforcement_learning/reinforce.py
@@ -99,12 +99,11 @@ def main(env, args):
         engine.state.running_reward = engine.state.running_reward * 0.99 + t * 0.01
         finish_episode(model, optimizer, args.gamma, eps)
 
-    @trainer.on(EPISODE_COMPLETED)
+    @trainer.on(EPISODE_COMPLETED(every=args.log_interval))
     def log_episode(engine):
         i_episode = engine.state.epoch
-        if i_episode % args.log_interval == 0:
-            print('Episode {}\tLast length: {:5d}\tAverage length: {:.2f}'.format(
-                i_episode, engine.state.timestep, engine.state.running_reward))
+        print('Episode {}\tLast length: {:5d}\tAverage length: {:.2f}'.format(
+            i_episode, engine.state.timestep, engine.state.running_reward))
 
     @trainer.on(EPISODE_COMPLETED)
     def should_finish_training(engine):
diff --git a/ignite/contrib/handlers/base_logger.py b/ignite/contrib/handlers/base_logger.py
index eb12ccdf..8bcc9ddd 100644
--- a/ignite/contrib/handlers/base_logger.py
+++ b/ignite/contrib/handlers/base_logger.py
@@ -5,12 +5,13 @@ import warnings
 import torch
 
 from ignite.engine import State, Engine
+from ignite.engine.engine import EventWithFilter
 from ignite._six import with_metaclass
 
 
 class BaseLogger(object):
     """
-    Base logger handler. See implementations: TensorboardLogger, VisdomLogger, PolyaxonLogger
+    Base logger handler. See implementations: TensorboardLogger, VisdomLogger, PolyaxonLogger, MLflowLogger, ...
 
     """
     def attach(self, engine, log_handler, event_name):
@@ -23,10 +24,14 @@ class BaseLogger(object):
                 or any `event_name` added by :meth:`~ignite.engine.Engine.register_events`.
 
         """
-        if event_name not in State.event_to_attr:
-            raise RuntimeError("Unknown event name '{}'".format(event_name))
+        name = event_name
+        if isinstance(event_name, EventWithFilter):
+            name = event_name.event
 
-        engine.add_event_handler(event_name, log_handler, self, event_name)
+        if name not in State.event_to_attr:
+            raise RuntimeError("Unknown event name '{}'".format(name))
+
+        engine.add_event_handler(event_name, log_handler, self, name)
 
     def __enter__(self):
         return self
diff --git a/ignite/contrib/handlers/tqdm_logger.py b/ignite/contrib/handlers/tqdm_logger.py
index 9e18f420..134447e4 100644
--- a/ignite/contrib/handlers/tqdm_logger.py
+++ b/ignite/contrib/handlers/tqdm_logger.py
@@ -4,7 +4,7 @@ import warnings
 import torch
 
 from ignite.engine import Events
-
+from ignite.engine.engine import EventWithFilter
 from ignite.contrib.handlers.base_logger import BaseLogger, BaseOutputHandler
 
 
@@ -79,7 +79,7 @@ class ProgressBar(BaseLogger):
 
     """
 
-    events_order = [
+    _events_order = [
         Events.STARTED,
         Events.EPOCH_STARTED,
         Events.ITERATION_STARTED,
@@ -90,7 +90,6 @@ class ProgressBar(BaseLogger):
 
     def __init__(self, persist=False,
                  bar_format='{desc}[{n_fmt}/{total_fmt}] {percentage:3.0f}%|{bar}{postfix} [{elapsed}<{remaining}]',
-
                  **tqdm_kwargs):
 
         try:
@@ -120,8 +119,12 @@ class ProgressBar(BaseLogger):
 
     @staticmethod
     def _compare_lt(event1, event2):
-        i1 = ProgressBar.events_order.index(event1)
-        i2 = ProgressBar.events_order.index(event2)
+        if isinstance(event1, EventWithFilter):
+            event1 = event1.event
+        if isinstance(event2, EventWithFilter):
+            event2 = event2.event
+        i1 = ProgressBar._events_order.index(event1)
+        i2 = ProgressBar._events_order.index(event2)
         return i1 < i2
 
     @staticmethod
@@ -158,8 +161,11 @@ class ProgressBar(BaseLogger):
         """
         desc = self.tqdm_kwargs.get("desc", "Epoch")
 
-        if not (event_name in Events and closing_event_name in Events):
-            raise ValueError("Logging and closing events should be only ignite.engine.Events")
+        if not isinstance(event_name, (Events, EventWithFilter)):
+            raise ValueError("Logging event should be only `ignite.engine.Events`")
+
+        if isinstance(closing_event_name, EventWithFilter):
+            raise ValueError("Closing event should not use any event filter")
 
         if not self._compare_lt(event_name, closing_event_name):
             raise ValueError("Logging event {} should be called before closing event {}"
@@ -204,6 +210,8 @@ class _OutputHandler(BaseOutputHandler):
 
     @staticmethod
     def get_max_number_events(event_name, engine):
+        if isinstance(event_name, EventWithFilter):
+            event_name = event_name.event
         if event_name in (Events.ITERATION_STARTED, Events.ITERATION_COMPLETED):
             return len(engine.state.dataloader)
         if event_name in (Events.EPOCH_STARTED, Events.EPOCH_COMPLETED):
@@ -212,8 +220,9 @@ class _OutputHandler(BaseOutputHandler):
 
     def __call__(self, engine, logger, event_name):
 
+        pbar_total = self.get_max_number_events(self.event_name, engine)
         if logger.pbar is None:
-            logger._reset(pbar_total=self.get_max_number_events(self.event_name, engine))
+            logger._reset(pbar_total=pbar_total)
 
         desc = self.tag
         max_num_of_closing_events = self.get_max_number_events(self.closing_event_name, engine)
@@ -242,4 +251,6 @@ class _OutputHandler(BaseOutputHandler):
         if rendered_metrics:
             logger.pbar.set_postfix(**rendered_metrics)
 
-        logger.pbar.update()
+        global_step = engine.state.get_event_attrib_value(event_name)
+        global_step = (global_step - 1) % pbar_total + 1
+        logger.pbar.update(global_step - logger.pbar.n)
diff --git a/ignite/engine/engine.py b/ignite/engine/engine.py
index a49c8723..64fb4b47 100644
--- a/ignite/engine/engine.py
+++ b/ignite/engine/engine.py
@@ -7,14 +7,117 @@ from enum import Enum
 import weakref
 import numbers
 
-
 from ignite._utils import _to_hours_mins_secs
 
 IS_PYTHON2 = sys.version_info[0] < 3
 
 
-class Events(Enum):
-    """Events that are fired by the :class:`~ignite.engine.Engine` during execution."""
+class EventWithFilter(object):
+
+    def __init__(self, event, filter):
+        if not callable(filter):
+            raise TypeError("Argument filter should be callable")
+        self.event = event
+        self.filter = filter
+
+    def __str__(self):
+        return "<%s event=%s, filter=%r>" % (self.__class__.__name__, self.event, self.filter)
+
+
+class CallableEvents(object):
+    """Base class for Events implementing call operator and storing event filter. This class should be inherited
+    for any custom events with event filtering feature:
+
+    .. code-block:: python
+
+        from ignite.engine.engine import CallableEvents
+
+        class CustomEvents(CallableEvents, Enum):
+            TEST_EVENT = "test_event"
+
+        engine = ...
+        engine.register_events(*CustomEvents, event_to_attr={CustomEvents.TEST_EVENT: "test_event"})
+
+        @engine.on(CustomEvents.TEST_EVENT(every=5))
+        def call_on_test_event_every(engine):
+            # do something
+
+    """
+    def __call__(self, event_filter=None, every=None, once=None):
+
+        if not((event_filter is not None) ^ (every is not None) ^ (once is not None)):
+            raise ValueError("Only one of the input arguments should be specified")
+
+        if (event_filter is not None) and not callable(event_filter):
+            raise TypeError("Argument event_filter should be a callable")
+
+        if (every is not None) and not (isinstance(every, numbers.Integral) and every > 1):
+            raise ValueError("Argument every should be integer and greater than one")
+
+        if (once is not None) and not (isinstance(once, numbers.Integral) and once > 0):
+            raise ValueError("Argument every should be integer and positive")
+
+        if every is not None:
+            event_filter = CallableEvents.every_event_filter(every)
+
+        if once is not None:
+            event_filter = CallableEvents.once_event_filter(once)
+
+        # check signature:
+        Engine._check_signature("engine", event_filter, "event_filter", "event")
+
+        return EventWithFilter(self, event_filter)
+
+    @staticmethod
+    def every_event_filter(every):
+        def wrapper(engine, event):
+            if event % every == 0:
+                return True
+            return False
+        return wrapper
+
+    @staticmethod
+    def once_event_filter(once):
+        def wrapper(engine, event):
+            if event == once:
+                return True
+            return False
+        return wrapper
+
+
+class Events(CallableEvents, Enum):
+    """Events that are fired by the :class:`~ignite.engine.Engine` during execution.
+
+    Since v0.3.0, Events become more flexible and allow to pass an event filter to the Engine:
+
+    .. code-block:: python
+
+        engine = Engine()
+
+        # a) custom event filter
+        def custom_event_filter(engine, event):
+            if event in [1, 2, 5, 10, 50, 100]:
+                return True
+            return False
+
+        @engine.on(Events.ITERATION_STARTED(event_filter=custom_event_filter))
+        def call_on_special_event(engine):
+             # do something on 1, 2, 5, 10, 50, 100 iterations
+
+        # b) "every" event filter
+        @engine.on(Events.ITERATION_STARTED(every=10))
+        def call_every(engine):
+            # do something every 10th iteration
+
+        # c) "once" event filter
+        @engine.on(Events.ITERATION_STARTED(once=50))
+        def call_once(engine):
+            # do something on 50th iteration
+
+    Event filter function `event_filter` accepts as input `engine` and `event` and should return True/False.
+    Argument `event` is the value of iteration or epoch, depending on which type of Events the function is passed.
+
+    """
     EPOCH_STARTED = "epoch_started"
     EPOCH_COMPLETED = "epoch_completed"
     STARTED = "started"
@@ -33,7 +136,7 @@ class State(object):
         Events.EPOCH_STARTED: "epoch",
         Events.EPOCH_COMPLETED: "epoch",
         Events.STARTED: "epoch",
-        Events.COMPLETED: "epoch"
+        Events.COMPLETED: "epoch",
     }
 
     def __init__(self, **kwargs):
@@ -43,9 +146,12 @@ class State(object):
             setattr(self, k, v)
 
         for value in self.event_to_attr.values():
-            setattr(self, value, 0)
+            if not hasattr(self, value):
+                setattr(self, value, 0)
 
     def get_event_attrib_value(self, event_name):
+        if isinstance(event_name, EventWithFilter):
+            event_name = event_name.event
         if event_name not in State.event_to_attr:
             raise RuntimeError("Unknown event name '{}'".format(event_name))
         return getattr(self, State.event_to_attr[event_name])
@@ -152,7 +258,7 @@ class Engine(object):
         if self._process_function is None:
             raise ValueError("Engine must be given a processing function in order to run.")
 
-        self._check_signature(process_function, 'process_function', None)
+        Engine._check_signature(self, process_function, 'process_function', None)
 
     def register_events(self, *event_names, **kwargs):
         """Add events that can be fired.
@@ -166,20 +272,21 @@ class Engine(object):
         Args:
             *event_names: An object (ideally a string or int) to define the
                 name of the event being supported.
-            event_to_attr (dict): A dictionary to map an event to a state attribute.
+            event_to_attr (dict, optional): A dictionary to map an event to a state attribute.
 
         Example usage:
 
         .. code-block:: python
 
             from enum import Enum
+            from ignite.engine import Engine
 
-            class Custom_Events(Enum):
+            class CustomEvents(CallableEvents, Enum):
                 FOO_EVENT = "foo_event"
                 BAR_EVENT = "bar_event"
 
             engine = Engine(process_function)
-            engine.register_events(*Custom_Events)
+            engine.register_events(*CustomEvents)
 
 
         Example with State Attribute:
@@ -187,28 +294,41 @@ class Engine(object):
         .. code-block:: python
 
             from enum import Enum
+            from ignite.engine.engine import Engine, CallableEvents
 
-            class TBPTT_Events(Enum):
+            class TBPTT_Events(CallableEvents, Enum):
                 TIME_ITERATION_STARTED = "time_iteration_started"
                 TIME_ITERATION_COMPLETED = "time_iteration_completed"
 
-            TBPTT_event_to_attr = {TBPTT_Events.TIME_ITERATION_STARTED: 'time_iteration',
-                                   TBPTT_Events.TIME_ITERATION_COMPLETED: 'time_iteration'}
+            TBPTT_event_to_attr = {
+                TBPTT_Events.TIME_ITERATION_STARTED: 'time_iteration',
+                TBPTT_Events.TIME_ITERATION_COMPLETED: 'time_iteration'
+            }
 
             engine = Engine(process_function)
             engine.register_events(*TBPTT_Events, event_to_attr=TBPTT_event_to_attr)
             engine.run(data)
             # engine.state contains an attribute time_iteration, which can be accessed using engine.state.time_iteration
         """
+        # for python2 compatibility:
         event_to_attr = kwargs.get('event_to_attr', None)
-        if event_to_attr:
+        if event_to_attr is not None:
             if not isinstance(event_to_attr, dict):
                 raise ValueError('Expected event_to_attr to be dictionary. Got {}.'.format(type(event_to_attr)))
 
-        for name in event_names:
-            self._allowed_events.append(name)
-            if event_to_attr:
-                State.event_to_attr[name] = event_to_attr[name]
+        for e in event_names:
+            self._allowed_events.append(e)
+            if event_to_attr and e in event_to_attr:
+                State.event_to_attr[e] = event_to_attr[e]
+
+    def _handler_wrapper(self, handler, event_name, event_filter):
+
+        def wrapper(*args, **kwargs):
+            event = self.state.get_event_attrib_value(event_name)
+            if event_filter(self, event):
+                return handler(*args, **kwargs)
+
+        return wrapper
 
     def add_event_handler(self, event_name, handler, *args, **kwargs):
         """Add an event handler to be executed when the specified event is fired.
@@ -221,11 +341,11 @@ class Engine(object):
             **kwargs: optional keyword args to be passed to `handler`.
 
         Note:
-              The handler function's first argument will be `self`, the :class:`~ignite.engine.Engine` object it
-              was bound to.
+            The handler function's first argument will be `self`, the :class:`~ignite.engine.Engine` object it
+            was bound to.
 
-              Note that other arguments can be passed to the handler in addition to the `*args` and  `**kwargs`
-              passed here, for example during :attr:`~ignite.engine.Events.EXCEPTION_RAISED`.
+            Note that other arguments can be passed to the handler in addition to the `*args` and  `**kwargs`
+            passed here, for example during :attr:`~ignite.engine.Events.EXCEPTION_RAISED`.
 
         Returns:
             :class:`~ignite.engine.RemovableEventHandler`, which can be used to remove the handler.
@@ -241,13 +361,22 @@ class Engine(object):
 
             engine.add_event_handler(Events.EPOCH_COMPLETED, print_epoch)
 
+
+        Note:
+            Since v0.3.0, Events become more flexible and allow to pass an event filter to the Engine.
+            See :class:`~ignite.engine.Events` for more details.
+
         """
+        if isinstance(event_name, EventWithFilter):
+            event_name, event_filter = event_name.event, event_name.filter
+            handler = self._handler_wrapper(handler, event_name, event_filter)
+
         if event_name not in self._allowed_events:
             self._logger.error("attempt to add event handler to an invalid event %s.", event_name)
             raise ValueError("Event {} is not a valid event for this Engine.".format(event_name))
 
         event_args = (Exception(), ) if event_name == Events.EXCEPTION_RAISED else ()
-        self._check_signature(handler, 'handler', *(event_args + args), **kwargs)
+        Engine._check_signature(self, handler, 'handler', *(event_args + args), **kwargs)
 
         self._event_handlers[event_name].append((handler, args, kwargs))
         self._logger.debug("added handler for event %s.", event_name)
@@ -291,6 +420,7 @@ class Engine(object):
             raise ValueError("Input handler '{}' is not found among registered event handlers".format(handler))
         self._event_handlers[event_name] = new_event_handlers
 
+    @staticmethod
     def _check_signature(self, fn, fn_description, *args, **kwargs):
         exception_msg = None
 


Test patch:
diff --git a/tests/ignite/contrib/handlers/test_base_logger.py b/tests/ignite/contrib/handlers/test_base_logger.py
index eaf53fdb..4a965a57 100644
--- a/tests/ignite/contrib/handlers/test_base_logger.py
+++ b/tests/ignite/contrib/handlers/test_base_logger.py
@@ -2,6 +2,7 @@ import math
 import torch
 
 from ignite.engine import Engine, State, Events
+from ignite.engine.engine import EventWithFilter
 from ignite.contrib.handlers.base_logger import BaseLogger, BaseOutputHandler, global_step_from_engine
 from ignite.contrib.handlers import CustomPeriodicEvent
 
@@ -112,6 +113,9 @@ def test_attach():
 
         trainer.run(data, max_epochs=n_epochs)
 
+        if isinstance(event, EventWithFilter):
+            event = event.event
+
         mock_log_handler.assert_called_with(trainer, logger, event)
         assert mock_log_handler.call_count == n_calls
 
@@ -122,6 +126,8 @@ def test_attach():
     _test(Events.STARTED, 1)
     _test(Events.COMPLETED, 1)
 
+    _test(Events.ITERATION_STARTED(every=10), len(data) // 10 * n_epochs)
+
 
 def test_attach_on_custom_event():
 
@@ -214,6 +220,10 @@ def test_as_context_manager():
                           event_name=event)
 
             trainer.run(data, max_epochs=n_epochs)
+
+            if isinstance(event, EventWithFilter):
+                event = event.event
+
             mock_log_handler.assert_called_with(trainer, logger, event)
             assert mock_log_handler.call_count == n_calls
 
@@ -226,6 +236,8 @@ def test_as_context_manager():
     _test(Events.STARTED, 1)
     _test(Events.COMPLETED, 1)
 
+    _test(Events.ITERATION_STARTED(every=10), len(data) // 10 * n_epochs)
+
 
 def test_global_step_from_engine():
 
diff --git a/tests/ignite/contrib/handlers/test_tqdm_logger.py b/tests/ignite/contrib/handlers/test_tqdm_logger.py
index b6fd4f1d..451b1225 100644
--- a/tests/ignite/contrib/handlers/test_tqdm_logger.py
+++ b/tests/ignite/contrib/handlers/test_tqdm_logger.py
@@ -308,6 +308,9 @@ def test_pbar_wrong_events_order():
     with pytest.raises(ValueError, match="should be called before closing event"):
         pbar.attach(engine, event_name=Events.ITERATION_COMPLETED, closing_event_name=Events.ITERATION_STARTED)
 
+    with pytest.raises(ValueError, match="Closing event should not use any event filter"):
+        pbar.attach(engine, event_name=Events.ITERATION_STARTED, closing_event_name=Events.EPOCH_COMPLETED(every=10))
+
 
 def test_pbar_on_custom_events(capsys):
 
@@ -315,7 +318,7 @@ def test_pbar_on_custom_events(capsys):
     pbar = ProgressBar()
     cpe = CustomPeriodicEvent(n_iterations=15)
 
-    with pytest.raises(ValueError, match=r"Logging and closing events should be only ignite.engine.Events"):
+    with pytest.raises(ValueError, match=r"Logging event should be only `ignite.engine.Events`"):
         pbar.attach(engine, event_name=cpe.Events.ITERATIONS_15_COMPLETED, closing_event_name=Events.EPOCH_COMPLETED)
 
 
@@ -345,3 +348,22 @@ def test_pbar_with_nan_input():
     assert engine.should_terminate
     assert engine.state.iteration == 1001
     assert engine.state.epoch == 1
+
+
+def test_pbar_on_callable_events(capsys):
+
+    n_epochs = 1
+    loader = list(range(100))
+    engine = Engine(update_fn)
+
+    pbar = ProgressBar()
+    pbar.attach(engine, event_name=Events.ITERATION_STARTED(every=10), closing_event_name=Events.EPOCH_COMPLETED)
+    engine.run(loader, max_epochs=n_epochs)
+
+    captured = capsys.readouterr()
+    err = captured.err.split('\r')
+    err = list(map(lambda x: x.strip(), err))
+    err = list(filter(None, err))
+    actual = err[-1]
+    expected = u'Epoch: [90/100]  90%|█████████  [00:00<00:00]'
+    assert actual == expected
diff --git a/tests/ignite/engine/test_engine.py b/tests/ignite/engine/test_engine.py
index 3ec1ccd2..c22d74fe 100644
--- a/tests/ignite/engine/test_engine.py
+++ b/tests/ignite/engine/test_engine.py
@@ -12,6 +12,7 @@ from torch.nn.functional import mse_loss
 from torch.optim import SGD
 
 from ignite.engine import Engine, Events, State, create_supervised_trainer, create_supervised_evaluator
+from ignite.engine.engine import CallableEvents, EventWithFilter
 from ignite.metrics import MeanSquaredError
 
 
@@ -55,7 +56,7 @@ def test_invalid_process_raises_with_invalid_signature():
 def test_add_event_handler_raises_with_invalid_event():
     engine = DummyEngine()
 
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match=r"is not a valid event for this Engine"):
         engine.add_event_handler("incorrect", lambda engine: None)
 
 
@@ -284,68 +285,304 @@ def test_args_and_kwargs_are_passed_to_event():
 
 
 def test_custom_events():
-    class Custom_Events(Enum):
+    class CustomEvents(Enum):
         TEST_EVENT = "test_event"
 
     # Dummy engine
     engine = Engine(lambda engine, batch: 0)
-    engine.register_events(*Custom_Events)
+    engine.register_events(*CustomEvents)
 
     # Handle is never called
     handle = MagicMock()
-    engine.add_event_handler(Custom_Events.TEST_EVENT, handle)
+    engine.add_event_handler(CustomEvents.TEST_EVENT, handle)
     engine.run(range(1))
     assert not handle.called
 
     # Advanced engine
     def process_func(engine, batch):
-        engine.fire_event(Custom_Events.TEST_EVENT)
+        engine.fire_event(CustomEvents.TEST_EVENT)
 
     engine = Engine(process_func)
-    engine.register_events(*Custom_Events)
+    engine.register_events(*CustomEvents)
 
     # Handle should be called
     handle = MagicMock()
-    engine.add_event_handler(Custom_Events.TEST_EVENT, handle)
+    engine.add_event_handler(CustomEvents.TEST_EVENT, handle)
     engine.run(range(1))
     assert handle.called
 
 
 def test_custom_events_with_event_to_attr():
-    class Custom_Events(Enum):
+
+    class CustomEvents(Enum):
         TEST_EVENT = "test_event"
 
-    custom_event_to_attr = {Custom_Events.TEST_EVENT: 'test_event'}
+    custom_event_to_attr = {CustomEvents.TEST_EVENT: 'test_event'}
 
     # Dummy engine
     engine = Engine(lambda engine, batch: 0)
-    engine.register_events(*Custom_Events, event_to_attr=custom_event_to_attr)
+    engine.register_events(*CustomEvents, event_to_attr=custom_event_to_attr)
 
     # Handle is never called
     handle = MagicMock()
-    engine.add_event_handler(Custom_Events.TEST_EVENT, handle)
+    engine.add_event_handler(CustomEvents.TEST_EVENT, handle)
     engine.run(range(1))
     assert hasattr(engine.state, 'test_event')
     assert engine.state.test_event == 0
 
     # Advanced engine
     def process_func(engine, batch):
-        engine.fire_event(Custom_Events.TEST_EVENT)
+        engine.fire_event(CustomEvents.TEST_EVENT)
 
     engine = Engine(process_func)
-    engine.register_events(*Custom_Events, event_to_attr=custom_event_to_attr)
+    engine.register_events(*CustomEvents, event_to_attr=custom_event_to_attr)
 
     def handle(engine):
         engine.state.test_event += 1
 
-    engine.add_event_handler(Custom_Events.TEST_EVENT, handle)
+    engine.add_event_handler(CustomEvents.TEST_EVENT, handle)
     engine.run(range(25))
     assert engine.state.test_event == 25
 
     custom_event_to_attr = 'a'
     engine = Engine(lambda engine, batch: 0)
     with pytest.raises(ValueError):
-        engine.register_events(*Custom_Events, event_to_attr=custom_event_to_attr)
+        engine.register_events(*CustomEvents, event_to_attr=custom_event_to_attr)
+
+
+def test_callable_events_with_wrong_inputs():
+
+    with pytest.raises(ValueError, match=r"Only one of the input arguments should be specified"):
+        Events.ITERATION_STARTED()
+
+    with pytest.raises(ValueError, match=r"Only one of the input arguments should be specified"):
+        Events.ITERATION_STARTED(event_filter="123", every=12)
+
+    with pytest.raises(TypeError, match=r"Argument event_filter should be a callable"):
+        Events.ITERATION_STARTED(event_filter="123")
+
+    with pytest.raises(ValueError, match=r"Argument every should be integer and greater than one"):
+        Events.ITERATION_STARTED(every=-1)
+
+    with pytest.raises(ValueError, match=r"but will be called with"):
+        Events.ITERATION_STARTED(event_filter=lambda x: x)
+
+
+def test_callable_events():
+
+    assert isinstance(Events.ITERATION_STARTED.value, str)
+
+    def foo(engine, event):
+        return True
+
+    ret = Events.ITERATION_STARTED(event_filter=foo)
+    assert isinstance(ret, EventWithFilter)
+    assert ret.event == Events.ITERATION_STARTED
+    assert ret.filter == foo
+    assert isinstance(Events.ITERATION_STARTED.value, str)
+
+    # assert ret in Events
+    assert Events.ITERATION_STARTED.name in "{}".format(ret)
+    # assert ret in State.event_to_attr
+
+    ret = Events.ITERATION_STARTED(every=10)
+    assert isinstance(ret, EventWithFilter)
+    assert ret.event == Events.ITERATION_STARTED
+    assert ret.filter is not None
+
+    # assert ret in Events
+    assert Events.ITERATION_STARTED.name in "{}".format(ret)
+    # assert ret in State.event_to_attr
+
+    ret = Events.ITERATION_STARTED(once=10)
+    assert isinstance(ret, EventWithFilter)
+    assert ret.event == Events.ITERATION_STARTED
+    assert ret.filter is not None
+
+    # assert ret in Events
+    assert Events.ITERATION_STARTED.name in "{}".format(ret)
+    # assert ret in State.event_to_attr
+
+    def _attach(e1, e2):
+        assert id(e1) != id(e2)
+
+    _attach(Events.ITERATION_STARTED(every=10), Events.ITERATION_COMPLETED(every=10))
+
+
+def test_every_event_filter_with_engine():
+
+    def _test(event_name, event_attr, every, true_num_calls):
+
+        engine = Engine(lambda e, b: b)
+
+        counter = [0, ]
+        counter_every = [0, ]
+        num_calls = [0, ]
+
+        @engine.on(event_name(every=every))
+        def assert_every(engine):
+            counter_every[0] += every
+            assert getattr(engine.state, event_attr) % every == 0
+            assert counter_every[0] == getattr(engine.state, event_attr)
+            num_calls[0] += 1
+
+        @engine.on(event_name)
+        def assert_(engine):
+            counter[0] += 1
+            assert getattr(engine.state, event_attr) == counter[0]
+
+        d = list(range(100))
+        engine.run(d, max_epochs=5)
+
+        assert num_calls[0] == true_num_calls
+
+    _test(Events.ITERATION_STARTED, "iteration", 10, 100 * 5 // 10)
+    _test(Events.ITERATION_COMPLETED, "iteration", 10, 100 * 5 // 10)
+    _test(Events.EPOCH_STARTED, "epoch", 2, 5 // 2)
+    _test(Events.EPOCH_COMPLETED, "epoch", 2, 5 // 2)
+
+
+def test_once_event_filter_with_engine():
+
+    def _test(event_name, event_attr):
+
+        engine = Engine(lambda e, b: b)
+
+        once = 2
+        counter = [0, ]
+        num_calls = [0, ]
+
+        @engine.on(event_name(once=once))
+        def assert_once(engine):
+            assert getattr(engine.state, event_attr) == once
+            num_calls[0] += 1
+
+        @engine.on(event_name)
+        def assert_(engine):
+            counter[0] += 1
+            assert getattr(engine.state, event_attr) == counter[0]
+
+        d = list(range(100))
+        engine.run(d, max_epochs=5)
+
+        assert num_calls[0] == 1
+
+    _test(Events.ITERATION_STARTED, "iteration")
+    _test(Events.ITERATION_COMPLETED, "iteration")
+    _test(Events.EPOCH_STARTED, "epoch")
+    _test(Events.EPOCH_COMPLETED, "epoch")
+
+
+def test_custom_event_filter_with_engine():
+
+    special_events = [1, 2, 5, 7, 17, 20]
+
+    def custom_event_filter(engine, event):
+        if event in special_events:
+            return True
+        return False
+
+    def _test(event_name, event_attr, true_num_calls):
+
+        engine = Engine(lambda e, b: b)
+
+        num_calls = [0, ]
+
+        @engine.on(event_name(event_filter=custom_event_filter))
+        def assert_on_special_event(engine):
+            assert getattr(engine.state, event_attr) == special_events.pop(0)
+            num_calls[0] += 1
+
+        d = list(range(50))
+        engine.run(d, max_epochs=25)
+
+        assert num_calls[0] == true_num_calls
+
+    _test(Events.ITERATION_STARTED, "iteration", len(special_events))
+    _test(Events.ITERATION_COMPLETED, "iteration", len(special_events))
+    _test(Events.EPOCH_STARTED, "epoch", len(special_events))
+    _test(Events.EPOCH_COMPLETED, "epoch", len(special_events))
+
+
+def test_callable_event_bad_behaviour():
+
+    special_events = [1, 2, 5, 7, 17, 20]
+
+    def custom_event_filter(engine, event):
+        if event in special_events:
+            return True
+        return False
+
+    # Check bad behaviour
+    engine = Engine(lambda e, b: b)
+    counter = [0, ]
+
+    # Modify events
+    Events.ITERATION_STARTED(event_filter=custom_event_filter)
+
+    @engine.on(Events.ITERATION_STARTED)
+    def assert_all_iters(engine):
+        counter[0] += 1
+        assert engine.state.iteration == counter[0]
+
+    d = list(range(50))
+    engine.run(d, max_epochs=25)
+
+    assert counter[0] == engine.state.iteration
+
+
+def test_custom_callable_events():
+
+    class CustomEvents(Enum):
+        TEST_EVENT = "test_event"
+
+    with pytest.raises(TypeError, match=r"object is not callable"):
+        CustomEvents.TEST_EVENT(every=10)
+
+    class CustomEvents2(CallableEvents, Enum):
+        TEST_EVENT = "test_event"
+
+    CustomEvents2.TEST_EVENT(every=10)
+
+
+def test_custom_callable_events_with_engine():
+
+    class CustomEvents(CallableEvents, Enum):
+        TEST_EVENT = "test_event"
+
+    event_to_attr = {
+        CustomEvents.TEST_EVENT: "test_event"
+    }
+
+    special_events = [1, 2, 5, 7, 17, 20]
+
+    def custom_event_filter(engine, event):
+        if event in special_events:
+            return True
+        return False
+
+    def _test(event_name, event_attr, true_num_calls):
+
+        def update_fn(engine, batch):
+            engine.state.test_event = engine.state.iteration
+            engine.fire_event(CustomEvents.TEST_EVENT)
+
+        engine = Engine(update_fn)
+        engine.register_events(*CustomEvents, event_to_attr=event_to_attr)
+
+        num_calls = [0, ]
+
+        @engine.on(event_name(event_filter=custom_event_filter))
+        def assert_on_special_event(engine):
+            assert getattr(engine.state, event_attr) == special_events.pop(0)
+            num_calls[0] += 1
+
+        d = list(range(50))
+        engine.run(d, max_epochs=25)
+
+        assert num_calls[0] == true_num_calls
+
+    _test(CustomEvents.TEST_EVENT, "test_event", len(special_events))
 
 
 def test_on_decorator_raises_with_invalid_event():
@@ -876,3 +1113,31 @@ def test_alter_batch():
     num_iters = 25
     data = list(range(num_iters))
     trainer.run(data, num_epochs)
+
+
+def test_state_get_event_attrib_value():
+    state = State()
+    state.iteration = 10
+    state.epoch = 9
+
+    e = Events.ITERATION_STARTED
+    assert state.get_event_attrib_value(e) == state.iteration
+    e = Events.ITERATION_COMPLETED
+    assert state.get_event_attrib_value(e) == state.iteration
+    e = Events.EPOCH_STARTED
+    assert state.get_event_attrib_value(e) == state.epoch
+    e = Events.EPOCH_COMPLETED
+    assert state.get_event_attrib_value(e) == state.epoch
+    e = Events.STARTED
+    assert state.get_event_attrib_value(e) == state.epoch
+    e = Events.COMPLETED
+    assert state.get_event_attrib_value(e) == state.epoch
+
+    e = Events.ITERATION_STARTED(every=10)
+    assert state.get_event_attrib_value(e) == state.iteration
+    e = Events.ITERATION_COMPLETED(every=10)
+    assert state.get_event_attrib_value(e) == state.iteration
+    e = Events.EPOCH_STARTED(once=5)
+    assert state.get_event_attrib_value(e) == state.epoch
+    e = Events.EPOCH_COMPLETED(once=5)
+    assert state.get_event_attrib_value(e) == state.epoch