• Stars
    star
    111
  • Rank 313,713 (Top 7 %)
  • Language
    Python
  • License
    MIT License
  • Created about 3 years ago
  • Updated about 1 month ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A generic service to send, retry, and manage webhooks

logo

>> A generic service to send, retry, and manage webhooks. <<

Description

What?

Hook Slinger acts as a simple service that lets you send, retry, and manage event-triggered POST requests, aka webhooks. It provides a fully self-contained docker image that is easy to orchestrate, manage, and scale.

Why?

Technically, a webhook is a mere POST request—triggered by a system—when a particular event occurs. The following diagram shows how a simple POST request takes the webhook nomenclature when invoked by an event trigger.

Webhook Concept

However, there are a few factors that make it tricky to manage the life cycle of a webhook, such as:

  • Dealing with server failures on both the sending and the receiving end.
  • Managing HTTP timeouts.
  • Retrying the requests gracefully without overloading the recipients.
  • Avoiding retry loop on the sending side.
  • Monitoring and providing scope for manual interventions.
  • Scaling them quickly; either vertically or horizontally.
  • Decoupling webhook management logic from your primary application logic.

Properly dealing with these concerns can be cumbersome; especially when sending webhooks is just another small part of your application and you just want it to work without you having to deal with all the hairy details every time. Hook Slinger aims to alleviate this pain point.

How?

Hook Slinger exposes a single endpoint where you can post your webhook payload, destination URL, auth details, and it'll make the POST request for you asynchronously in the background. Under the hood, the service uses:

  • FastAPI to provide a Uvicorn driven ASGI server.

  • Redis and RQ for implementing message queues that provide the asynchrony and robust failure handling mechanism.

  • Rqmonitor to provide a dashboard for monitoring the status of the webhooks and manually retrying the failed jobs.

  • Rich to make the container logs colorful and more human friendly.

The simplified app architecture looks something this:

Topology

In the above image, the webhook payload is first sent to the app and the app leverages the worker instance to make the POST request. Redis DB is used for fast bookkeeping and async message queue implementation. The monitor instance provides a GUI to monitor and manage the webhooks. Multiple worker instances can be spawned to achieve linear horizontal scale-up.

Installation

  • Make sure you've got the latest version of Docker and Docker Compose V2 installed in your system.

  • Clone the repository and head over to the root directory.

  • To start the orchestra, run:

    make start-servers
    

    This will:

    • Start an app server that can be accessed from port 5000.

    • Start an Alpine-based Redis server that exposes port 6380.

    • Start a single worker that will carry out the actual tasks.

    • Start a rqmonitor instance that opens port 8899.

  • To shut down everything, run:

    make stop-servers
    

TODO: Generalize it more before making it installable with a docker pull command.

Usage

Exploring the interactive API docs

To try out the entire workflow interactively, head over to the following URL on your browser:

http://localhost:5000/docs

You should see a panel like this:

API Docs

This app implements a rudimentary token-based authentication system where you're expected to send an API token by adding Authorization: Token <token_value> field to your request header. To do that here, click the POST /hook_slinger/ ribbon and that will reveal the API description like this:

API Description

Copy the default token value from the description corpus, then click the green button on the top right that says Authorize, and paste the value in the prompt box. Click the Authorize button again and that'll conclude the login step. In your production application, you should implement a robust authentication system or at least change this default token.

To send a webhook, you'll need a URL where you'll be able to make the POST request. For this demonstration, let's pick this webhook site service to monitor the received webhooks. It gives you a unique URL against which you'll be able to make the post requests and monitor them in a dashboard like this:

Webhook Site

On the API docs page, click the Try it out button near the request body section:

API Request

This should reveal a panel like the following one where you can make your request:

API Request

Notice that the section is prefilled with an example request payload. You can use this exact payload to make a request. Go ahead and click the execute button. If you scroll down a little, you'll notice the HTTP response:

API Response

Now, if you head over to the webhook site URL, you should be able to see your API payload:

API Response

To monitor the webhook tasks, head over to the following URL:

http://localhost:8899/

You should be presented with a GUI like this:

RQ Monitor

If you click Workers on the left panel, you'll be presented with a panel where you can monitor all the workers:

RQ Monitor

The Jobs panel lists all the tasks, and from there you'll be able to requeue a failed job. By default, Hook Slinger retries a failed job 3 times with 5 seconds linear backoff. However, this can be configured using environment variables in the .env file.

RQ Monitor

Sending a webhook via cURL

Run the following command on your terminal; this assumes that you haven't changed the auth token (you should):

