Ultimate Guide to learn Python logging module [ In-Depth Tutorial]

Best logging practice in Python

Programmer working about software cyberspace
0 48

An In-Depth Tutorial to learn around logging module in Python, Key Concepts of Standard Library Logging Module various options and best practises , Integration with Splunk and other tools.

1 Introduction

Logs are Important..and so is the better logging !

Be a Developer or Support Professional, Better logging is incredibly important for your application to provide insight into why it happened and how to fix it…

Organised, Structured, Meaningful logs with contextual information is key for each application to perform an in-depth investigation to know when, how & why system goes wrong! Not only this …if you have well-formatted log…you can build analysis top of it using various log aggregators like Splunk, ELK etc to build monitoring and early detection system.

Everyone knows the importance of good logging practise but.. still print statement is widely used to see how things moving…without adding enough context information to ascertain what went wrong.

In this tutorial, I have covered Good logging practice in Python to get most out of Standard Python Logging Module by adding context information with other key metrics to generate log events.

So, I will cover some of the best practices you can follow when you are heading for new projects!

By end of the article, you will learn about –

  • Details Around Standard Python Logging Module
  • Logging Best Practise to Follow
  • Make better use of Python logs with JSON
  • Django’s & Flask Logging Configuration
  • Logging aggregators
  • Logging from Python in Splunk

let’s get started!

2 Why Standard Python Logging Module

  • Standard Python Module API is very flexible and easy to use with lot many examples floating in developer community with mature reference documentation.
  • Key Benefit of  standard logging module is that it works well with other python module and easy to integrate your code logs with third-party or standard python modules
  • Logger API interface provides Handlers (How to handle each log record ) with various Formatters(What to Include in log records) along with Filters (Which log records to send  to handlers)
  • Various Third Party Tool Integration provides an interface to send the stranded log for monitoring and early detection.
  • logger module provide you ways to generate different logs based on severity, application environment (DEV/QA/UAT/PROD) and Infrastructure (PaaS, Unix Server, Central Log Server)
  • Adding Context Information to log such as IP, user, time, module, class, function,line_no, variable etc.
  • Use one log analyzer tool to overview all your environments and their log entries for services, query
  • Audit logging records events for business analysis.

3 … Why not Print?

  • The broadly used concept of the print statement to validate the correctness of block code can be useful if your goal is to display help/log statement to console …in all other cases logger is much better and should be widely used.
  • Print statements don’t contain diagnostic information such as the module name, script path, function, and line number of the logging event which are readily available with logger module

4 The Basics of  Stranded Logging Module

Let’s go through the working of Stranded Logging Module by –

  • Writing a simple logger
  • How to  include log from multiple/parent  modules
  • various Log formatting options
  • ways of Log configuration

Thanks to Python community, logging is a standard module, it was well designed to be easy-to-use and very flexible. so all you need to do to get started is

import logging

The core functionality of logging module works around the below  key concepts of LOGGER, LOG RECORD, HANDLER, FORMATTERS, FILTER 

  • Loggers expose the Python API  interface that your application code directly uses.
  • Handlers send the log records based on log events (created by loggers) to the appropriate destination. (file/console/DB/third-party tools)
  • Filters provide a finer grained facility for determining which log records to need to send to output.
  • Formatters specify the layout of log records in the final output.

Log event information is passed between loggers, handlers, filters and formatters with LogRecord instance.

logger, handler, and log message call each specifies a level. The log message is only emitted if the handler and logger are configured to emit messages of that level or higher.

if you wish to read more information around please see official documents –

5 High level overview of working of Python Loggers

High level overview of working of Python Loggers

 

6 Details of LOG LEVEL 

Level Numeric Value Function Uses/Synatax
CRITICAL 50 logging.critical() Show a serious error, the program may be unable to continue running

logger.critical('SQL Server Has Failed to Initiate Reporting Service')

This will indicate serious error, that the program itself may be unable to continue running. and some trouble shooting is required.

ERROR 40 logging.error() Show a more serious problem

logger.error("An error has happened!")

WARNING 30 logging.warning() Indicate something unexpected happened, or could happen

