15

I'm trying to use HTTPHandler class of standard python logging library to send logs. I need to make a https post request with basic credentials(username and password). This is how i'm setting up the HTTPHandler-

host = 'example.com'
url = '/path'
handler = logging.handlers.HTTPHandler(host, url, method='POST', secure=True, credentials=('username','password'), context=None)
logger.addHandler(handler)

But the problem is, I'm not getting anylogs in my remote server.I'm not even seeing any exception from the handler. Am I setting up the handler arguments incorrectly? I can send similar logs using simple pythong http request-

url = 'https://username:password@example.com/path'
headers = {'content-type': 'application/json'}
jsonLog = { 'id': '4444','level': 'info', 'message': 'python log' };

r = requests.post(url, data = json.dumps(jsonLog), headers=headers)

Do i need to setup header somehow because of json content-type? If yes than how do i set that up in the httphandler?

Update

I thought I should update what I ended up doing. After numerous search i found i can create a custom handler by overriding emit() of logging.Handler.

class CustomHandler(logging.Handler):
    def emit(self, record):
        log_entry = self.format(record)
        # some code....
        url = 'url'
        # some code....
        return requests.post(url, log_entry, headers={"Content-type": "application/json"}).content

Feel free to post if any has any better suggestions.

Jthorpe
  • 9,756
  • 2
  • 49
  • 64
saz
  • 955
  • 5
  • 15
  • 26
  • 1
    The `HTTPHandler` doesn't use JSON - it sends data as `application/x-www-form-urlencoded`. So if your server is expecting JSON, I would expect it to fail. Are you seeing these requests in the web server's access log? – Vinay Sajip Jul 25 '18 at 18:53
  • yeah the server is expecting json. unfortunately I don't have access to the server log. Is there any way to change the content-type to `application/json`? like setting headers in the handler? – saz Jul 25 '18 at 19:08

3 Answers3

9

Expanding on the solution saz gave, here's how add a custom HTTP handler that will forward the logs emitted to the specified URL using a bearer token.

It uses a requests session instead of having to establish a new session every log event.

Furthermore, if the request fails it attempts to resend the logs for a given number of retries.

Note: make sure your logging handler is as simple as possible to prevent the application from halting because of a log event.

I tested it with a simple localhost echo server and it works.

Feel free to suggest any changes.

import json
import logging
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

class CustomHttpHandler(logging.Handler):
    def __init__(self, url: str, token: str, silent: bool = True):
        '''
        Initializes the custom http handler
        Parameters:
            url (str): The URL that the logs will be sent to
            token (str): The Authorization token being used
            silent (bool): If False the http response and logs will be sent 
                           to STDOUT for debug
        '''
        self.url = url
        self.token = token
        self.silent = silent

        # sets up a session with the server
        self.MAX_POOLSIZE = 100
        self.session = session = requests.Session()
        session.headers.update({
            'Content-Type': 'application/json',
            'Authorization': 'Bearer %s' % (self.token)
        })
        self.session.mount('https://', HTTPAdapter(
            max_retries=Retry(
                total=5,
                backoff_factor=0.5,
                status_forcelist=[403, 500]
            ),
            pool_connections=self.MAX_POOLSIZE,
            pool_maxsize=self.MAX_POOLSIZE
        ))

        super().__init__()

    def emit(self, record):
        '''
        This function gets called when a log event gets emitted. It recieves a
        record, formats it and sends it to the url
        Parameters:
            record: a log record
        '''
        logEntry = self.format(record)
        response = self.session.post(self.url, data=logEntry)

        if not self.silent:
            print(logEntry)
            print(response.content)

# create logger
log = logging.getLogger('')
log.setLevel(logging.INFO)

# create formatter - this formats the log messages accordingly
formatter = logging.Formatter(json.dumps({
    'time': '%(asctime)s',
    'pathname': '%(pathname)s',
    'line': '%(lineno)d',
    'logLevel': '%(levelname)s',
    'message': '%(message)s'
}))

