When you first start programming in any language, you’re often taught to print()
to the console in order to see the values of variables or just to help you debug.
As a beginner, this is an acceptable technique. As you advance in your programming career, you find out sooner or later, that print()
-ing to the console to debug is easy, but comes with a lot of drawbacks. But what else should you do?
Use the debugger and use the Logging module instead.
Python created “PEP-282 — A Logging System” back in February 2002! https://www.python.org/dev/peps/pep-0282/
It was inspired by other logging packages of other languages. Logging is a little bit more complex than print()
, but it is much better suited to debugging your program than print()
.
Let’s enhance our REST adapter with Logging!
import requests
import requests.packages
from json import JSONDecodeError
from typing import List, Dict
from .exceptions import TheCatApiException
from .models import Result
class RestAdapter:
def __init__(self, hostname: str, api_key: str = '', ver: str = 'v1', ssl_verify: bool = True):
self.url = "https://{}/{}".format(hostname, ver)
self._api_key = api_key
self._ssl_verify = ssl_verify
if not ssl_verify:
# noinspection PyUnresolvedReferences
requests.packages.urllib3.disable_warnings()
Let’s start by importing logging
into our project.
import logging
That was easy. How do we add it to our REST adapter? Tack on parameter to our __init__
constructor called logger
and hint that it is of type logging.Logger
and set it to the default value of None
:
class RestAdapter:
def __init__(self, hostname: str, api_key: str = '', ver: str = 'v1', ssl_verify: bool = True, logger: logging.Logger = None):
Then in the constructor, add the following line:
self._logger = logger or logging.getLogger(__name__)
This takes either a logger that the consumer of this REST adapter supplied. Or, if none is provided, it auto-generates one for you based on the class name.
Ok, we have a logger now, but what do we do with it? How do we even use it?
Things you should know about loggers
- Logger output can be sent to many different locations: the screen, log files, network locations, or even other programs.
- Logger output should be optimized for machine readability, but it also should be readable by humans.
Why is this? For the most part, you want your logs parse-able by other code for analysis. But you still want to be able to read the logs yourself. - The Logger has many different “log levels”. Python has 6 levels built into the logger. The default levels are:
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0
If you wish, you can create additional levels between the existing levels. As an example, you could create your own custom logger that has 2 additional levels. One called MESG 25
and another LOWDEBUG 7
.
What are these “levels” exactly? Well, when you send a message to the logger, you often want to specify how important this info is. Perhaps, you’re only interested in Warnings and Errors, so you set your log level to 30. Maybe you want to see all the piddly little details, so you set your log level to 0. Maybe you only want to log critical errors to the screen, warnings/errors/critical to the local log file, and send all info to a log collector program.
How to use the logger in your code
self._logger.info("Something happened")
This will send the message “Something happened” to the logger at the INFO (20) level. This is a silly thing to send to the logger because it doesn’t tell anyone anything.
It does point out a good practice with logging:
- Log the state just before something important is going to happen.
- Log the result just after something important happened (especially if the something that happened was “bad”)
Let’s look at our code again:
class RestAdapter:
def __init__(self, hostname: str, api_key: str = '', ver: str = 'v1', ssl_verify: bool = True, logger: logging.Logger = None):
self._logger = logger or logging.getLogger(__name__)
self.url = f"https://{hostname}/{ver}"
self._api_key = api_key
self._ssl_verify = ssl_verify
if not ssl_verify:
# noinspection PyUnresolvedReferences
requests.packages.urllib3.disable_warnings()
def _do(self, http_method: str, endpoint: str, def _do(self, http_method: str, endpoint: str, ep_params: Dict = None, data: Dict = None):
full_url = self.url + endpoint
headers = {'x-api-key': self._api_key}
try:
response = requests.request(method=http_method, url=full_url, verify=self._ssl_verify,
headers=headers, params=ep_params, json=data)
except requests.exceptions.RequestException as e:
raise TheCatApiException("Request failed") from e
try:
data_out = response.json()
except (ValueError, JSONDecodeError) as e:
raise TheCatApiException("Bad JSON in response") from e
if 299 >= response.status_code >= 200: # 200 to 299 is OK
return Result(response.status_code, message=response.reason, data=data_out)
raise TheCatApiException(f"{response.status_code}: {response.reason}")
def get(self, endpoint: str, ep_params: Dict = None) -> Result:
return self._do(http_method='GET', endpoint=endpoint, ep_params=ep_params)
def post(self, endpoint: str, ep_params: Dict = None, data: Dict = None) -> Result:
return self._do(http_method='POST', endpoint=endpoint, ep_params=ep_params, data=data)
def delete(self, endpoint: str, ep_params: Dict = None, data: Dict = None) -> Result:
return self._do(http_method='DELETE', endpoint=endpoint, ep_params=ep_params, data=data)
If we look over our code, we might be tempted to put log lines in the __init__
constructor of our RestAdapter
class, but not much is actually happening there.
Let’s add a couple logger lines before the first try-except block because some action is happening there:
log_line_pre = f"method={http_method}, url={full_url}, params={ep_params}"
log_line_post = ', '.join((log_line_pre, "success={}, status_code={}, message={}"))
try:
self._logger.debug(msg=log_line_pre)
response = requests.request(method=http_method, url=full_url, verify=self._ssl_verify,
headers=headers, params=ep_params, json=data)
except requests.exceptions.RequestException as e:
self._logger.error(msg=(str(e)))
raise TheCatApiException("Request failed") from e
First, we’ve created two formatted-string using pairs of variables and variable values in the form of variable_name=variable_value
separated by commas (,)
Then we pass this log_line_pre
to the logger and set it at the debug
level. This is to log that the code is about to do something significant. In this case, it is about to call requests.request
We also put an error logging msg
in the except
block to log when anything goes wrong.
The reason we use key-value pairs in the comma-delimited equals format is because many logging collector programs (like the enterprise logging service, Splunk) readily use this format. It’s also easy for you to write your own parsing program.
Let’s continue to add more log lines in our REST adapter:
try:
data_out = response.json()
except (ValueError, JSONDecodeError) as e:
self._logger.error(msg=log_line_post.format(False, None, e))
raise TheCatApiException("Bad JSON in response") from e
is_success = 299 >= response.status_code >= 200 # 200 to 299 is OK
log_line = log_line_post.format(is_success, response.status_code, response.reason)
if is_success:
self._logger.debug(msg=log_line)
return Result(response.status_code, message=response.reason, data=data_out)
self._logger.error(msg=log_line)
raise TheCatApiException(f"{response.status_code}: {response.reason}")
Note here that we don’t log the response.json()
deserialization. This is a common occurrence and doesn’t need to be logged, except for when there is an error (as shown above).
We refactor our code slightly to convert the 200-299 status code test into a boolean (is_success
).
If it IS a success, we log the response as a debug
level info. Generally, we’re not interested in this info, unless we’re debugging. Thus debug
level.
If NOT a success, then we log as an error and raise the exception.
And that’s it for logging! The get()
, post()
, and delete()
methods don’t require logging because they all leverage the same _do()
method which already has logging added.
Step 7: Time to comment our code!
Source code: https://github.com/PretzelLogix/py-cat-api/tree/06_add_logging
2 replies on “Step 6: Add Logging”
[…] Step 6: Add Logging […]
[…] Step 6: Add Logging […]