curl -X 'POST' \
  'http://localhost:5000/hook_slinger/' \
  -H 'accept: application/json' \
  -H 'Authorization: Token $5$1O/inyTZhNvFt.GW$Zfckz9OL.lm2wh3IewTm8YJ914wjz5txFnXG5XW.wb4' \
  -H 'Content-Type: application/json' \
  -d '{
  "to_url": "https://webhook.site/b30da7ce-c3cc-47e2-b2ae-68747b3d7789",
  "to_auth": "",
  "tag": "Dhaka",
  "group": "Bangladesh",
  "payload": {
    "greetings": "Hello, world!"
  }
}' | python -m json.tool

You should expect the following output:

{
    "status": "queued",
    "ok": true,
    "message": "Webhook registration successful.",
    "job_id": "Bangladesh_Dhaka_a07ca786-0b7a-4029-bac0-9a7c6eb68a98",
    "queued_at": "2021-11-06T16:54:54.728999"
}

Sending a webhook via Python

For this purpose, you can use an HTTP library like httpx.

Make the request with the following script:

import asyncio
from http import HTTPStatus
from pprint import pprint

import httpx


async def send_webhook() -> None:
    wh_payload = {
        "to_url": "https://webhook.site/b30da7ce-c3cc-47e2-b2ae-68747b3d7789",
        "to_auth": "",
        "tag": "Dhaka",
        "group": "Bangladesh",
        "payload": {"greetings": "Hello, world!"},
    }

    async with httpx.AsyncClient(http2=True) as session:
        headers = {
            "Content-Type": "application/json",
            "Authorization": (
                "Token $5$1O/inyTZhNvFt.GW$Zfckz9OL.lm2wh3IewTm8YJ914wjz5txFnXG5XW.wb4"
            ),
        }

        response = await session.post(
            "http://localhost:5000/hook_slinger",
            headers=headers,
            json=wh_payload,
            follow_redirects=True,
        )

        # Hook Slinger returns http code 202, accepted, for a successful request.
        assert response.status_code == HTTPStatus.ACCEPTED
        result = response.json()
        pprint(result)


if __name__ == "__main__":
    asyncio.run(send_webhook())

This should return a similar response as before:

{
    'job_id': 'Bangladesh_Dhaka_139fc35a-d2a5-4d01-a6af-e980c52f55bc',
    'message': 'Webhook registration successful.',
    'ok': True,
    'queued_at': '2021-07-23T20:15:04.389690',
    'status': 'queued'
}

Exploring the container logs

Hook Slinger overloads the Python root logger to give you a colorized and user-friendly logging experience. To explore the logging messages of the application server, run:

make app-logs

Notice the colorful logs cascading down from the app server:

App Logs

Now, to explore the worker instance logs, in a separate terminal, run:

make worker-logs

You should see something like this:

Worker Logs

Scaling up the service

Hook Slinger offers easy horizontal scale-up, powered by the docker-compose --scale command. In this case, scaling up means, spawning new workers in separate containers. Let's spawn 3 worker containers this time. To do so, first shut down the orchestra by running:

make stop-servers

Now, run:

make worker-scale n=3

This will start the App server, Redis DB, RQmonitor, and 3 Worker instances. Spawning multiple worker instances are a great way to achieve job concurrency with the least amount of hassle.

Troubleshooting

On the Rqmonitor dashboard, if you see that your webhooks aren't reaching the destination, make sure that the destination URL in the webhook payload can accept the POST requests sent by the workers. Your webhook payload looks like this:

{
    "to_url": "https://webhook.site/f864d28d-9162-4ad5-9205-458e2b561c07",
    "to_auth": "",
    "tag": "Dhaka",
    "group": "Bangladesh",
    "payload": {"greetings": "Hello, world!"},
}

Here, to_url must be able to receive the payloads and return HTTP code 201.

Philosophy & limitations

Hooks Slinger is designed to be simple, transparent, upgradable, and easily extensible to cater to your specific needs. It's not built around AMQP compliant message queues with all the niceties and complexities that come with them—this is intentional.

Also, if you scrutinize the end-to-end workflow, you'll notice that it requires making HTTP requests from the sending service to the Hook Slinger. This inevitably adds another point of failure. However, from the sending service's POV, it's sending the HTTP requests to a single service, and the target service is responsible for fanning out the webhooks to the destinations. The developers are expected to have control over both services, which theoretically should mitigate the failures. The goal is to transfer some of the code complexity around managing webhooks from the sending service over to the Hook Slinger. Also, I'm playing around with some of the alternatives to using HTTP POST requests to send the payloads from the sending end to the Hook Slinger. Suggestions are always appreciated.

✨ 🍰 ✨

More Repositories

1

fastapi-nano

🐍 Simple FastAPI template employing divisional architecture pattern
Python
843
star
2

think-async

🌿 Exploring cooperative concurrency primitives in Python
Python
232
star
3

pysanity

Opinionated coding guidelines and best practices in Python
208
star
4

urban-sound-classification

