django-live-logs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_live_logs-0.1.0/MANIFEST.in +1 -0
- django_live_logs-0.1.0/PKG-INFO +10 -0
- django_live_logs-0.1.0/README.md +101 -0
- django_live_logs-0.1.0/django_live_logs/__init__.py +1 -0
- django_live_logs-0.1.0/django_live_logs/consumers.py +38 -0
- django_live_logs-0.1.0/django_live_logs/handlers.py +40 -0
- django_live_logs-0.1.0/django_live_logs/routing.py +6 -0
- django_live_logs-0.1.0/django_live_logs/templates/django_live_logs/dashboard.html +226 -0
- django_live_logs-0.1.0/django_live_logs/urls.py +6 -0
- django_live_logs-0.1.0/django_live_logs/views.py +11 -0
- django_live_logs-0.1.0/django_live_logs.egg-info/PKG-INFO +10 -0
- django_live_logs-0.1.0/django_live_logs.egg-info/SOURCES.txt +15 -0
- django_live_logs-0.1.0/django_live_logs.egg-info/dependency_links.txt +1 -0
- django_live_logs-0.1.0/django_live_logs.egg-info/requires.txt +2 -0
- django_live_logs-0.1.0/django_live_logs.egg-info/top_level.txt +1 -0
- django_live_logs-0.1.0/setup.cfg +4 -0
- django_live_logs-0.1.0/setup.py +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include django_live_logs/templates *
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-live-logs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A standalone Django package to stream logs over WebSockets.
|
|
5
|
+
Author: Your Name
|
|
6
|
+
Requires-Dist: Django>=3.2
|
|
7
|
+
Requires-Dist: channels>=4.0
|
|
8
|
+
Dynamic: author
|
|
9
|
+
Dynamic: requires-dist
|
|
10
|
+
Dynamic: summary
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Django Live Logs
|
|
2
|
+
|
|
3
|
+
**A lightweight, zero-configuration package that streams your Django server logs directly to a beautiful, real-time web dashboard using WebSockets and Redis.**
|
|
4
|
+
|
|
5
|
+
 *(Imagine a beautiful dark-mode dashboard here)*
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
- ๐ **Real-Time Streaming**: Uses Django Channels and Redis to push logs instantly.
|
|
9
|
+
- ๐จ **Built-In Dashboard**: Comes with a sleek, dark-mode Tailwind CSS interface out of the box.
|
|
10
|
+
- ๐ก๏ธ **Secure by Default**: Dashboard and WebSockets are natively protected by Django's `@staff_member_required` and Session cookies.
|
|
11
|
+
- ๐งต **Non-Blocking**: Dispatches logs in a background thread to prevent ASGI async event loop collisions.
|
|
12
|
+
- ๐ **Filtering**: Instantly filter between `INFO`, `WARNING`, and `ERROR` logs.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Installation
|
|
17
|
+
|
|
18
|
+
Install the package via pip:
|
|
19
|
+
```bash
|
|
20
|
+
pip install django-live-logs
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
*Note: This package requires `channels` and `channels-redis` to be installed and configured in your project.*
|
|
24
|
+
|
|
25
|
+
## 2. Configuration
|
|
26
|
+
|
|
27
|
+
### Step A: Update `settings.py`
|
|
28
|
+
Add the package to your installed apps:
|
|
29
|
+
```python
|
|
30
|
+
INSTALLED_APPS = [
|
|
31
|
+
# ...
|
|
32
|
+
'django_live_logs',
|
|
33
|
+
]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Configure your `LOGGING` dictionary to catch everything (the root logger) and send it to the WebSocket handler:
|
|
37
|
+
```python
|
|
38
|
+
LOGGING = {
|
|
39
|
+
'version': 1,
|
|
40
|
+
'disable_existing_loggers': False,
|
|
41
|
+
'handlers': {
|
|
42
|
+
'console': {'class': 'logging.StreamHandler'},
|
|
43
|
+
'websocket': {'class': 'django_live_logs.handlers.WebSocketLogHandler'},
|
|
44
|
+
},
|
|
45
|
+
'root': {
|
|
46
|
+
'handlers': ['console', 'websocket'],
|
|
47
|
+
'level': 'INFO',
|
|
48
|
+
},
|
|
49
|
+
'loggers': {
|
|
50
|
+
'django': {
|
|
51
|
+
'handlers': ['console', 'websocket'],
|
|
52
|
+
'level': 'INFO',
|
|
53
|
+
'propagate': False,
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Step B: Route the Dashboard (HTTP)
|
|
60
|
+
In your main `urls.py`, include the dashboard URL:
|
|
61
|
+
```python
|
|
62
|
+
from django.urls import path, include
|
|
63
|
+
|
|
64
|
+
urlpatterns = [
|
|
65
|
+
# ...
|
|
66
|
+
path('live-logs/', include('django_live_logs.urls')),
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Step C: Route the WebSockets (ASGI)
|
|
71
|
+
In your `asgi.py`, make sure your WebSockets are wrapped in Django's `AuthMiddlewareStack` so it can read your admin session cookies securely. Then include the routing:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from channels.auth import AuthMiddlewareStack
|
|
75
|
+
import django_live_logs.routing
|
|
76
|
+
|
|
77
|
+
application = ProtocolTypeRouter({
|
|
78
|
+
"http": get_asgi_application(),
|
|
79
|
+
"websocket": AllowedHostsOriginValidator(
|
|
80
|
+
AuthMiddlewareStack(
|
|
81
|
+
URLRouter(
|
|
82
|
+
# Your existing websocket routes + Live Logs routes
|
|
83
|
+
your_app_routing.websocket_urlpatterns +
|
|
84
|
+
django_live_logs.routing.websocket_urlpatterns
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
),
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 3. Usage
|
|
94
|
+
|
|
95
|
+
1. Log into your standard Django Admin panel (e.g., `/admin/`).
|
|
96
|
+
2. Navigate to `/live-logs/` in your browser.
|
|
97
|
+
3. Click **Connect**.
|
|
98
|
+
4. Watch your server logs flow in real-time!
|
|
99
|
+
|
|
100
|
+
## How it Works
|
|
101
|
+
The custom `WebSocketLogHandler` intercepts Python logs. Instead of blocking the main thread, it tosses the log payload to a daemon thread. The daemon thread safely uses `async_to_sync` to dispatch the JSON payload to the Redis Channel Layer. Finally, the Django Channels WebSocket consumer securely beams it to the authenticated frontend UI.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
|
|
4
|
+
class LiveLogConsumer(AsyncJsonWebsocketConsumer):
|
|
5
|
+
async def connect(self):
|
|
6
|
+
# Allow configuration of who can view logs. Defaults to superusers only.
|
|
7
|
+
require_superuser = getattr(settings, 'LIVE_LOGS_REQUIRE_SUPERUSER', True)
|
|
8
|
+
|
|
9
|
+
user = self.scope.get("user")
|
|
10
|
+
if require_superuser and (not user or not user.is_superuser):
|
|
11
|
+
await self.close(code=4003)
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
self.group_name = "admin_live_logs"
|
|
15
|
+
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
|
16
|
+
await self.accept()
|
|
17
|
+
|
|
18
|
+
# Send an initial welcome message so the client knows it's connected
|
|
19
|
+
await self.send_json({
|
|
20
|
+
"level": "INFO",
|
|
21
|
+
"message": "Connected to django-live-logs stream.",
|
|
22
|
+
"module": "system",
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
async def disconnect(self, close_code):
|
|
26
|
+
if hasattr(self, 'group_name'):
|
|
27
|
+
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
|
28
|
+
|
|
29
|
+
async def log_message(self, event):
|
|
30
|
+
"""
|
|
31
|
+
Receives the log from the Redis channel and sends it down the WebSocket
|
|
32
|
+
"""
|
|
33
|
+
await self.send_json({
|
|
34
|
+
"level": event.get("level"),
|
|
35
|
+
"message": event.get("message"),
|
|
36
|
+
"module": event.get("module"),
|
|
37
|
+
"timestamp": event.get("timestamp")
|
|
38
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from asgiref.sync import async_to_sync
|
|
3
|
+
from channels.layers import get_channel_layer
|
|
4
|
+
import datetime
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
def _send_to_channel(channel_layer, data):
|
|
8
|
+
try:
|
|
9
|
+
async_to_sync(channel_layer.group_send)("admin_live_logs", data)
|
|
10
|
+
except Exception as e:
|
|
11
|
+
print("LIVE LOGS BACKGROUND ERROR:", e)
|
|
12
|
+
|
|
13
|
+
class WebSocketLogHandler(logging.Handler):
|
|
14
|
+
"""
|
|
15
|
+
Custom logging handler that intercepts logs and broadcasts them
|
|
16
|
+
to a Redis channel layer for real-time WebSocket streaming.
|
|
17
|
+
"""
|
|
18
|
+
def emit(self, record):
|
|
19
|
+
try:
|
|
20
|
+
log_entry = self.format(record)
|
|
21
|
+
channel_layer = get_channel_layer()
|
|
22
|
+
|
|
23
|
+
if not channel_layer:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
data = {
|
|
27
|
+
"type": "log_message",
|
|
28
|
+
"level": record.levelname,
|
|
29
|
+
"message": log_entry,
|
|
30
|
+
"module": record.module,
|
|
31
|
+
"timestamp": datetime.datetime.now().isoformat()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Run in a separate thread so it never blocks the main app
|
|
35
|
+
# and avoids ASGI async_to_sync event loop conflicts.
|
|
36
|
+
threading.Thread(target=_send_to_channel, args=(channel_layer, data), daemon=True).start()
|
|
37
|
+
|
|
38
|
+
except Exception as e:
|
|
39
|
+
# We must never crash the application just because a log failed to send
|
|
40
|
+
print("LIVE LOGS ERROR:", e)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Django Live Logs</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script>
|
|
9
|
+
tailwind.config = {
|
|
10
|
+
darkMode: 'class',
|
|
11
|
+
theme: {
|
|
12
|
+
extend: {
|
|
13
|
+
colors: {
|
|
14
|
+
gray: {
|
|
15
|
+
900: '#0f172a',
|
|
16
|
+
800: '#1e293b',
|
|
17
|
+
700: '#334155'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
<style>
|
|
25
|
+
/* Custom scrollbar */
|
|
26
|
+
::-webkit-scrollbar { width: 8px; }
|
|
27
|
+
::-webkit-scrollbar-track { background: #0f172a; }
|
|
28
|
+
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
|
29
|
+
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
|
30
|
+
.log-enter { animation: slideIn 0.3s ease-out forwards; }
|
|
31
|
+
@keyframes slideIn {
|
|
32
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
33
|
+
to { opacity: 1; transform: translateY(0); }
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body class="bg-gray-900 text-gray-100 min-h-screen font-mono flex flex-col">
|
|
38
|
+
|
|
39
|
+
<!-- Header -->
|
|
40
|
+
<header class="bg-gray-800 border-b border-gray-700 p-4 shadow-lg sticky top-0 z-10">
|
|
41
|
+
<div class="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
|
|
42
|
+
<div class="flex items-center gap-3">
|
|
43
|
+
<div class="relative flex h-3 w-3">
|
|
44
|
+
<span id="status-ping" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
|
45
|
+
<span id="status-dot" class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
|
|
46
|
+
</div>
|
|
47
|
+
<h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-emerald-400">
|
|
48
|
+
Django Live Logs
|
|
49
|
+
</h1>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Connection Controls -->
|
|
53
|
+
<div class="flex w-full md:w-auto items-center gap-3">
|
|
54
|
+
<button id="connect-btn" onclick="toggleConnection()"
|
|
55
|
+
class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-1.5 rounded text-sm font-semibold transition-colors whitespace-nowrap shadow">
|
|
56
|
+
Connect
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</header>
|
|
61
|
+
|
|
62
|
+
<!-- Controls Bar -->
|
|
63
|
+
<div class="bg-gray-800/50 border-b border-gray-700 p-2">
|
|
64
|
+
<div class="max-w-7xl mx-auto flex justify-between items-center px-2">
|
|
65
|
+
<div class="flex gap-2 text-xs">
|
|
66
|
+
<button onclick="filterLogs('ALL')" class="filter-btn bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded transition-colors active ring-1 ring-white" data-level="ALL">All</button>
|
|
67
|
+
<button onclick="filterLogs('INFO')" class="filter-btn bg-blue-900/50 text-blue-300 hover:bg-blue-800/50 px-3 py-1 rounded transition-colors" data-level="INFO">Info</button>
|
|
68
|
+
<button onclick="filterLogs('WARNING')" class="filter-btn bg-amber-900/50 text-amber-300 hover:bg-amber-800/50 px-3 py-1 rounded transition-colors" data-level="WARNING">Warn</button>
|
|
69
|
+
<button onclick="filterLogs('ERROR')" class="filter-btn bg-red-900/50 text-red-300 hover:bg-red-800/50 px-3 py-1 rounded transition-colors" data-level="ERROR">Error</button>
|
|
70
|
+
</div>
|
|
71
|
+
<button onclick="clearLogs()" class="text-gray-400 hover:text-white text-xs px-3 py-1 rounded hover:bg-gray-700 transition-colors">
|
|
72
|
+
Clear Logs
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Logs Container -->
|
|
78
|
+
<main class="flex-1 overflow-auto p-4 max-w-7xl w-full mx-auto" id="logs-container">
|
|
79
|
+
<!-- Waiting State -->
|
|
80
|
+
<div id="waiting-state" class="flex flex-col items-center justify-center h-full text-gray-500 space-y-4 pt-20">
|
|
81
|
+
<svg class="w-16 h-16 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 9l3 3-3 3m5 0h3M4 15V9a2 2 0 012-2h12a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2z"></path></svg>
|
|
82
|
+
<p>Click Connect to start streaming server logs in real-time.</p>
|
|
83
|
+
</div>
|
|
84
|
+
</main>
|
|
85
|
+
|
|
86
|
+
<script>
|
|
87
|
+
let ws = null;
|
|
88
|
+
let activeFilter = 'ALL';
|
|
89
|
+
const logsContainer = document.getElementById('logs-container');
|
|
90
|
+
const waitingState = document.getElementById('waiting-state');
|
|
91
|
+
const connectBtn = document.getElementById('connect-btn');
|
|
92
|
+
const statusPing = document.getElementById('status-ping');
|
|
93
|
+
const statusDot = document.getElementById('status-dot');
|
|
94
|
+
|
|
95
|
+
function getLevelColors(level) {
|
|
96
|
+
switch(level) {
|
|
97
|
+
case 'INFO': return 'text-blue-400 border-blue-900/50 bg-blue-900/10';
|
|
98
|
+
case 'WARNING': return 'text-amber-400 border-amber-900/50 bg-amber-900/10';
|
|
99
|
+
case 'ERROR': return 'text-red-400 border-red-900/50 bg-red-900/10';
|
|
100
|
+
case 'CRITICAL': return 'text-purple-400 border-purple-900/50 bg-purple-900/10 font-bold';
|
|
101
|
+
default: return 'text-gray-300 border-gray-700 bg-gray-800/30';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function filterLogs(level) {
|
|
106
|
+
activeFilter = level;
|
|
107
|
+
|
|
108
|
+
// Update buttons
|
|
109
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
110
|
+
if (btn.dataset.level === level) {
|
|
111
|
+
btn.classList.add('ring-1', 'ring-white');
|
|
112
|
+
} else {
|
|
113
|
+
btn.classList.remove('ring-1', 'ring-white');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Update visible logs
|
|
118
|
+
document.querySelectorAll('.log-entry').forEach(entry => {
|
|
119
|
+
if (level === 'ALL' || entry.dataset.level === level) {
|
|
120
|
+
entry.style.display = 'block';
|
|
121
|
+
} else {
|
|
122
|
+
entry.style.display = 'none';
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clearLogs() {
|
|
128
|
+
document.querySelectorAll('.log-entry').forEach(e => e.remove());
|
|
129
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
130
|
+
waitingState.style.display = 'flex';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function addLog(data) {
|
|
135
|
+
waitingState.style.display = 'none';
|
|
136
|
+
const colors = getLevelColors(data.level);
|
|
137
|
+
|
|
138
|
+
// Format timestamp if it exists, otherwise use current time
|
|
139
|
+
let timeStr = new Date().toLocaleTimeString();
|
|
140
|
+
if (data.timestamp) {
|
|
141
|
+
const d = new Date(data.timestamp);
|
|
142
|
+
timeStr = d.toLocaleTimeString() + '.' + d.getMilliseconds().toString().padStart(3, '0');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const div = document.createElement('div');
|
|
146
|
+
div.className = `log-entry log-enter border-l-4 rounded p-3 mb-2 text-sm shadow-sm ${colors}`;
|
|
147
|
+
div.dataset.level = data.level;
|
|
148
|
+
|
|
149
|
+
if (activeFilter !== 'ALL' && activeFilter !== data.level) {
|
|
150
|
+
div.style.display = 'none';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
div.innerHTML = `
|
|
154
|
+
<div class="flex justify-between items-start mb-1">
|
|
155
|
+
<div class="flex gap-2 text-xs font-semibold opacity-75">
|
|
156
|
+
<span class="px-1.5 py-0.5 rounded bg-gray-900/50">${data.level}</span>
|
|
157
|
+
<span class="px-1.5 py-0.5 rounded bg-gray-900/50">${data.module || 'system'}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<span class="text-xs opacity-50">${timeStr}</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="whitespace-pre-wrap break-words leading-relaxed">${data.message}</div>
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
logsContainer.appendChild(div);
|
|
165
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
166
|
+
|
|
167
|
+
// Keep only last 500 logs to prevent memory leaks
|
|
168
|
+
if (logsContainer.children.length > 501) { // +1 for waiting state
|
|
169
|
+
logsContainer.removeChild(logsContainer.children[1]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toggleConnection() {
|
|
174
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
175
|
+
ws.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
connectBtn.textContent = 'Connecting...';
|
|
180
|
+
connectBtn.classList.replace('bg-blue-600', 'bg-amber-600');
|
|
181
|
+
|
|
182
|
+
// Determine WS URL automatically
|
|
183
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
184
|
+
const wsUrl = `${protocol}//${window.location.host}/ws/live-logs/`;
|
|
185
|
+
|
|
186
|
+
ws = new WebSocket(wsUrl);
|
|
187
|
+
|
|
188
|
+
ws.onopen = () => {
|
|
189
|
+
connectBtn.textContent = 'Disconnect';
|
|
190
|
+
connectBtn.classList.replace('bg-amber-600', 'bg-red-600');
|
|
191
|
+
connectBtn.classList.replace('bg-blue-600', 'bg-red-600');
|
|
192
|
+
statusPing.classList.replace('bg-red-400', 'bg-emerald-400');
|
|
193
|
+
statusDot.classList.replace('bg-red-500', 'bg-emerald-500');
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
ws.onmessage = (event) => {
|
|
197
|
+
try {
|
|
198
|
+
const data = JSON.parse(event.data);
|
|
199
|
+
addLog(data);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.error("Failed to parse log message:", event.data);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
ws.onclose = (event) => {
|
|
206
|
+
connectBtn.textContent = 'Connect';
|
|
207
|
+
connectBtn.classList.replace('bg-red-600', 'bg-blue-600');
|
|
208
|
+
connectBtn.classList.replace('bg-amber-600', 'bg-blue-600');
|
|
209
|
+
statusPing.classList.replace('bg-emerald-400', 'bg-red-400');
|
|
210
|
+
statusDot.classList.replace('bg-emerald-500', 'bg-red-500');
|
|
211
|
+
|
|
212
|
+
if (event.code === 4003) {
|
|
213
|
+
addLog({ level: 'ERROR', message: 'Connection Rejected: Invalid Token or Not a Superuser', module: 'system' });
|
|
214
|
+
} else if (!event.wasClean) {
|
|
215
|
+
addLog({ level: 'WARNING', message: 'Connection Lost. Retrying...', module: 'system' });
|
|
216
|
+
}
|
|
217
|
+
ws = null;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
ws.onerror = (error) => {
|
|
221
|
+
console.error("WebSocket Error:", error);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
</script>
|
|
225
|
+
</body>
|
|
226
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.shortcuts import render
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.contrib.admin.views.decorators import staff_member_required
|
|
4
|
+
|
|
5
|
+
@staff_member_required
|
|
6
|
+
def live_logs_dashboard(request):
|
|
7
|
+
"""
|
|
8
|
+
Renders the live log dashboard UI.
|
|
9
|
+
Requires the user to be logged into the standard Django admin.
|
|
10
|
+
"""
|
|
11
|
+
return render(request, 'django_live_logs/dashboard.html')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-live-logs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A standalone Django package to stream logs over WebSockets.
|
|
5
|
+
Author: Your Name
|
|
6
|
+
Requires-Dist: Django>=3.2
|
|
7
|
+
Requires-Dist: channels>=4.0
|
|
8
|
+
Dynamic: author
|
|
9
|
+
Dynamic: requires-dist
|
|
10
|
+
Dynamic: summary
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
django_live_logs/__init__.py
|
|
5
|
+
django_live_logs/consumers.py
|
|
6
|
+
django_live_logs/handlers.py
|
|
7
|
+
django_live_logs/routing.py
|
|
8
|
+
django_live_logs/urls.py
|
|
9
|
+
django_live_logs/views.py
|
|
10
|
+
django_live_logs.egg-info/PKG-INFO
|
|
11
|
+
django_live_logs.egg-info/SOURCES.txt
|
|
12
|
+
django_live_logs.egg-info/dependency_links.txt
|
|
13
|
+
django_live_logs.egg-info/requires.txt
|
|
14
|
+
django_live_logs.egg-info/top_level.txt
|
|
15
|
+
django_live_logs/templates/django_live_logs/dashboard.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_live_logs
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="django-live-logs",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
include_package_data=True,
|
|
8
|
+
install_requires=[
|
|
9
|
+
"Django>=3.2",
|
|
10
|
+
"channels>=4.0",
|
|
11
|
+
],
|
|
12
|
+
author="Your Name",
|
|
13
|
+
description="A standalone Django package to stream logs over WebSockets.",
|
|
14
|
+
)
|