logger.warning("A Sample Warn Statement")

it’s  indication that something unexpected happened, or indicative of some problem in the near future (e.g. ‘disk space low’). The code block is still working as expected.

INFO 20 logging.info() Confirm that things are working as expected

logger.info(' This is info Message')

Confirmation for  blocks are working as expected.  To ascertain some milestone …such as DB Connection Connected, Records been updated , Payment has been transferred(etc.)

DEBUG 10 logging.debug() Diagnose problems, show detailed information

logger.debug('Hey I am Debug Message')

Detailed information, typically of interest …such as variables ..some intermediate results and check around responses from other micro/rest services ..etc

 

**There are two other logging calls available:

  • logger.log(): Manually emits a logging message with a specific log level.
  • logger.exception(): Creates an ERROR level logging message wrapping the current exception stack frame. Useful in case you need to

logger.exception is very useful and detailed uses will be covered later. let’s see the basic example of logging module to see the different type of messages.

# importing module
import logging

# Create and configure logger
logging.basicConfig(filename="log.log", format='%(asctime)s %(message)s', filemode='w')

# Creating an object
logger = logging.getLogger(__name__)

# Setting the threshold of logger to DEBUG
logger.setLevel(logging.DEBUG)

# Debug messages
logger.debug("Here is debug Message")

# Debug messages
logger.info("This is just an information")

# Warning messages
logger.warning("Oops ! Its a Warning")

#Error message
logger.error("Did you try to reach out of bound index ")

#critical message
logger.critical("Server went down")

When you run the above code snippet you will see below in filename=”log.log” .

2018-07-01 18:20:38,755 Here is debug Message
2018-07-01 18:20:38,755 This is just an information
2018-07-01 18:20:38,755 Oops ! Its a Warning
2018-07-01 18:20:38,755 Did you try to reach out of bound index 
2018-07-01 18:20:38,755 Server went down

In the above example threshold of logger is set to DEBUG…so all the log statement will be printed …how ever in production ready application you will not like to send  every thing in log

Change  Setting  of the threshold of logger to WARNING –>  logger.setLevel(logging.WARNING)

You will see the difference in log file ..

2018-07-01 18:19:53,036 Oops ! Its a Warning
2018-07-01 18:19:53,036 Did you try to divide by zero
2018-07-01 18:19:53,036 Server is down

 

7 How about Multiple handlers and formatters

Loggers are plain Python objects. The addHandler() method has no minimum or maximum quota for the number of handlers you may add to your logging configuration file.

This is also useful when your application need multiple logs created based on severity of log level …

for example you might need multiple handlers for different type of log message

You might need to send info & warning message to go into log file …while at the same time you might need critical alerts to be send to other stream such as HPOV/Service Now/ Splunk or any other log aggregate supporting monitoring tool …

To set this up, simply configure the multiple handlers and add it back to logger object…in below example we have configured two handlers

In the example below I set the logger to DEBUG, the stream handler to INFO and the TimedRotatingFileHandler to DEBUG.

So the file has DEBUG entries and the stream outputs only INFO. You can’t direct only DEBUG to one and only INFO to another handler. For that you’ll need another logger.

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s: %(message)s') 
logger.setLevel(logging.DEBUG) 

#Stream Handler 
stream_handler = logging.StreamHandler() 
stream_handler.setLevel(logging.INFO) 
stream_handler.setFormatter(formatter) 

#File Handler 
logFilePath = "my.log" 
file_handler = logging.handlers.TimedRotatingFileHandler(filename = logFilePath, when = 'midnight', backupCount = 30) 
file_handler.setFormatter(formatter) 
file_handler.setLevel(logging.DEBUG) 
logger.addHandler(file_handler)
logger.addHandler(stream_handler)

 

8 Few Existing handler classes