# create a custom http logger handler
httpHandler = CustomHttpHandler(
    url='<YOUR_URL>',
    token='<YOUR_TOKEN>',
    silent=False
)

httpHandler.setLevel(logging.INFO)

# add formatter to custom http handler
httpHandler.setFormatter(formatter)

# add handler to logger
log.addHandler(httpHandler)

log.info('Hello world!')
Istvan
  • 121
  • 2
  • 5
  • 1
    couldn't you use threading to put the uploading process in a seperate thread so it won't slow your application down if the server doesn't respond quickly? – Matthijs990 Jun 16 '22 at 12:47
  • @Matthijs990 yes, great suggestion, you could use a thread to handle logging and not block the execution of the program – Istvan Jun 17 '22 at 20:45
  • better is using a threadpoolexecutor to prevend a lot of threads taking up a lot of RAM. you can use a worker with like 10 threads, see my answer for example – Matthijs990 Jun 22 '22 at 10:56
6

You will need to subclass HTTPHandler and override the emit() method to do what you need. You can use the current implementation of HTTPHandler.emit() as a guide.

Vinay Sajip
  • 95,872
  • 14
  • 179
  • 191
2

following up to istvan, you can use threads to prevent slowing down the program

import asyncio
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
import time
import json
import logging
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

class CustomHttpHandler(logging.Handler):
    def __init__(self, url: str, token: str, silent: bool = True):
        '''
        Initializes the custom http handler
        Parameters:
            url (str): The URL that the logs will be sent to
            token (str): The Authorization token being used
            silent (bool): If False the http response and logs will be sent 
                           to STDOUT for debug
        '''
        self.url = url
        self.token = token
        self.silent = silent

        # sets up a session with the server
        self.MAX_POOLSIZE = 100
        self.session = session = requests.Session()
        session.headers.update({
            'Content-Type': 'application/json',
            'Authorization': 'Bearer %s' % (self.token)
        })
    
        self.session.mount('https://', HTTPAdapter(
            max_retries=Retry(
                total=5,
                backoff_factor=0.5,
                status_forcelist=[403, 500]
            ),
            pool_connections=self.MAX_POOLSIZE,
            pool_maxsize=self.MAX_POOLSIZE
        ))
        
        super().__init__()

    def emit(self, record):
        '''
        This function gets called when a log event gets emitted. It recieves a
        record, formats it and sends it to the url
        Parameters:
            record: a log record
        '''
        
        executor.submit(actual_emit, self, record)

def actual_emit(self, record):
    
    logEntry = self.format(record)
    response = self.session.post(self.url, data=logEntry)
    print(response)

    if not self.silent:
        print(logEntry)
        print(response.content)

# create logger
log = logging.getLogger('test')
log.setLevel(logging.INFO)

# create formatter - this formats the log messages accordingly
formatter = logging.Formatter(json.dumps({
    'time': '%(asctime)s',
    'pathname': '%(pathname)s',
    'line': '%(lineno)d',
    'logLevel': '%(levelname)s',
    'message': '%(message)s'
}))

# create a custom http logger handler
httpHandler = CustomHttpHandler(
    url='<URL>',
    token='<YOUR_TOKEN>',
    silent=False
)

httpHandler.setLevel(logging.INFO)
log.addHandler(httpHandler)

def main():
    print("start")
    log.error("\nstop")
    print("now")

if __name__ == "__main__":
    main()

what this program does is send the logs to the threadpoolexecutor, with 10 max threads, if there are more logs then the threads can handle, it should queue up, this prevents slowdowns of the program. What you can also do, atleast what I am doing on my project of making a local host logging central database and viewer, I make a seperate thread on the serverside, and then instantly return a HTTP response to make it so all the database stuff happens after the HTTP resonse has been send back. This removes the need for threads on client, seen it is on localhost and then latancy is almost 0

Matthijs990
  • 637
  • 3
  • 26