TurboGears uses the Python standard library logger module, which you can read about in detail in the official Python library reference .
The default Turbogears logging is defined in two places:
dev.cfg and prod.cfg
From your quickstart directory, contains configuration specific to your environment.
Contains configuration for your logging that won’t change based on your environment.
Logging specific configuration is marked by the tag [logging] in both of these files.
In addition, there is also the configuration file test.cfg. This takes the role of dev.cfg/prod.cfg when you run the unit tests for your project.
Please note the <your package> used throughout this doc. Remember to substitute your app’s package name when you try these things out for yourself.
To get the most out of this section, check out the first page of the logging module documentation.
Two lines in your controllers.py are all you need to get going. First, create a logger:
log = logging.getLogger("<your package>.controllers")
That is the preferred way to create a logger. The param string is the name of your new logger. Although this could be anything you want, it’s good practice to follow a convention and keep your logs organized from the get go. Here we name the logger after the package/module since this is a unique name.
One way to easily set up logger names by package is:
log = logging.getLogger(__name__)
Next, send some info to our new logger object:
log.debug("Happy TurboGears Controller Responding For Duty")
Which, as you can read in the official docs, is a log with level 10, the debug level of severity. You can probably just ignore the level numbers, unless you want to create log levels of your own.
Because then you’d be shorting yourself on the power and flexibility of the Python logging module.
We want to be able to send our logs to Multiple Destinations, as defined by you. So you can later find out if the error is on your side or on the framework just by looking at the logs.
You can also send different logs to different places, all without sprinkling your app code with needless boilerplate code, thanks to the config files.
In general is a better practice to do it the first way and take full advantage of the Python logging module. If not, it’s only one extra line of code anyways.
So let’s take a look at the format. This config file is read by ConfigObj and you can check out the documentation for the ConfigObj File Format for more detail. For our purposes here the only difference from standard library ConfigParser is the nested tags, which will generate a dictionary of the tag name and fill it with whatever’s in that nested section.
If you want to explore the goodness of ConfigObj, take a look at the docs, especially the part about storing Python types in a config file storing python types in a config file, which might clear up what’s going on in the default log.cfg.
Think of *() as %(), since we can’t put %() into a config file.
The reason is that ConfigObj module will interpret %() as something else.
The tg-admin quickstart command will create 3 loggers for you:
- your_project - The one we used at the start of this tutorial, prints DEBUG messages to stdout
- allinfo - The main logger (note the missing qualname)
- access - Prints INFO messages to stdout.
Let’s take a look at how TurboGears does it! We’ll go through file by file.
[logging] [[loggers]] [[[<your_package>]]] level='DEBUG' qualname='<your package>' handlers=['debug_out'] [[[allinfo]]] level='INFO' handlers=['debug_out'] [[[access]]] level='INFO' qualname='turbogears.access' handlers=['access_out'] propagate=0
In your config files, always start the logging specific stuff with:
like in dev/prod.cfg. They’ll all load up into the same dict thanks to ConfigObj so in practice defining things in either file is ok.
When all is said and done, you have 3 sections defined somewhere in your config files like so:
[[formatters]] [[handlers]] [[loggers]]
While all three are technically optional, it’s recommended you have at least the “handlers” and “loggers” sections.
Let’s go over these sections and their keys and what they do. Remember that these will be read into the dictionary as keyword arguments, so make sure you keep them unique:
Each subsection within this section should have only one key as per the following:
The format string which initializes a Formatter Object. Just like in the standard docs, but remember to replace %( with *( like we said above.
Each subsection within this section is required to have a class key.
Should be one of the Handler Objects or a subclass thereof. If you subclass, you might get some errors from TG since your class is not in logging.dict.
Parameters pass to each subclass of Handler and will vary based on which on you pick.
Indicates the logging level of this handler and can be any of the default levels DEBUG, INFO, WARNING, ERROR and CRITICAL. With a patch it can also be a string with a level number (Is this fixed yet?). Defaults to NOTSET.
Can be the name of any subsection that you defined under the Formatters section. Defaults to %(message)s.
This is another way to create Logger Objects. You can either define them here or do the calls in your code, it depends on your situation/taste. Code accomplishing the same thing follow the descriptions of the parameters.
Name of the logger
if qualname: log = logging.getLogger(qualname) else: log = logging.getLogger()
The level threshold for this logger. Levels less severe than this will be ignored.
Equivalent to calling the addhandler method of logger objects. Useful for sending logs to multiple places.
for handler in handlers: log.addHandlers(handler)
Same as the propagate class attribute of logger objects. Sets whether or not log messages are passed to parent loggers. Defaults to 1.
[logging] [[formatters]] [[[message_only]]] format='*(message)s' [[[full_content]]] format='*(asctime)s *(name)s *(levelname)s *(message)s' [[handlers]] [[[debug_out]]] class='StreamHandler' level='DEBUG' args='(sys.stdout,)' formatter='full_content' [[[access_out]]] class='StreamHandler' level='INFO' args='(sys.stdout,)' formatter='message_only' [[[error_out]]] class='StreamHandler' level='ERROR' args='(sys.stdout,)'
Can you follow it yet? We define 2 formatters named “message_only” and “full_content”, then we pick 3 handlers which all stream to stdout but with different levels and using the two formatters we defined in the same file.