if you wish to read more details around the Handlers check out official documentation.

  • FileHandler: Basic file appender
  • RotatingFileHandler (writes to a configurable number of a configurable size, which rotate)
  • TimedRotatingFileHandler: Like the last, but rotates based on time and not log size
  • WatchedFileHandler (≥ Py2.6): Re-opens the file when it is changed by something else. Not on windows. For example useful when you log to a file that may be rotated by something else.
  • SocketHandler: TCP logger
  • DatagramHandler: UDP logger
  • HTTPHandler: Log to some web server (GET or POST)
  • SMTPHandler: Send mail
  • MemoryHandler: Allows you to buffer many records and have them emptied into a target handler in chunks
  • SysLogHandler: unix syslog daemon (via an UDP socket, or a local filename (e.g. /dev/log))
  • NTEventLogHandler: local Win NT/2000/XP event log
  • StreamHandler: writes to file-like object — anything with write() and flush() (e.g. sys.stderr, or your own object)
  • NullHandler: used e.g. when you want to silence a library’s logging

9 And …few key points around setting various handlers formatter

And …few key points around setting various handlers formatter

If you have noticed the above example there is only formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s: %(message)s')  

 

Formatter uses various attribute to include in log record ans same can be referred from below table  

 

Attribute name Format Description
args You shouldn’t need to
format this yourself.
The tuple of arguments merged into msg to
produce message, or a dict whose values
are used for the merge (when there is only one
argument, and it is a dictionary).
asctime %(asctime)s Human-readable time when the
LogRecord was created. By default
this is of the form ‘2003-07-08 16:49:45,896’
(the numbers after the comma are millisecond
portion of the time).
created %(created)f Time when the LogRecord was created
(as returned by time.time()).
exc_info You shouldn’t need to
format this yourself.
Exception tuple (à la sys.exc_info) or,
if no exception has occurred, None.
filename %(filename)s Filename portion of pathname.
funcName %(funcName)s Name of function containing the logging call.
levelname %(levelname)s Text logging level for the message
('DEBUG', 'INFO', 'WARNING',
'ERROR', 'CRITICAL').
levelno %(levelno)s Numeric logging level for the message
(DEBUG, INFO,
WARNING, ERROR,
CRITICAL).
lineno %(lineno)d Source line number where the logging call was
issued (if available).
message %(message)s The logged message, computed as msg %
args
. This is set when
Formatter.format() is invoked.
module %(module)s Module (name portion of filename).
msecs %(msecs)d Millisecond portion of the time when the
LogRecord was created.
msg You shouldn’t need to
format this yourself.
The format string passed in the original
logging call. Merged with args to
produce message, or an arbitrary object
name %(name)s Name of the logger used to log the call.
pathname %(pathname)s Full pathname of the source file where the
logging call was issued (if available).
process %(process)d Process ID (if available).
processName %(processName)s Process name (if available).
relativeCreated %(relativeCreated)d Time in milliseconds when the LogRecord was
created, relative to the time the logging
module was loaded.
stack_info You shouldn’t need to
format this yourself.
Stack frame information (where available)
from the bottom of the stack in the current
thread, up to and including the stack frame
of the logging call which resulted in the
creation of this record.
thread %(thread)d Thread ID (if available).
threadName %(threadName)s Thread name (if available).

