Skip to content

Commit 1a4a10d

Browse files
authored
Added CLI starter example to logging cookbook. (GH-9910)
1 parent ea75187 commit 1a4a10d

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed

Doc/howto/logging-cookbook.rst

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2548,3 +2548,164 @@ In this case, the message #5 printed to ``stdout`` doesn't appear, as expected.
25482548
Of course, the approach described here can be generalised, for example to attach
25492549
logging filters temporarily. Note that the above code works in Python 2 as well
25502550
as Python 3.
2551+
2552+
2553+
.. _starter-template:
2554+
2555+
A CLI application starter template
2556+
----------------------------------
2557+
2558+
Here's an example which shows how you can:
2559+
2560+
* Use a logging level based on command-line arguments
2561+
* Dispatch to multiple subcommands in separate files, all logging at the same
2562+
level in a consistent way
2563+
* Make use of simple, minimal configuration
2564+
2565+
Suppose we have a command-line application whose job is to stop, start or
2566+
restart some services. This could be organised for the purposes of illustration
2567+
as a file ``app.py`` that is the main script for the application, with individual
2568+
commands implemented in ``start.py``, ``stop.py`` and ``restart.py``. Suppose
2569+
further that we want to control the verbosity of the application via a
2570+
command-line argument, defaulting to ``logging.INFO``. Here's one way that
2571+
``app.py`` could be written::
2572+
2573+
import argparse
2574+
import importlib
2575+
import logging
2576+
import os
2577+
import sys
2578+
2579+
def main(args=None):
2580+
scriptname = os.path.basename(__file__)
2581+
parser = argparse.ArgumentParser(scriptname)
2582+
levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
2583+
parser.add_argument('--log-level', default='INFO', choices=levels)
2584+
subparsers = parser.add_subparsers(dest='command',
2585+
help='Available commands:')
2586+
start_cmd = subparsers.add_parser('start', help='Start a service')
2587+
start_cmd.add_argument('name', metavar='NAME',
2588+
help='Name of service to start')
2589+
stop_cmd = subparsers.add_parser('stop',
2590+
help='Stop one or more services')
2591+
stop_cmd.add_argument('names', metavar='NAME', nargs='+',
2592+
help='Name of service to stop')
2593+
restart_cmd = subparsers.add_parser('restart',
2594+
help='Restart one or more services')
2595+
restart_cmd.add_argument('names', metavar='NAME', nargs='+',
2596+
help='Name of service to restart')
2597+
options = parser.parse_args()
2598+
# the code to dispatch commands could all be in this file. For the purposes
2599+
# of illustration only, we implement each command in a separate module.
2600+
try:
2601+
mod = importlib.import_module(options.command)
2602+
cmd = getattr(mod, 'command')
2603+
except (ImportError, AttributeError):
2604+
print('Unable to find the code for command \'%s\'' % options.command)
2605+
return 1
2606+
# Could get fancy here and load configuration from file or dictionary
2607+
logging.basicConfig(level=options.log_level,
2608+
format='%(levelname)s %(name)s %(message)s')
2609+
cmd(options)
2610+
2611+
if __name__ == '__main__':
2612+
sys.exit(main())
2613+
2614+
And the ``start``, ``stop`` and ``restart`` commands can be implemented in
2615+
separate modules, like so for starting::
2616+
2617+
# start.py
2618+
import logging
2619+
2620+
logger = logging.getLogger(__name__)
2621+
2622+
def command(options):
2623+
logger.debug('About to start %s', options.name)
2624+
# actually do the command processing here ...
2625+
logger.info('Started the \'%s\' service.', options.name)
2626+
2627+
and thus for stopping::
2628+
2629+
# stop.py
2630+
import logging
2631+
2632+
logger = logging.getLogger(__name__)
2633+
2634+
def command(options):
2635+
n = len(options.names)
2636+
if n == 1:
2637+
plural = ''
2638+
services = '\'%s\'' % options.names[0]
2639+
else:
2640+
plural = 's'
2641+
services = ', '.join('\'%s\'' % name for name in options.names)
2642+
i = services.rfind(', ')
2643+
services = services[:i] + ' and ' + services[i + 2:]
2644+
logger.debug('About to stop %s', services)
2645+
# actually do the command processing here ...
2646+
logger.info('Stopped the %s service%s.', services, plural)
2647+
2648+
and similarly for restarting::
2649+
2650+
# restart.py
2651+
import logging
2652+
2653+
logger = logging.getLogger(__name__)
2654+
2655+
def command(options):
2656+
n = len(options.names)
2657+
if n == 1:
2658+
plural = ''
2659+
services = '\'%s\'' % options.names[0]
2660+
else:
2661+
plural = 's'
2662+
services = ', '.join('\'%s\'' % name for name in options.names)
2663+
i = services.rfind(', ')
2664+
services = services[:i] + ' and ' + services[i + 2:]
2665+
logger.debug('About to restart %s', services)
2666+
# actually do the command processing here ...
2667+
logger.info('Restarted the %s service%s.', services, plural)
2668+
2669+
If we run this application with the default log level, we get output like this:
2670+
2671+
.. code-block:: shell-session
2672+
2673+
$ python app.py start foo
2674+
INFO start Started the 'foo' service.
2675+
2676+
$ python app.py stop foo bar
2677+
INFO stop Stopped the 'foo' and 'bar' services.
2678+
2679+
$ python app.py restart foo bar baz
2680+
INFO restart Restarted the 'foo', 'bar' and 'baz' services.
2681+
2682+
The first word is the logging level, and the second word is the module or
2683+
package name of the place where the event was logged.
2684+
2685+
If we change the logging level, then we can change the information sent to the
2686+
log. For example, if we want more information:
2687+
2688+
.. code-block:: shell-session
2689+
2690+
$ python app.py --log-level DEBUG start foo
2691+
DEBUG start About to start foo
2692+
INFO start Started the 'foo' service.
2693+
2694+
$ python app.py --log-level DEBUG stop foo bar
2695+
DEBUG stop About to stop 'foo' and 'bar'
2696+
INFO stop Stopped the 'foo' and 'bar' services.
2697+
2698+
$ python app.py --log-level DEBUG restart foo bar baz
2699+
DEBUG restart About to restart 'foo', 'bar' and 'baz'
2700+
INFO restart Restarted the 'foo', 'bar' and 'baz' services.
2701+
2702+
And if we want less:
2703+
2704+
.. code-block:: shell-session
2705+
2706+
$ python app.py --log-level WARNING start foo
2707+
$ python app.py --log-level WARNING stop foo bar
2708+
$ python app.py --log-level WARNING restart foo bar baz
2709+
2710+
In this case, the commands don't print anything to the console, since nothing
2711+
at ``WARNING`` level or above is logged by them.

0 commit comments

Comments
 (0)