Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

52 analytics and monitoring #54

Merged
merged 14 commits into from
May 23, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Changelog
[Unreleased]
------------

- Analytics and Monitoring (#52)

[v1.0.2] - 2023-09-16
------------------

Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ format:
${bin}black ${pysources}

migrations:
@echo "Running migrations..."
${bin}python -m scripts.makemigrations


migrations-check:
@echo "Running migrations checks..."
${bin}python -m scripts.makemigrations --check

test:
Expand Down
89 changes: 89 additions & 0 deletions docs/analytics.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
======================
API Key Analytics Middleware
======================

The API Key Analytics Middleware is a component of the `rest_framework_simple_api_key` package that provides
real-time analytics on API key usage. It records each API request, tracking which endpoints are accessed and how frequently.

API Key Analytics Middleware Usage cases
=====================================

Here is why you can use the analytics feature in your application:

- **Enhanced Decision Making**: It provides comprehensive data on API usage patterns, enabling better resource management and optimization decisions.

- **Accurate Billing**: You can use it for precise billing by tracking each user's API usage, ensuring customers are billed based on actual usage.

- **Improved Security Oversight**: It monitor API access patterns that can help you quickly detect and respond to unauthorized or suspicious activities.


This page details how to integrate and configure the API Key Analytics Middleware in your Django project,
and explains its operation.

Middleware Overview
-------------------

The `ApiKeyAnalyticsMiddleware` automatically tracks access to different API endpoints by intercepting API requests.
It logs each access in the database, allowing you to monitor API usage and optimize API key allocations.

Setup
-----

To use the `ApiKeyAnalyticsMiddleware`, follow these setup instructions:

1. Ensure the middleware app `rest_framework_simple_api_key.analytics` is included in the ``INSTALLED_APPS`` setting
of your Django project.

.. code-block:: python

INSTALLED_APPS = (
...
"rest_framework",
"rest_framework_simple_api_key",
"rest_framework_simple_api_key.analytics", # Ensure this app is added
)

2. Add the `ApiKeyAnalyticsMiddleware` to the `MIDDLEWARE` settings in your Django configuration.

.. code-block:: python

MIDDLEWARE = [
...
'django.middleware.security.SecurityMiddleware',
'rest_framework_simple_api_key.analytics.middleware.ApiKeyAnalyticsMiddleware', # Add the middleware here
...
]

3. Run the migrate command to create the necessary database tables:

.. code-block:: shell

python manage.py migrate rest_framework_simple_api_key_analytics

Activation
----------

The middleware is activated as soon as it is added to the `MIDDLEWARE` list and the project is restarted.
No further actions are required to start collecting data.

How the Middleware Works
------------------------

Once activated, the middleware performs the following functions:

1. **Request Interception**: Upon receiving an API request, the middleware extracts the API key used to authenticate the request.

2. **Endpoint Tracking**: It logs the endpoint accessed by the API key.

3. **Data Storage**: All access data is stored in the `ApiKeyAnalytics` model, which can be queried to retrieve analytics.

Data Access
-----------

To access the analytics data:

1. Use Django's admin interface to view and manage the data collected by the middleware.

2. Access the `ApiKeyAnalytics` model through Django's ORM to perform custom queries or export data for further analysis.


14 changes: 1 addition & 13 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# -- Project information -----------------------------------------------------

project = "Django REST Simple Api Key"
copyright = "2023, Kolawole with ❤️"
copyright = "2024, koladev with ❤️"
author = "Kolawole Mangabo"


Expand Down Expand Up @@ -49,15 +49,3 @@
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Contents
permissions
authentication
rotation
analytics
development_and_contributing
changelog

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ build-backend = "setuptools.build_meta"

[project]
name = "djangorestframework-simple-apikey"
version = "1.0.2"
version = "1.0.2"
dynamic = ["description", "readme", "optional-dependencies", "dependencies", "classifiers", "authors", "license"]
2 changes: 1 addition & 1 deletion rest_framework_simple_api_key/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def save_model(
change: bool = False,
) -> None:
"""
If there is obj.pk, it means that the object is been created. We need then to display the
If there is obj.pk, it means that the object is being created. We need then to display the
`api_key` value in the Django admin dashboard.
"""

Expand Down
Empty file.
23 changes: 23 additions & 0 deletions rest_framework_simple_api_key/analytics/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.contrib import admin
from .models import ApiKeyAnalytics


class ApiKeyAnalyticsAdmin(admin.ModelAdmin):
list_display = ("id", "request_number", "accessed_endpoints", "api_key")

list_filter = (
"request_number",
"api_key",
)

def has_add_permission(self, request):
return False

def has_change_permission(self, request, obj=None):
return False

def has_delete_permission(self, request, obj=None):
return False


admin.site.register(ApiKeyAnalytics, ApiKeyAnalyticsAdmin)
7 changes: 7 additions & 0 deletions rest_framework_simple_api_key/analytics/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.apps import AppConfig


class AnalyticsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "rest_framework_simple_api_key.analytics"
label = "rest_framework_simple_api_key_analytics"
26 changes: 26 additions & 0 deletions rest_framework_simple_api_key/analytics/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from rest_framework_simple_api_key.analytics.models import ApiKeyAnalytics
from rest_framework_simple_api_key.crypto import get_crypto
from rest_framework_simple_api_key.parser import APIKeyParser
from rest_framework_simple_api_key.utils import get_key


class ApiKeyAnalyticsMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.

response = self.get_response(request)

key = get_key(APIKeyParser(), request)

payload = get_crypto().decrypt(key)

# Use the custom manager to handle endpoint access logging
ApiKeyAnalytics.objects.add_endpoint_access(
api_key_id=payload["_pk"], endpoint=request.path
)

return response
40 changes: 40 additions & 0 deletions rest_framework_simple_api_key/analytics/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 5.0.6 on 2024-05-22 22:37

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("rest_framework_simple_api_key", "0002_alter_apikey_options"),
]