(Source- Python official Document https://docs.python.org/3/library/logging.html#formatter-objects)

Well. You can specify multiple formatter based on need and usages …and then set  handler file_handler.setFormatter(file_formatter) 

Example –

stream_formatter = logging.Formatter("[%(levelname)s] %(message)s")
file_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

In this way; you have configured two handlers formatter to be used with two different stream & file handler.

10 Next…How about writing own LOG HANDLER AND FORMATTER

As explained above, Log Handlers dictate how the log entries are handled. Customised log handlers can be written to perform

Writing a custom handler is simple you need to subclass it from logging.Handler class and must define the emit method.

Here’s an example of a custom log handler which send critical message using sms to configured mobile no;  minimum you need to subclass it from logging.Handler class and must define the emit method. 

  • SMSHandler- Class that is been extended from logging.Handler class & emit method  take care of sending message on mobile 

***sms is not python library i have written with method send_msg which takes mobile_no and msg as parameter.

import logging
import os.path

# We need to send Message on Mobile to Support Team for Critical Alerts
LOG_FILENAME= 'error.log'

# Inherit from logging.Handler
class SMSHandler(logging.Handler):  # Inherit from logging.Handler
    def __init__(self, mobile_no):
        # run the regular Handler __init__
        logging.Handler.__init__(self)
        # Our custom argument
        self.mobile_no = mobile_no

    def emit(self, record):
        # record.message is the log message ...we will use send_sms method of sms module which takes two parmeter to send Text message
        sms.send_sms(self.mobile_no,record.message)


if '__name__' == '__main__':
    # Create a logging object (after configuring logging)
    logger = logging.getLogger()

    # create error file handler and set level to error
    handler = logging.FileHandler(os.path.join(LOG_FILENAME), "w", encoding=None, delay="true")
    handler.setLevel(logging.ERROR)
    formatter = logging.Formatter("%(levelname)s - %(message)s")
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    # an SMSHandler object
    SMSHandler = SMSHandler('+91 9999911111')
    # Configure the handler to only send SMS for critical errors
    SMSHandler.setLevel(logging.CRITICAL)
    # and finally we add the handler to the logging object
    logger.addHandler(SMSHandler)


Now, SMSHandler  and be further customised add custom formatter.

A formatter has a format method which gets the record. You can take the record and return a message formatted as per your need and add the context information you require.

class LogsFormatter(Formatter):
    def __init__(self, action=None):
        self.action = action

        super(LogsFormatter, self).__init__()

    def format(self, record):
        data = {'@message': record.msg,
                '@timestamp': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
                }

        if self.action:
            data['@action'] = self.action

        return json.dumps(data)


Next …lets see the Formatter defined above in action.

import logging
import os.path
import datetime
from logging import Formatter
import json
import sms # Other module with send_sms method
# We need to send Message on Mobile to Support Team for Critical Alerts

LOG_FILENAME= 'error.log'

# Inherit from logging.Handler
class SMSHandler(logging.Handler):  # Inherit from logging.Handler
    def __init__(self, mobile_no):
        # run the regular Handler __init__
        logging.Handler.__init__(self)
        # Our custom argument
        self.mobile_no = mobile_no

    def emit(self, record):
        # record.message is the log message ...we will use send_sms method of sms module which takes two parmeter to send Text message
        sms.send_sms(self.mobile_no,record.message)


class LogsFormatter(Formatter):
    def __init__(self, action=None):
        self.action = action

        super(LogsFormatter, self).__init__()

    def format(self, record):
        data = {'@message': record.msg,
                '@timestamp': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
                }

        if self.action:
            data['@action'] = self.action

        return json.dumps(data)

if '__name__' == '__main__':
    # Create a logging object (after configuring logging)
    logger = logging.getLogger()

    # create error file handler and set level to error
    handler = logging.FileHandler(os.path.join(LOG_FILENAME), "w", encoding=None, delay="true")
    handler.setLevel(logging.ERROR)
    formatter = logging.Formatter("%(levelname)s - %(message)s")
    logformatter = LogsFormatter()
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    # an SMSHandler object
    SMSHandler = SMSHandler('+91 9999911111')
    # Configure the handler to only send SMS for critical errors
    SMSHandler.setFormatter(logformatter)
    SMSHandler.setLevel(logging.CRITICAL)
    # and finally we add the handler to the logging object
    logger.addHandler(SMSHandler)

    # And finally a Checking Logs with logger example
    logger.debug('Checking Logs with logger example 1')
    logger.info('Checking Logs with logger example 2')
    logger.warning('Checking Logs with logger example 3')
    logger.error('Checking Logs with logger example 4')
    logger.critical('Checking Logs with logger example 5')

If you to run this above script then the only message that would be sent as an SMS would be the last one,

And this code is simple enough that I doubt anyone using the logging module would  face any issue if they want to employ this how ever this is sample code and not production ready …you must take care of other stuff to write better code !

11 Log formatting with colors!

In some batch based application, there are requirement for logs out records must be in different colors based on log level …specially when you log on stream… I mean you want to show INFO records in Green while ERROR records in Red Color.

You can always write your own log handler and formatter using methods shown above. How ever there are many Free/Open Source code available on Github can be use to test your requirement.

Lets take example of such module available on github which provide Colored terminal output for Python’s logging module.

  • https://github.com/xolox/python-coloredlogs
  • https://github.com/borntyping/python-colorlog

In the below example we will use -python-coloredlogs module to see its working.

The coloredlogs package is available on PyPI which means installation should be as simple as:

techfossguru@techfossguru:~$ pip install coloredlogs
Collecting coloredlogs
  Downloading https://files.pythonhosted.org/packages/08/0f/7877fc42fff0b9d70b6442df62d53b3868d3a6ad1b876bdb54335b30ff23/coloredlogs-10.0-py2.py3-none-any.whl (47kB)
    100% |████████████████████████████████| 51kB 347kB/s 
Collecting humanfriendly>=4.7 (from coloredlogs)
  Downloading https://files.pythonhosted.org/packages/e8/c0/6586306983dcc3062abd2a4a2ea74f019ea06de4f289f39409763ed17bee/humanfriendly-4.15-py2.py3-none-any.whl (69kB)
    100% |████████████████████████████████| 71kB 868kB/s 
Installing collected packages: humanfriendly, coloredlogs
Successfully installed coloredlogs-10.0 humanfriendly-4.15

Here’s an example of how easy it is to get started:

import coloredlogs, logging

# Create a logger object.
logger = logging.getLogger(__name__)

# By default the install() function installs a handler on the root logger,
# this means that log messages from your code and log messages from the
# libraries that you use will all show up on the terminal.
coloredlogs.install(level='DEBUG')

# If you don't want to see log messages from libraries, you can pass a
# specific logger object to the install() function. In this case only log
# messages originating from that logger will show up on the terminal.
coloredlogs.install(level='DEBUG', logger=logger)

# Some examples.
logger.debug("this is a debugging message")
logger.info("this is an informational message")
logger.warning("this is a warning message")
logger.error("this is an error message")
logger.critical("this is a critical message")

 

12 Using Python logger with Flask Based Application

How to use python logger with flask,  How Werkzeug logs are different

This is extremely easy and quick to set up python logger with flask. Its simple, because …

Flask is using the python standard logging module to do things … Here’s the basic example …to register logger with simple app.

from flask import Flask
import logging
from logging import Formatter, FileHandler

app = Flask(__name__)



@app.route('/')
def hello_world():
    app.logger.info('second test message...')
    return 'Hello World!'


if __name__ == '__main__':
    #Setup the logger
    file_handler = FileHandler('output.log')
    handler = logging.StreamHandler()
    file_handler.setLevel(logging.DEBUG)
    handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(Formatter(
        '%(asctime)s %(levelname)s: %(message)s '
        '[in %(pathname)s:%(lineno)d]'
     ))
     handler.setFormatter(Formatter(
        '%(asctime)s %(levelname)s: %(message)s '
        '[in %(pathname)s:%(lineno)d]'
     ))
     app.logger.addHandler(handler)
     app.logger.addHandler(file_handler)
     app.logger.debug('first test message...')
     app.run()

when you run this you will see nothing in the output.log file. The reason why you cant see any thing in the log file is app.run()  

You need to run the application in DEBUG mode to see the logs… let change app.run() -> app.run(debug=True) 

Then..You will be able to see something like below-

/home/techfossguru/anaconda3/bin/python /home/techfossguru/PycharmProjects/betterlogs/app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 319-307-242
--------------------------------------------------------------------------------
INFO in app [/home/techfossguru/PycharmProjects/betterlogs/app.py:11]:
second test message...
--------------------------------------------------------------------------------
2018-07-18 21:59:08,885 INFO: second test message... [in /home/techfossguru/PycharmProjects/betterlogs/app.py:11]
127.0.0.1 - - [18/Jul/2018 21:59:08] "GET / HTTP/1.1" 200 

What we’re seeing above is Werkzeug (a WSGI utility library for Python, which Flask uses out-of-the-box) output.

… in the output.log[ logfile ] you will see below!

2018-07-18 21:59:08,885 INFO: second test message... [in /home/techfossguru/PycharmProjects/betterlogs/app.py:11]

Configuration of logging for flask depend upon how you are using your application …if you are using factory model ..you should register the logger through app init  ‘OR’ simply defying them in method.

So I won’t cover it in every detail again here and instead give you a brief overview.

[Below code snippet is not complete and will not run if you try to run …this will require other config variables defined in config.py file in same directory …for sake of simplicity removed other part of code and to highlight only logging setup]

You can should checkout complete working code by using Fbone [ Flask (Python microframework) starter/template/bootstrap/boilerplate application. Github link  ]

# -*- coding: utf-8 -*-

from flask import Flask, render_template
app = Flask(__name__)

def configure_logging(app):
    """Configure file(info) and email(error) logging."""
    if app.debug or app.testing:
        # Skip debug and test mode. Just check standard output.
        return

    import logging
    import os
    from logging.handlers import SMTPHandler

    # Set info level on logger, which might be overwritten by handers.
    # Suppress DEBUG messages.
    app.logger.setLevel(logging.INFO)

    info_log = os.path.join(app.config['LOG_FOLDER'], 'info.log')
    info_file_handler = logging.handlers.RotatingFileHandler(info_log, maxBytes=100000, backupCount=10)
    info_file_handler.setLevel(logging.INFO)
    info_file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s '
        '[in %(pathname)s:%(lineno)d]')
    )
    app.logger.addHandler(info_file_handler)

    mail_handler = SMTPHandler(app.config['MAIL_SERVER'],
                               app.config['MAIL_USERNAME'],
                               app.config['ADMINS'],
                               'Your Application Failed!',
                               (app.config['MAIL_USERNAME'],
                                app.config['MAIL_PASSWORD']))
    mail_handler.setLevel(logging.ERROR)
    mail_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s '
        '[in %(pathname)s:%(lineno)d]')
    )
    app.logger.addHandler(mail_handler)