Urban sound source tagging from an aggregation of four second noisy audio clips via 1D and 2D CNN (Xception)
Jupyter Notebook
58
star
5

konfik

🐍 The strangely familiar config parser
Python
54
star
6

stress-test-locust

Template for stress testing with Python, Locust & Docker
Python
46
star
7

rush

🏃‍♀️ Minimalistic CLI Tool for Managing and Running Bash Snippets
Python
37
star
8

fork-purger

Delete all of your forked repositories on Github
Python
30
star
9

rubric

Linter config initializer for Python
Python
28
star
10

reflections

Musings on software
JavaScript
24
star
11

epilog

Docker container log aggregation with Elasticsearch, Kibana & Filebeat
Python
23
star
12

fork-sweeper

Remove unused GitHub forks
Go
23
star
13

link-patrol

Detect dead links in markdown files
Go
22
star
14

httpurr

ᗢ HTTP status codes on speed dial
Go
19
star
15

github-follow-bot

Bot that follows everyone who follows the target user(s)
Python
19
star
16

redis-request-caching

🐍 Simple Python App to Demonstrate API Request Caching with Redis
Python
18
star
17

flask-factory

Dockerized flask app template with factory pattern architecture
Python
13
star
18

fly-fastapi

Deploying a simple FastAPI app to Fly.io >> https://fly-fastapi.fly.dev/docs <<
Makefile
12
star
19

rednafi.com

Musings & rants on software
HTML
10
star
20

py-vscode

An Opinionated Beginner Friendly Guide for Optimizing Python Coding Experience in Visual Studio Code. Check it out here: https://py-vscode.readthedocs.io/en/latest/index.html
10
star
21

prinumco

Algorithmically generating Bengali digits and classification using MobileNetV2 for Bangladeshi license plate detection
Python
10
star
22

digressions

Sporadic cogitations on software, tech & personal beliefs
HTML
9
star
23

celery-template

Simple celery template to demonstrate chaining and task routing
Python
7
star
24

indoor-movement-prediction

Predicting user movements from temporal streams of RSS (Radio Signal Strength) measured between the nodes of a WSN (Wireless Sensor Network WSN)
Jupyter Notebook
7
star
25

dot

🎲 Dotfiles & workspace setup
Shell
7
star
26

exert

Declaratively apply converter functions to class attributes.
Python
6
star
27

docker-local-subdomain

Locally access Docker services as 'subdomain.localhost' instead of 'localhost:port'
Python
5
star
28

crusher

Crush a deeply nested JSON string
Python
5
star
29

ashen

Redisearch based cross-language fuzzy search engine
Python
5
star
30

rednafi

5
star
31

lego

Learning + Go : Documenting the journey as I Go along
Go
4
star
32

i-have-seen

Automatically add a single comment every time an ISSUE or a PULL REQUEST is created
3
star
33

cronies

Bespoke cron jobs ✵
Python
3
star
34

cors-proxy

Simple CORS proxy server built with Cloudflare Workers
JavaScript
3
star
35

django-template-delocalized

POC for Decoupled Template Rendering in a Django Application
Python
3
star
36

django-epiphanies

Lessons, musings, and resources on web development with Django
3
star
37

city-map

A simple CLI tool to generate beautiful city map with the roads color coded by lenghts
Jupyter Notebook
2
star
38

django-polls

Djagno polls app for experimentation.
Python
2
star
39

scradium

Scraping blog posts from top data science publications on Medium
Julia
2
star
40

django-unchained

Dipping toes into the land of Python's Django framework
Python
2
star
41

stackoverflow-login

Automated login to stackoverflow
Python
2
star
42

pypress

An intimate journey to clearing away conceptual fogs in Python
2
star
43

html-to-text

Extract pure text from any webpage
Python
2
star
44

sqlite-xplore

Exploring sqlite
1
star
45

scratchpad

1
star
46

get-ignore

A CLI tool for downloading gitignore files with a single command
Go
1
star
47

rust-book

Working my way through the canonical Rust book
Rust
1
star
48

backtick

A tiny fixed-point task scheduling app built on top of rq
Python
1
star
49

tcp-port-scanner

Simple IPV4 Port Scanner Leveraging Python's Asyncio
Python
1
star
50

example-rq-sentry

Python
1
star
51

example-pull-rebase

Experimenting with 'git pull' and 'git pull --rebase'
1
star
52

brisk-SQL

Up and running with MySQL on Linux in no time
1
star
53

brisk-bash

Up and running with shell scripting in minutes
Shell
1
star
54

serve-init

Initialize an Ubuntu VM
Go
1
star
55

nginx-ratelimit

A simple hello-world JSON API service that incorporates rate limiting via Nginx.
Go
1
star
56

django-jupyter

Connecting an ipython notebook server to a dockerized Django application
Python
1
star