A couple of weeks ago I came across a new python web framework called Falcon. I was quickly impressed with its ease of use and most importantly this fragment: It doesn’t try to be everything to everyone, focusing instead on a single use case: HTTP APIs. I decided to build a simple server that will consume data from SendGrid’s Event Webhook and use Fluentd to log and store the events. Fluentd provides a sense of dependency injection when it comes to logging and storing to multiple data outputs with a simple configuration file. Building the Environment First, install Fluentd. You can find several options in their quickstart, but for the purposes of this tutorial I will use the ruby gem they have provided: $ gem install fluentd --no-ri --no-rdoc Now, lets create a folder and initialize our python environment: $ mkdir falcon_sample $ cd falcon_sample $ touch server.py $ virtualenv venv $ source venv/bin/activate $ pip install cython falcon fluent-logger gunicorn $ fluentd --setup ./fluent Notice that the last command will generate the configuration file needed to run Fluentd. Modify the fluent.conf to reflect the following settings: <source> type forward port 24224 </source> <match events.log.**> type stdout </match> This states that data will come in through port 24224 and everything that is tagged with the prefix events.log will be logged to STDOUT. We can specify multiple outputs, but for the purposes of this tutorial, we will stick to STDOUT. Fire up the Fluend Daemon by running: $ fluentd -c ./fluent/fluent.conf -vv & Now lets get our hands dirty with some python (specifically with Falcon). Starting a Falcon API server its as simple as: import falcon app = falcon.API() Falcon has something called Hooks. It’s similar to middleware, but it only runs before specified functions rather than before and after. For illustration purposes, lets write a simple one that verifies that the request’s content type is application/json and it’s executed before any other function: import falcon def check_content_type(req, resp, params): """Check that the request is properly encoded""" if 'application/json' not in req.content_type: # Yup, Falcon brings in a suite of exceptions which are pretty sweet raise falcon.HTTPUnsupportedMediaType('Data must be provided in application/json', href='https://sendgrid.com/docs/API_Reference/Webhooks/event.html') app = falcon.API(before=[check_content_type]) # before will run the hooks before any handler Now lets initialize our Fluentd client to start sending logs: from fluent import sender as fluent_sender from fluent import event as fluent_event fluent_sender.setup('events.log', host='localhost', port=24224) Finally, lets actually handle the Event Webhook by creating a resource. In Falcon, resources have special methods. These define which HTTP verbs will get support. To accomplish this in a semantic way, Falcon uses on_verb syntax. In our case, we only need to support HTTP POST for this example. This is how it will end up looking like: import json import falcon import logging from fluent import sender as fluent_sender from fluent import event as fluent_event fluent_sender.setup('events.log', host='localhost', port=24224) def check_content_type(req, resp, params): """Check that the request is properly encoded""" if 'application/json' not in req.content_type: raise falcon.HTTPUnsupportedMediaType('Data must be provided in application/json', href='https://sendgrid.com/docs/API_Reference/Webhooks/event.html') class EventResource: def on_post(self, req, resp): """Handle incoming requests from sendgrid""" payload = json.loads(req.stream.read().decode('utf-8')) # Requests have a JSON payload for event in payload: # Events come in batches fluent_event.Event(event['event'], event) # Tag the event with its type resp.status = falcon.HTTP_204 # Return a 204 to SendGrid app = falcon.API(before=[check_content_type]) event_resource = EventResource() app.add_route('/event', event_resource) # Set up that resource to its route There we have it! To run it, simply run: $ gunicorn server:app Hope you enjoy this tutorial! If you have any questions, feel free to reach out!