@app.route('/')
def hello_world():
    app.logger.info('second test message...')
    # Testing
    #app.logger.info("testing info.")
    #app.logger.warn("testing warn.")
    #app.logger.error("testing error.")
    return 'Hello World!'


if __name__ == '__main__':
     #Logger Config
     configure_logging(app)
     app.logger.debug('first test message...')
     app.run()

 

Well …above configuration will work and will send you email alerts based on severity of LOGLEVEL.

let take another use case where we need to save logs in db for audit and analysis …

13 How about custom log handler that logs to database models

Audit logging records events are useful for business analysis.

It gives context details around user’s transactions, behaviour and operation that can be extracted and further used  with other user details for reports or to optimise marketing goal.

For example –

  • How many user has bought First product on website after first login
  • On what days on Month users are more active or Max no of DB Connection were exhausted !

In this section, we will see how the log records can be inserted into database models so that they can be further used with conjunction with other events to define meaningful result.

We will create single page web application using flask to show how we create own log handler to log to db models, which extends logging.Handler (in the exactly same way we have covered above for SMShandler)

lets get started.

First we need to define table/model structure in which log records to be stored.

 

 

 

14 Hiding Sensitive Data from Logs with Python

 

There are definitely more than one approach to solve this problem, but I will discuss a couple of approaches below:

  • Using a filter
  • Using a custom formatter

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

15 Ways to configure a logger

There are different ways to centrally configure your application’s logs, but I prefer to use YAML. For example:

 

Example Configuration via an INI File

Pro: possible to update configuration while running using the function tologging.config.listen() listen on a socket.

Con: less control (e.g. custom subclassed filters or loggers) than possible when configuring a logger in code.

    http://docs.python.org/library/logging.config.html#logging.config.listenUsing a dictionary or a JSON-formatted file:

Example Configuration via a Dictionary

Pro: in addition to updating while running, it is possible to load from a file using the modulejson, in the standard library since Python 2.6.

Con: less control than when configuring a logger in code.

Example Configuration Directly in Code

Pro: complete control over the configuration.

Con: modifications require a change to source code.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Comments
Loading...

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More