operations = [
migrations.CreateModel(
name="ApiKeyAnalytics",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("request_number", models.IntegerField(default=0)),
("accessed_endpoints", models.JSONField(default=dict)),
(
"api_key",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="analytics",
to="rest_framework_simple_api_key.apikey",
),
),
],
),
]
Empty file.
54 changes: 54 additions & 0 deletions rest_framework_simple_api_key/analytics/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from django.db import models
from rest_framework_simple_api_key.settings import package_settings


class ApiKeyAnalyticsManager(models.Manager):
def add_endpoint_access(self, api_key_id, endpoint):
"""
Retrieve or create ApiKeyAnalytics instance and increment the endpoint access count.
"""
obj, created = self.get_or_create(
api_key_id=api_key_id, defaults={"accessed_endpoints": {"endpoints": {}}}
)

# Initialize endpoints dictionary if it doesn't exist
if "endpoints" not in obj.accessed_endpoints:
obj.accessed_endpoints["endpoints"] = {}

# Increment endpoint count
obj.accessed_endpoints["endpoints"][endpoint] = (
obj.accessed_endpoints["endpoints"].get(endpoint, 0) + 1
)
obj.request_number += 1
obj.save()

def get_most_accessed_endpoints(self, api_key_id):
"""
Returns the most accessed endpoints for a given API key, sorted by access count.
"""
obj = self.get(api_key_id=api_key_id)
if (
"endpoints" in obj.accessed_endpoints
and obj.accessed_endpoints["endpoints"]
):
return sorted(
obj.accessed_endpoints["endpoints"].items(),
key=lambda item: item[1],
reverse=True,
)
return []


class ApiKeyAnalytics(models.Model):
api_key = models.ForeignKey(
package_settings.API_KEY_CLASS,
on_delete=models.CASCADE,
related_name="analytics",
)
request_number = models.IntegerField(default=0)
accessed_endpoints = models.JSONField(default=dict)

objects = ApiKeyAnalyticsManager()

def __str__(self):
return f"API Key {self.api_key.name} Analytics"
24 changes: 15 additions & 9 deletions rest_framework_simple_api_key/management/commands/rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,36 @@


class Command(BaseCommand):
help = 'Starts or stops a rotation based on command arguments'
help = "Starts or stops a rotation based on command arguments"

def add_arguments(self, parser):
parser.add_argument(
'--stop',
action='store_true',
help='Stops the rotation (Default is to start)',
"--stop",
action="store_true",
help="Stops the rotation (Default is to start)",
)

def handle(self, *args, **options):
if options['stop']:
if options["stop"]:
# Stop the rotation logic
try:
rotation = Rotation.objects.filter(is_rotation_enabled=True).latest('started')
rotation = Rotation.objects.filter(is_rotation_enabled=True).latest(
"started"
)
rotation.is_rotation_enabled = False
rotation.ended = timezone.now()
rotation.save()
self.stdout.write(self.style.SUCCESS('Successfully stopped rotation'))
self.stdout.write(self.style.SUCCESS("Successfully stopped rotation"))
except Rotation.DoesNotExist:
raise CommandError('No active rotation found to stop')
raise CommandError("No active rotation found to stop")
else:
# Start the rotation logic
obj = Rotation()
obj.is_rotation_enabled = True
obj.ended = timezone.now() + package_settings.ROTATION_PERIOD
obj.save()
self.stdout.write(self.style.SUCCESS(f'Successfully started rotation ending at {obj.ended}'))
self.stdout.write(
self.style.SUCCESS(
f"Successfully started rotation ending at {obj.ended}"
)
)
1 change: 0 additions & 1 deletion rest_framework_simple_api_key/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@


class Migration(migrations.Migration):

initial = True

dependencies = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@


class Migration(migrations.Migration):

dependencies = [
("rest_framework_simple_api_key", "0001_initial"),
]
Expand Down
3 changes: 3 additions & 0 deletions rest_framework_simple_api_key/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class Meta:
verbose_name = "API key"
verbose_name_plural = "API keys"

def __str__(self):
return self.name


class APIKey(AbstractAPIKey):
"""
Expand Down
1 change: 0 additions & 1 deletion rest_framework_simple_api_key/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class APIKeyParser:
message = "No API key provided."

def get(self, request: HttpRequest) -> typing.Optional[str]:

return self.get_from_authorization(request)

def get_from_authorization(self, request: HttpRequest) -> typing.Optional[str]:
Expand Down
Loading
Loading