├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README ├── README.rst ├── logtailer ├── __init__.py ├── admin.py ├── apps.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── it │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── logtailer ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20210831_0738.py │ └── __init__.py ├── models.py ├── static │ └── logtailer │ │ ├── css │ │ └── logtailer.css │ │ ├── img │ │ └── colorbox │ │ │ ├── border.png │ │ │ ├── controls.png │ │ │ ├── loading.gif │ │ │ ├── loading_background.png │ │ │ └── overlay.png │ │ └── js │ │ ├── init.js │ │ ├── logtailer.js │ │ └── utils.js ├── templates │ ├── admin │ │ └── logtailer │ │ │ └── logfile │ │ │ └── change_form.html │ └── logtailer │ │ ├── log_reader.html │ │ └── templatetags │ │ └── filters.html ├── templatetags │ ├── __init__.py │ └── logtailer_utils.py ├── urls.py └── views.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .project 3 | .pydevproject 4 | .idea/ 5 | dist/ 6 | build/ 7 | *.egg 8 | *.egg-info 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ========= 2 | AUTHORS 3 | ========= 4 | :order: sorted 5 | 6 | Mauro Rocco -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All enhancements and patches to Django Logtailer will be documented in this file. 3 | 4 | ## [1.3] 5 | ### Changed 6 | - Adding download log file link 7 | - Making compatible with Django 5 8 | - Remove jquery colorbox 9 | - Adapt popup for save logs to new django admin dynamic color scheme 10 | 11 | 12 | ## [1.2] 13 | ### Changed 14 | - Add form input field to define last lines to read from file 15 | - Making compatible with Django 3 16 | 17 | ## [1.1] 18 | ### Changed 19 | - Making compatible to Django 2.x 20 | - Making compatible with Python 3.7 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Mauro Rocco and contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | Neither the name of Mauro Rocco nor the names of its contributors may be used 14 | to endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 19 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 20 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 21 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | recursive-include logtailer/templates * 5 | recursive-include logtailer/static * 6 | recursive-include logtailer/locale * -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | Django LogTailer 3 | ================================= 4 | 5 | :Version: 1.3 6 | :Source: http://github.com/fireantology/django-logtailer/ 7 | 8 | 9 | Allows the viewing of any log file entries in real time directly from the Django admin interface. 10 | It allows you to filter on logs with regex and offer also a log clipboard for save desired log lines to the django db. 11 | 12 | Demos 13 | ======== 14 | - Demo `Video`_ 15 | 16 | .. _`Video`: http://www.vimeo.com/28891014 17 | 18 | Requirements 19 | ============= 20 | 21 | - Django > 4.0 22 | - Python 3.x 23 | - Sessions enabled 24 | 25 | Installation 26 | ============ 27 | 28 | - Install the package with pip install django-logtailer 29 | - Add it to the INSTALLED_APPS in your SETTINGS 30 | - add to urls.py: url(r'^logs/', include('logtailer.urls')), 31 | - Run manage.py migrate for create the required tables 32 | - Run manage.py collectstatic 33 | -------------------------------------------------------------------------------- /logtailer/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 3, 0) 2 | __version__ = '.'.join(map(str, VERSION)) 3 | -------------------------------------------------------------------------------- /logtailer/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import gettext as _ 3 | from django.http import HttpResponse, HttpResponseRedirect 4 | from django.urls import path, reverse 5 | from logtailer.models import LogFile, Filter, LogsClipboard 6 | 7 | 8 | class LogFileAdmin(admin.ModelAdmin): 9 | list_display = ('__unicode__', 'path') 10 | 11 | class Media: 12 | css = { 13 | 'all': ('logtailer/css/logtailer.css',) 14 | } 15 | 16 | def get_urls(self): 17 | info = self.model._meta.app_label, self.model._meta.model_name 18 | urls = super().get_urls() 19 | my_urls = [ 20 | path('/download/', 21 | self.admin_site.admin_view(self.download), {}, 22 | name="%s_%s_download" % info), 23 | ] 24 | return my_urls + urls 25 | 26 | def download(self, request, object_id): 27 | try: 28 | log_file = self.get_object(request, object_id) 29 | with open(log_file.path, 'r') as f: 30 | buffer = f.read() 31 | response = HttpResponse(buffer, content_type='plain/text') 32 | response['Content-Disposition'] = 'attachment; filename=%s' % log_file.name 33 | except Exception as e: 34 | try: 35 | from django.contrib import messages 36 | self.message_user(request, _('ERROR') + ': ' + str(e), level=messages.ERROR) 37 | except: 38 | pass 39 | response = HttpResponseRedirect(reverse('admin:logtailer_logfile_change', args=(object_id, ))) 40 | return response 41 | 42 | 43 | class FilterAdmin(admin.ModelAdmin): 44 | list_display = ('name', 'regex') 45 | 46 | 47 | class LogsClipboardAdmin(admin.ModelAdmin): 48 | list_display = ('name', 'notes', 'log_file') 49 | readonly_fields = ('name', 'notes', 'logs', 'log_file') 50 | 51 | 52 | admin.site.register(LogFile, LogFileAdmin) 53 | admin.site.register(Filter, FilterAdmin) 54 | admin.site.register(LogsClipboard, LogsClipboardAdmin) 55 | -------------------------------------------------------------------------------- /logtailer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LogtailerConfig(AppConfig): 5 | name = 'logtailer' 6 | verbose_name = "Logtailer" 7 | -------------------------------------------------------------------------------- /logtailer/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /logtailer/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) Mauro Rocco 2 | # This file is distributed under the same license as the Logtailer package. 3 | # Mauro Rocco , 2011. 4 | # 5 | #, fuzzy 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.1.0\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-04-19 20:04+0000\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Mauro Rocco \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: English\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: logtailer/admin.py:36 20 | msgid "ERROR" 21 | msgstr "" 22 | 23 | #: logtailer/models.py:6 logtailer/models.py:18 logtailer/models.py:30 24 | #: logtailer/templates/logtailer/log_reader.html:46 25 | msgid "name" 26 | msgstr "Name" 27 | 28 | #: logtailer/models.py:7 29 | msgid "path" 30 | msgstr "Path" 31 | 32 | #: logtailer/models.py:13 33 | #, fuzzy 34 | #| msgid "log_file" 35 | msgid "Log file" 36 | msgstr "Log file" 37 | 38 | #: logtailer/models.py:14 39 | #, fuzzy 40 | #| msgid "log_files" 41 | msgid "Log files" 42 | msgstr "Log files" 43 | 44 | #: logtailer/models.py:19 45 | msgid "regex" 46 | msgstr "Regex" 47 | 48 | #: logtailer/models.py:22 49 | msgid "pattern" 50 | msgstr "Pattern" 51 | 52 | #: logtailer/models.py:25 53 | msgid "filter" 54 | msgstr "Filter" 55 | 56 | #: logtailer/models.py:26 logtailer/templates/logtailer/log_reader.html:8 57 | msgid "filters" 58 | msgstr "Filters" 59 | 60 | #: logtailer/models.py:31 logtailer/templates/logtailer/log_reader.html:47 61 | msgid "notes" 62 | msgstr "Notes" 63 | 64 | #: logtailer/models.py:32 65 | msgid "logs" 66 | msgstr "Logs" 67 | 68 | #: logtailer/models.py:33 69 | #, fuzzy 70 | #| msgid "log_file" 71 | msgid "log file" 72 | msgstr "Log file" 73 | 74 | #: logtailer/models.py:39 logtailer/models.py:40 75 | #, fuzzy 76 | #| msgid "logs_clipboard" 77 | msgid "logs clipboard" 78 | msgstr "Logs clipboard" 79 | 80 | #: logtailer/templates/admin/logtailer/logfile/change_form.html:20 81 | msgid "Download Log File" 82 | msgstr "" 83 | 84 | #: logtailer/templates/logtailer/log_reader.html:13 85 | msgid "apply" 86 | msgstr "Apply" 87 | 88 | #: logtailer/templates/logtailer/log_reader.html:19 89 | msgid "Last lines to read" 90 | msgstr "" 91 | 92 | #: logtailer/templates/logtailer/log_reader.html:25 93 | msgid "auto_scroll" 94 | msgstr "Auto scroll" 95 | 96 | #: logtailer/templates/logtailer/log_reader.html:31 97 | msgid "start_read" 98 | msgstr "Start read" 99 | 100 | #: logtailer/templates/logtailer/log_reader.html:32 101 | msgid "stop_read" 102 | msgstr "Stop read" 103 | 104 | #: logtailer/templates/logtailer/log_reader.html:33 105 | msgid "save_selected_rows" 106 | msgstr "Save selected rows" 107 | 108 | #: logtailer/templates/logtailer/log_reader.html:45 109 | msgid "save_logs" 110 | msgstr "Save Logs" 111 | 112 | #: logtailer/templates/logtailer/log_reader.html:52 113 | msgid "save" 114 | msgstr "Save" 115 | 116 | #: logtailer/templates/logtailer/log_reader.html:53 117 | msgid "close" 118 | msgstr "Close" 119 | 120 | #: logtailer/templates/logtailer/templatetags/filters.html:6 121 | msgid "custom" 122 | msgstr "custom" 123 | 124 | #: logtailer/views.py:50 125 | msgid "error_logfile_notexist" 126 | msgstr "ERROR: Log file record does not exist" 127 | 128 | #: logtailer/views.py:56 129 | msgid "error_no_suchfile" 130 | msgstr "No such File" 131 | 132 | #: logtailer/views.py:81 133 | msgid "loglines_saved" 134 | msgstr "Log lines saved" 135 | -------------------------------------------------------------------------------- /logtailer/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /logtailer/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) Mauro Rocco 2 | # This file is distributed under the same license as the Logtailer package. 3 | # Mauro Rocco , 2011. 4 | # 5 | #, fuzzy 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.1.0\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2025-04-19 20:04+0000\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Mauro Rocco \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: English\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 19 | 20 | #: logtailer/admin.py:36 21 | msgid "ERROR" 22 | msgstr "" 23 | 24 | #: logtailer/models.py:6 logtailer/models.py:18 logtailer/models.py:30 25 | #: logtailer/templates/logtailer/log_reader.html:46 26 | msgid "name" 27 | msgstr "Nome" 28 | 29 | #: logtailer/models.py:7 30 | msgid "path" 31 | msgstr "Percorso" 32 | 33 | #: logtailer/models.py:13 34 | #, fuzzy 35 | #| msgid "log_file" 36 | msgid "Log file" 37 | msgstr "Log file" 38 | 39 | #: logtailer/models.py:14 40 | #, fuzzy 41 | #| msgid "log_files" 42 | msgid "Log files" 43 | msgstr "Log files" 44 | 45 | #: logtailer/models.py:19 46 | msgid "regex" 47 | msgstr "Espressione regolare" 48 | 49 | #: logtailer/models.py:22 50 | msgid "pattern" 51 | msgstr "Pattern" 52 | 53 | #: logtailer/models.py:25 54 | msgid "filter" 55 | msgstr "Filtro" 56 | 57 | #: logtailer/models.py:26 logtailer/templates/logtailer/log_reader.html:8 58 | msgid "filters" 59 | msgstr "Filtri" 60 | 61 | #: logtailer/models.py:31 logtailer/templates/logtailer/log_reader.html:47 62 | msgid "notes" 63 | msgstr "Note" 64 | 65 | #: logtailer/models.py:32 66 | msgid "logs" 67 | msgstr "Logs" 68 | 69 | #: logtailer/models.py:33 70 | #, fuzzy 71 | #| msgid "log_file" 72 | msgid "log file" 73 | msgstr "Log file" 74 | 75 | #: logtailer/models.py:39 logtailer/models.py:40 76 | #, fuzzy 77 | #| msgid "logs_clipboard" 78 | msgid "logs clipboard" 79 | msgstr "Appunti" 80 | 81 | #: logtailer/templates/admin/logtailer/logfile/change_form.html:20 82 | msgid "Download Log File" 83 | msgstr "" 84 | 85 | #: logtailer/templates/logtailer/log_reader.html:13 86 | msgid "apply" 87 | msgstr "Applica" 88 | 89 | #: logtailer/templates/logtailer/log_reader.html:19 90 | msgid "Last lines to read" 91 | msgstr "" 92 | 93 | #: logtailer/templates/logtailer/log_reader.html:25 94 | msgid "auto_scroll" 95 | msgstr "Scroll automatico" 96 | 97 | #: logtailer/templates/logtailer/log_reader.html:31 98 | msgid "start_read" 99 | msgstr "Inizia la lettura" 100 | 101 | #: logtailer/templates/logtailer/log_reader.html:32 102 | msgid "stop_read" 103 | msgstr "Ferma la lettura" 104 | 105 | #: logtailer/templates/logtailer/log_reader.html:33 106 | msgid "save_selected_rows" 107 | msgstr "Salve le linee selezionate" 108 | 109 | #: logtailer/templates/logtailer/log_reader.html:45 110 | msgid "save_logs" 111 | msgstr "Salva Logs" 112 | 113 | #: logtailer/templates/logtailer/log_reader.html:52 114 | msgid "save" 115 | msgstr "Salva" 116 | 117 | #: logtailer/templates/logtailer/log_reader.html:53 118 | msgid "close" 119 | msgstr "Chiudi" 120 | 121 | #: logtailer/templates/logtailer/templatetags/filters.html:6 122 | msgid "custom" 123 | msgstr "personalizzato" 124 | 125 | #: logtailer/views.py:50 126 | msgid "error_logfile_notexist" 127 | msgstr "Errore: Il file di log non esiste" 128 | 129 | #: logtailer/views.py:56 130 | msgid "error_no_suchfile" 131 | msgstr "File non trovato" 132 | 133 | #: logtailer/views.py:81 134 | msgid "loglines_saved" 135 | msgstr "Linee del log salvate" 136 | -------------------------------------------------------------------------------- /logtailer/logtailer: -------------------------------------------------------------------------------- 1 | logtailer -------------------------------------------------------------------------------- /logtailer/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-07-25 19:42 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Filter', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=180, verbose_name='name')), 22 | ('regex', models.CharField(max_length=500, verbose_name='regex')), 23 | ], 24 | options={ 25 | 'verbose_name': 'filter', 26 | 'verbose_name_plural': 'filters', 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='LogFile', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('name', models.CharField(max_length=180, verbose_name='name')), 34 | ('path', models.CharField(max_length=500, verbose_name='path')), 35 | ], 36 | options={ 37 | 'verbose_name': 'log_file', 38 | 'verbose_name_plural': 'log_files', 39 | }, 40 | ), 41 | migrations.CreateModel( 42 | name='LogsClipboard', 43 | fields=[ 44 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 45 | ('name', models.CharField(max_length=180, verbose_name='name')), 46 | ('notes', models.TextField(blank=True, null=True, verbose_name='notes')), 47 | ('logs', models.TextField(verbose_name='logs')), 48 | ('log_file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='logtailer.LogFile', verbose_name='log_file')), 49 | ], 50 | options={ 51 | 'verbose_name': 'logs_clipboard', 52 | 'verbose_name_plural': 'logs_clipboard', 53 | }, 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /logtailer/migrations/0002_auto_20210831_0738.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-08-31 05:38 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('logtailer', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='logfile', 16 | options={'verbose_name': 'Log file', 'verbose_name_plural': 'Log files'}, 17 | ), 18 | migrations.AlterModelOptions( 19 | name='logsclipboard', 20 | options={'verbose_name': 'logs clipboard', 'verbose_name_plural': 'logs clipboard'}, 21 | ), 22 | migrations.AlterField( 23 | model_name='logsclipboard', 24 | name='log_file', 25 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='logtailer.logfile', verbose_name='log file'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /logtailer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/migrations/__init__.py -------------------------------------------------------------------------------- /logtailer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class LogFile(models.Model): 6 | name = models.CharField(_('name'), max_length=180) 7 | path = models.CharField(_('path'), max_length=500) 8 | 9 | def __unicode__(self): 10 | return '%s' % self.name 11 | 12 | class Meta: 13 | verbose_name = _('Log file') 14 | verbose_name_plural = _('Log files') 15 | 16 | 17 | class Filter(models.Model): 18 | name = models.CharField(_('name'), max_length=180) 19 | regex = models.CharField(_('regex'), max_length=500) 20 | 21 | def __unicode__(self): 22 | return '%s | %s: %s ' % (self.name, _('pattern'), self.regex) 23 | 24 | class Meta: 25 | verbose_name = _('filter') 26 | verbose_name_plural = _('filters') 27 | 28 | 29 | class LogsClipboard(models.Model): 30 | name = models.CharField(_('name'), max_length=180) 31 | notes = models.TextField(_('notes'), blank=True, null=True) 32 | logs = models.TextField(_('logs')) 33 | log_file = models.ForeignKey(LogFile, on_delete=models.CASCADE, verbose_name=_('log file')) 34 | 35 | def __unicode__(self): 36 | return "%s" % self.name 37 | 38 | class Meta: 39 | verbose_name = _('logs clipboard') 40 | verbose_name_plural = _('logs clipboard') 41 | -------------------------------------------------------------------------------- /logtailer/static/logtailer/css/logtailer.css: -------------------------------------------------------------------------------- 1 | #logfile_form #apply-label { 2 | margin-left: 5px; 3 | margin-right: 5px; 4 | } 5 | 6 | #logfile_form #apply-filter { 7 | margin: 0px; 8 | } 9 | 10 | #logfile_form #start-button { 11 | float: left; 12 | } 13 | 14 | #logfile_form #stop-button { 15 | float: left; 16 | } 17 | 18 | @media (max-width: 767px){ 19 | #logfile_form #apply-filter-row { 20 | width: 100%; 21 | padding: 10px 0px 10px 0px; 22 | }} 23 | 24 | #log-window-container{ 25 | text-align: center; width: 100%; margin-bottom: 5px; 26 | } 27 | 28 | #log-window{ 29 | width: 98%; 30 | text-align: left; 31 | background-color: #000; 32 | color: #ccc; 33 | font-size: 14px; 34 | height: 500px; 35 | overflow: auto; 36 | padding: 10px; 37 | line-height: 18px; 38 | margin: 0 auto; 39 | } 40 | 41 | #clipboard-form-container{ 42 | margin: 20px; 43 | text-align: center; 44 | } 45 | 46 | #clipboard-form{ 47 | margin: 0 auto; 48 | text-align: left; 49 | } 50 | 51 | .modal-overlay { 52 | background: rgba(0, 0, 0, 0.7); 53 | width: 100%; 54 | height: 100%; 55 | position: fixed; 56 | top: 0; 57 | bottom: 0; 58 | left: 0; 59 | right: 0; 60 | z-index:99; 61 | } 62 | 63 | .modal-wrapper { 64 | background: var(--body-bg); 65 | position: fixed; 66 | top: 50%; 67 | left: 50%; 68 | transform: translate(-50%, -50%); 69 | border-radius: 25px; 70 | border: 2px solid var(--hairline-color); 71 | } 72 | 73 | .modal-content { 74 | margin: 20px auto; 75 | max-width: 210px; 76 | width: 100%; 77 | } 78 | 79 | .hide { 80 | display: none; 81 | } 82 | 83 | h1 { 84 | text-align: center; 85 | } 86 | -------------------------------------------------------------------------------- /logtailer/static/logtailer/img/colorbox/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/static/logtailer/img/colorbox/border.png -------------------------------------------------------------------------------- /logtailer/static/logtailer/img/colorbox/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/static/logtailer/img/colorbox/controls.png -------------------------------------------------------------------------------- /logtailer/static/logtailer/img/colorbox/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/static/logtailer/img/colorbox/loading.gif -------------------------------------------------------------------------------- /logtailer/static/logtailer/img/colorbox/loading_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/static/logtailer/img/colorbox/loading_background.png -------------------------------------------------------------------------------- /logtailer/static/logtailer/img/colorbox/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/static/logtailer/img/colorbox/overlay.png -------------------------------------------------------------------------------- /logtailer/static/logtailer/js/init.js: -------------------------------------------------------------------------------- 1 | django.jQuery('#start-button').click(function() { 2 | LogTailer.startReading(); 3 | }); 4 | 5 | django.jQuery('#stop-button').click(function() { 6 | LogTailer.stopReading(); 7 | }); 8 | 9 | django.jQuery('#auto-scroll').click(function() { 10 | LogTailer.changeAutoScroll(); 11 | }); 12 | 13 | django.jQuery('#filter-select').change(function() { 14 | LogTailer.customFilter(); 15 | }); 16 | 17 | 18 | 19 | django.jQuery('#log-window').html("") 20 | LogTailer.customFilter(); 21 | 22 | django.jQuery('#clipboard-form').submit(function() { 23 | var error = false; 24 | if(django.jQuery("#clipboard-name").val().length<1){ 25 | django.jQuery("#clipboard-error").html("Field name is mandatory"); 26 | error = true; 27 | } 28 | if(django.jQuery("#clipboard-logs").val().length<1){ 29 | django.jQuery("#clipboard-error").html("No log lines selected"); 30 | error = true; 31 | } 32 | 33 | if(!error){ 34 | django.jQuery.ajax({type: 'POST', 35 | url: clipboard_url, 36 | data: { 37 | name: django.jQuery("#clipboard-name").val(), 38 | notes: django.jQuery("#clipboard-notes").val(), 39 | logs: django.jQuery("#clipboard-logs").val(), 40 | file: django.jQuery("#clipboard-file").val(), 41 | csrfmiddlewaretoken: django.jQuery("[name=csrfmiddlewaretoken]").val() 42 | }, 43 | success: function(result){ 44 | alert(result); 45 | django.jQuery("#modal-overlay").hide(); 46 | }, 47 | dataType: "text"}); 48 | } 49 | 50 | return false; 51 | }); 52 | 53 | const saveRows = document.getElementById("save-rows"); 54 | const modal = document.getElementById("modal-overlay"); 55 | const closeButton = document.getElementById("close-modal-btn"); 56 | 57 | function openModal() { 58 | django.jQuery("#clipboard-logs").val(getSelectedText()); 59 | django.jQuery("#clipboard-name").val(""); 60 | django.jQuery("#clipboard-notes").val(""); 61 | django.jQuery("#clipboard-error").html(""); 62 | modal.classList.remove("hide"); 63 | } 64 | 65 | function closeModal(e, clickedOutside) { 66 | if (clickedOutside) { 67 | if (e.target.classList.contains("modal-overlay")) 68 | modal.classList.add("hide"); 69 | } else modal.classList.add("hide"); 70 | } 71 | 72 | saveRows.addEventListener("click", openModal); 73 | modal.addEventListener("click", (e) => closeModal(e, true)); 74 | closeButton.addEventListener("click", closeModal); 75 | -------------------------------------------------------------------------------- /logtailer/static/logtailer/js/logtailer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Logtailer object 3 | */ 4 | 5 | var LogTailer = { 6 | timeout_id: null, 7 | timeout: 2000, 8 | scroll: true, 9 | file_id: 0, 10 | first_read: true, 11 | } 12 | 13 | LogTailer.getLines = function (){ 14 | LogTailer.currentScrollPosition = django.jQuery("#log-window").scrollTop(); 15 | django.jQuery.ajax({ 16 | url: LOGTAILER_URL_GETLOGLINE, 17 | success: function(result){ 18 | LogTailer.printLines(result); 19 | }, 20 | dataType: "json" 21 | }); 22 | 23 | } 24 | 25 | LogTailer.getHistory = function (callback, lines){ 26 | LogTailer.currentScrollPosition = django.jQuery("#log-window").scrollTop(); 27 | django.jQuery.ajax({ 28 | url: LOGTAILER_URL_GETLOGLINE, 29 | type: "get", 30 | data: { 31 | history: lines, 32 | }, 33 | success: function(result){ 34 | LogTailer.printLines(result); 35 | callback && callback(); 36 | }, 37 | dataType: "json" 38 | }); 39 | 40 | } 41 | 42 | LogTailer.printLines = function(result){ 43 | if(django.jQuery("#apply-filter").is(':checked')){ 44 | for(var i=0;i-1){ 57 | django.jQuery("#log-window").append(result[i]); 58 | } 59 | } 60 | } 61 | else{ 62 | for(var i=0;i0){ 64 | django.jQuery("#log-window").append(result[i]); 65 | } 66 | } 67 | } 68 | if(LogTailer.scroll && result.length){ 69 | django.jQuery("#log-window").scrollTop(django.jQuery("#log-window")[0].scrollHeight - django.jQuery("#log-window").height()); 70 | } 71 | else{ 72 | django.jQuery("#log-window").scrollTop(LogTailer.currentScrollPosition); 73 | } 74 | window.clearTimeout(LogTailer.timeout_id); 75 | LogTailer.timeout_id = window.setTimeout("LogTailer.getLines("+LogTailer.file_id+")", LogTailer.timeout); 76 | } 77 | 78 | LogTailer.startReading = function (){ 79 | if (LogTailer.first_read) { 80 | var lines = django.jQuery('#history_lines').val(); 81 | if(!isInt(lines)){ 82 | alert("Last lines parameter is not an integer"); 83 | return; 84 | } 85 | LogTailer.first_read = false; 86 | django.jQuery('#history_lines').prop("disabled", true); 87 | LogTailer.getHistory( function(){ 88 | LogTailer.timeout_id = window.setTimeout("LogTailer.getLines("+LogTailer.file_id+")", LogTailer.timeout); 89 | }, lines); 90 | } else { 91 | LogTailer.timeout_id = window.setTimeout("LogTailer.getLines("+LogTailer.file_id+")", LogTailer.timeout); 92 | } 93 | django.jQuery("#start-button").hide(); 94 | django.jQuery("#stop-button").show(); 95 | } 96 | 97 | LogTailer.stopReading = function (){ 98 | window.clearTimeout(LogTailer.timeout_id); 99 | django.jQuery("#stop-button").hide(); 100 | django.jQuery("#start-button").show(); 101 | } 102 | 103 | 104 | LogTailer.changeAutoScroll = function(){ 105 | if(LogTailer.scroll){ 106 | LogTailer.scroll = false; 107 | django.jQuery('#auto-scroll').val("OFF"); 108 | } 109 | else{ 110 | LogTailer.scroll = true; 111 | django.jQuery('#auto-scroll').val("ON"); 112 | } 113 | } 114 | 115 | LogTailer.customFilter = function(){ 116 | if(django.jQuery('#filter-select').val()=="custom"){ 117 | django.jQuery('#filter').show(); 118 | } 119 | else{ 120 | django.jQuery('#filter').hide(); 121 | } 122 | } -------------------------------------------------------------------------------- /logtailer/static/logtailer/js/utils.js: -------------------------------------------------------------------------------- 1 | function getSelectedText() { 2 | if(window.getSelection) { return window.getSelection(); } 3 | else if(document.getSelection) { return document.getSelection(); } 4 | else { 5 | var selection = document.selection && document.selection.createRange(); 6 | if(selection.text) { return selection.text; } 7 | return false; 8 | } 9 | return false; 10 | } 11 | 12 | function isInt(value) { 13 | return !isNaN(value) && 14 | parseInt(Number(value)) == value && 15 | !isNaN(parseInt(value, 10)); 16 | } -------------------------------------------------------------------------------- /logtailer/templates/admin/logtailer/logfile/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | {% block extrahead %} 4 | {{ block.super }} 5 | {% if change %} 6 | 9 | {% endif %} 10 | {% endblock extrahead %} 11 | {% block object-tools %} 12 | {{block.super}} 13 | {% if change %}{% if not is_popup %} 14 | {% include "logtailer/log_reader.html" with logfile_id=original.pk %} 15 | {% endif %}{% endif %} 16 | {% endblock %} 17 | 18 | 19 | {% block object-tools-items %} 20 |
  • {% trans 'Download Log File' %}
  • 21 | {{ block.super }} 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /logtailer/templates/logtailer/log_reader.html: -------------------------------------------------------------------------------- 1 | {% load logtailer_utils static %} 2 | {% load i18n %} 3 |
    4 |
    5 |
    6 |
    7 | 10 | {% filters_select %} 11 | 12 | 13 | {% trans "apply" %} 14 | 15 | 16 |
    17 |
    18 | 21 | 22 |
    23 |
    24 | 27 | 28 |
    29 |
    30 |
    31 | 32 | 33 | 34 |
    35 |
    36 |
    37 |
    38 |
    39 |
    40 | 41 | 60 | 61 | 62 | 66 | -------------------------------------------------------------------------------- /logtailer/templates/logtailer/templatetags/filters.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | -------------------------------------------------------------------------------- /logtailer/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireantology/django-logtailer/ab0b672192385a7f4cbdf9558fbb9d4e5c551b2b/logtailer/templatetags/__init__.py -------------------------------------------------------------------------------- /logtailer/templatetags/logtailer_utils.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from logtailer.models import Filter 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.inclusion_tag('logtailer/templatetags/filters.html') 8 | def filters_select(): 9 | filters = Filter.objects.all() 10 | return {'filters': filters} -------------------------------------------------------------------------------- /logtailer/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path('readlogs/', views.read_logs, name='logtailer_read_logs'), 6 | path('get-log-line//', views.get_log_lines, name='logtailer_get_log_lines'), 7 | path('save-to-clipboard/', views.save_to_clipoard, name="logtailer_save_to_clipboard"), 8 | ] 9 | -------------------------------------------------------------------------------- /logtailer/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from django.http import HttpResponse 4 | from django.template import RequestContext 5 | from django.shortcuts import render 6 | from logtailer.models import LogsClipboard, LogFile 7 | from django.utils.translation import gettext as _ 8 | from django.views.decorators.csrf import csrf_exempt 9 | from django.contrib.admin.views.decorators import staff_member_required 10 | 11 | 12 | @staff_member_required 13 | def read_logs(request): 14 | context = {} 15 | return render(request, 'logtailer/log_reader.html', 16 | context, RequestContext(request, {}),) 17 | 18 | 19 | def get_history(f, lines=0): 20 | buffer_size = 1024 21 | f.seek(0, os.SEEK_END) 22 | bytes = f.tell() 23 | size = lines 24 | block = -1 25 | data = [] 26 | while size > 0 and bytes > 0: 27 | if bytes - buffer_size > 0: 28 | # Seek back one whole buffer_size 29 | f.seek(f.tell()+block*buffer_size, 0) 30 | # read buffer 31 | data.append(f.read(buffer_size)) 32 | else: 33 | # file too small, start from beginning 34 | f.seek(0, 0) 35 | # only read what was not read 36 | data.append(f.read(bytes)) 37 | lines_found = data[-1].count('\n') 38 | size -= lines_found 39 | bytes += block*buffer_size 40 | block -= 1 41 | return ''.join(data).splitlines(True)[-lines:] 42 | 43 | 44 | @staff_member_required 45 | def get_log_lines(request, file_id): 46 | history = int(request.GET.get('history', 0)) 47 | try: 48 | file_record = LogFile.objects.get(id=file_id) 49 | except LogFile.DoesNotExist: 50 | return HttpResponse(json.dumps([_('error_logfile_notexist')]), 51 | content_type='text/html') 52 | content = [] 53 | try: 54 | file = open(file_record.path, 'r') 55 | except FileNotFoundError: 56 | return HttpResponse(json.dumps([_('error_no_suchfile')]),) 57 | 58 | if history > 0: 59 | content = get_history(file, history) 60 | content = [line.replace('\n','
    ') for line in content] 61 | else: 62 | last_position = request.session.get('file_position_%s' % file_id) 63 | file.seek(0, os.SEEK_END) 64 | if last_position and last_position <= file.tell(): 65 | file.seek(last_position) 66 | 67 | for line in file: 68 | content.append('%s' % line.replace('\n','
    ')) 69 | 70 | request.session['file_position_%s' % file_id] = file.tell() 71 | file.close() 72 | return HttpResponse(json.dumps(content), content_type='application/json') 73 | 74 | 75 | @staff_member_required 76 | def save_to_clipoard(request): 77 | LogsClipboard(name=request.POST['name'], 78 | notes=request.POST['notes'], 79 | logs=request.POST['logs'], 80 | log_file=LogFile.objects.get(id=int(request.POST['file']))).save() 81 | return HttpResponse(_('loglines_saved'), content_type='text/html') 82 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools.packages] 6 | find = {} 7 | 8 | [tool.setuptools.dynamic] 9 | version = {attr = "logtailer.__version__"} 10 | 11 | [project] 12 | dynamic = ["version"] 13 | name = "django-logtailer" 14 | authors = [ 15 | { name="Mauro", email="fireantology@gmail.com" }, 16 | ] 17 | description = "Allows to read log files from disk with a tail like web console on Django admin interface." 18 | readme = "README.rst" 19 | requires-python = ">=3.9" 20 | dependencies = [ 21 | 'Django >= 5.0', 22 | ] 23 | classifiers = [ 24 | "Programming Language :: Python :: 3", 25 | "Operating System :: OS Independent", 26 | "Development Status :: 5 - Production/Stable", 27 | "Framework :: Django", 28 | "Intended Audience :: Developers", 29 | "Topic :: Internet :: WWW/HTTP", 30 | "Framework :: Django", 31 | "Framework :: Django :: 4.0", 32 | "Framework :: Django :: 5.0", 33 | ] 34 | license = "BSD-2-Clause" 35 | license-files = ["LICENSE"] 36 | 37 | [project.urls] 38 | Homepage = "https://github.com/fireantology/django-logtailer" --------------------------------------------------------------------------------