Building a Custom Log Inspection Tool with Django and Nginx
Implementing a centralized log viewer allows for efficient diagnostics without the overhead of complex database-driven analysis platforms. This approach utilizes Nginx paired with uWSGI to serve a Django application, bypassing the configuration difficulties often encountered with Apache. The system reads flat log files directly from the filesystem based on user-defined dates and keywords.
Project Configuration
The Django configuration is adjusted to serve static assets and define the search application module. Static file handling is explicitly mapped to the project directory.
# settings.py
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'change_this_to_a_random_string'
DEBUG = False
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'log_viewer',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'log_project.urls'
WSGI_APPLICATION = 'log_project.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
URL Routing
The routing structure handles authentication, the search dashboard, and logout actions.
# urls.py
from django.conf.urls import url
from django.contrib import admin
from log_viewer import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', views.auth_view, name='login'),
url(r'^dashboard/', views.search_view, name='dashboard'),
url(r'^signout/', views.logout_view, name='logout'),
]
View Logic
The core logic involves authenticating users and executing system commands to search specific log directories. The application constructs a file path based on the selected date and uses grep to filter content.
# views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from django import forms
from django.contrib.admin.widgets import AdminDateWidget
import subprocess
import os
from datetime import datetime
class AuthForm(forms.Form):
user_id = forms.CharField(max_length=100)
pass_key = forms.CharField(widget=forms.PasswordInput)
class LogSearchForm(forms.Form):
query = forms.CharField(label="Search Query", min_length=1)
log_date = forms.DateTimeField(label='Log Date', widget=AdminDateWidget())
def auth_view(request):
if request.user.is_authenticated:
return redirect('/dashboard/')
msg = ""
if request.method == 'POST':
form = AuthForm(request.POST)
if form.is_valid():
uid = form.cleaned_data['user_id']
pwd = form.cleaned_data['pass_key']
user = authenticate(username=uid, password=pwd)
if user and user.is_active:
login(request, user)
return redirect('/dashboard/')
else:
msg = "Invalid credentials"
else:
form = AuthForm()
return render(request, 'login.html', {'form': form, 'msg': msg})
def search_view(request):
if not request.user.is_authenticated:
return redirect('/')
context = {'form': LogSearchForm(initial={'log_date': datetime.now()})}
if request.method == 'POST':
form = LogSearchForm(request.POST)
if form.is_valid():
keyword = form.cleaned_data['query']
target_date = form.cleaned_data['log_date']
# Prevent command injection characters
if '|' in keyword:
context['error'] = "Illegal character '|' detected."
return render(request, 'dashboard.html', context)
# Construct path: /var/logs/YYYY/MM/DD
date_path = os.path.join(
'/var/logs/',
str(target_date.year),
f"{target_date.month:02d}",
f"{target_date.day:02d}"
)
if not os.path.exists(date_path):
context['error'] = "No logs found for this date."
return render(request, 'dashboard.html', context)
# Execute grep command
file_pattern = os.path.join(date_path, '*')
cmd = f"sudo grep '{keyword}' {file_pattern}"
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
results = stdout.decode('utf-8').split('\n')
# Filter empty lines
clean_results = [line for line in results if line.strip()]
context['results'] = clean_results
context['form'] = form
return render(request, 'dashboard.html', context)
def logout_view(request):
logout(request)
return render(request, 'login.html')
HTML Templates
The interface relies on Bootstrap for layout.
Base Template html {% load staticfiles %}
Login Template html {% extends "base.html" %} {% block content %}
Dashboard Template html {% extends "base.html" %} {% block content %}
{% if error %}
<div class="alert alert-warning" style="margin-top: 20px;">{{ error }}</div>
{% endif %}
{% if results %}
<div style="margin-top: 20px;">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th width="5%">#</th>
<th>Log Entry</th>
</tr>
</thead>
<tbody>
{% for line in results %}
<tr>
<td>{{ forloop.counter }}</td>
<td><code>{{ line }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>