diff --git a/DnsServerCore/www/js/logs.js b/DnsServerCore/www/js/logs.js
new file mode 100644
index 00000000..de8b366e
--- /dev/null
+++ b/DnsServerCore/www/js/logs.js
@@ -0,0 +1,392 @@
+/*
+Technitium DNS Server
+Copyright (C) 2021 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+$(function () {
+ $('#dtpQueryLogStart').datetimepicker({ format: "YYYY-MM-DD HH:mm:ss" });
+ $('#dtpQueryLogEnd').datetimepicker({ format: "YYYY-MM-DD HH:mm:ss" });
+
+ $("#optQueryLogsAppName").change(function () {
+ if (appsList == null)
+ return;
+
+ var appName = $("#optQueryLogsAppName").val();
+ var optClassPaths = "";
+
+ for (var i = 0; i < appsList.length; i++) {
+ if (appsList[i].name == appName) {
+ for (var j = 0; j < appsList[i].loggers.length; j++) {
+ optClassPaths += "" + appsList[i].loggers[j].classPath + " ";
+ }
+ }
+ }
+
+ $("#optQueryLogsClassPath").html(optClassPaths);
+ $("#txtAddEditRecordDataData").val("");
+ });
+});
+
+function refreshLogsTab() {
+ if ($("#logsTabListLogViewer").hasClass("active"))
+ refreshLogFilesList();
+ else if ($("#logsTabListQueryLogs").hasClass("active"))
+ refreshQueryLogsTab();
+}
+
+function refreshLogFilesList() {
+ var lstLogFiles = $("#lstLogFiles");
+
+ HTTPRequest({
+ url: "/api/listLogs?token=" + token,
+ success: function (responseJSON) {
+ var logFiles = responseJSON.response.logFiles;
+
+ var list = "
";
+
+ if (logFiles.length == 0) {
+ list += "No Log Was Found
";
+ }
+ else {
+ list += "";
+
+ for (var i = 0; i < logFiles.length; i++) {
+ var logFile = logFiles[i];
+
+ list += ""
+ }
+ }
+
+ lstLogFiles.html(list);
+ },
+ invalidToken: function () {
+ showPageLogin();
+ },
+ objLoaderPlaceholder: lstLogFiles
+ });
+
+ return false;
+}
+
+function viewLog(logFile) {
+ var divLogViewer = $("#divLogViewer");
+ var txtLogViewerTitle = $("#txtLogViewerTitle");
+ var divLogViewerLoader = $("#divLogViewerLoader");
+ var preLogViewerBody = $("#preLogViewerBody");
+
+ txtLogViewerTitle.text(logFile);
+
+ preLogViewerBody.hide();
+ divLogViewerLoader.show();
+ divLogViewer.show();
+
+ HTTPGetFileRequest({
+ url: "/log/" + logFile + "?limit=2&token=" + token,
+ success: function (response) {
+
+ divLogViewerLoader.hide();
+
+ preLogViewerBody.text(response);
+ preLogViewerBody.show();
+ },
+ objLoaderPlaceholder: divLogViewerLoader
+ });
+
+ return false;
+}
+
+function downloadLog() {
+ var logFile = $("#txtLogViewerTitle").text();
+
+ window.open("/log/" + logFile + "?token=" + token + "&ts=" + (new Date().getTime()), "_blank");
+
+ return false;
+}
+
+function deleteLog() {
+ var logFile = $("#txtLogViewerTitle").text();
+
+ if (!confirm("Are you sure you want to permanently delete the log file '" + logFile + "'?"))
+ return false;
+
+ var btn = $("#btnDeleteLog").button('loading');
+
+ HTTPRequest({
+ url: "/api/deleteLog?token=" + token + "&log=" + logFile,
+ success: function (responseJSON) {
+ refreshLogFilesList();
+
+ $("#divLogViewer").hide();
+ btn.button('reset');
+
+ showAlert("success", "Log Deleted!", "Log file was deleted successfully.");
+ },
+ error: function () {
+ btn.button('reset');
+ },
+ invalidToken: function () {
+ btn.button('reset');
+ showPageLogin();
+ }
+ });
+
+ return false;
+}
+
+function deleteAllLogs() {
+ if (!confirm("Are you sure you want to permanently delete all log files?"))
+ return false;
+
+ HTTPRequest({
+ url: "/api/deleteAllLogs?token=" + token,
+ success: function (responseJSON) {
+ refreshLogFilesList();
+
+ $("#divLogViewer").hide();
+
+ showAlert("success", "Logs Deleted!", "All log files were deleted successfully.");
+ },
+ invalidToken: function () {
+ showPageLogin();
+ }
+ });
+
+ return false;
+}
+
+function deleteAllStats() {
+ if (!confirm("Are you sure you want to permanently delete all stats files?"))
+ return false;
+
+ HTTPRequest({
+ url: "/api/deleteAllStats?token=" + token,
+ success: function (responseJSON) {
+ showAlert("success", "Stats Deleted!", "All stats files were deleted successfully.");
+ },
+ invalidToken: function () {
+ showPageLogin();
+ }
+ });
+
+ return false;
+}
+
+var appsList;
+
+function refreshQueryLogsTab() {
+ var frmQueryLogs = $("#frmQueryLogs");
+ var divQueryLogsLoader = $("#divQueryLogsLoader");
+ var tableQueryLogs = $("#tableQueryLogs");
+
+ var optQueryLogsAppName = $("#optQueryLogsAppName");
+ var optQueryLogsClassPath = $("#optQueryLogsClassPath");
+
+ var currentAppName = optQueryLogsAppName.val();
+ var currentClassPath = optQueryLogsClassPath.val();
+ var loader;
+
+ if (appsList == null) {
+ frmQueryLogs.hide();
+ tableQueryLogs.hide();
+ loader = divQueryLogsLoader;
+ }
+ else {
+ optQueryLogsAppName.prop('disabled', true);
+ optQueryLogsClassPath.prop('disabled', true);
+ }
+
+ HTTPRequest({
+ url: "/api/apps/list?token=" + token,
+ success: function (responseJSON) {
+ var apps = responseJSON.response.apps;
+
+ var optApps = "";
+ var optClassPaths = "";
+
+ for (var i = 0; i < apps.length; i++) {
+ if (apps[i].loggers.length > 0) {
+ optApps += "" + apps[i].name + " ";
+
+ if (currentAppName == null)
+ currentAppName = apps[i].name;
+ }
+ }
+
+ for (var i = 0; i < apps.length; i++) {
+ if (apps[i].name == currentAppName) {
+ for (var j = 0; j < apps[i].loggers.length; j++) {
+ optClassPaths += "" + apps[i].loggers[j].classPath + " ";
+ }
+ }
+ }
+
+ optQueryLogsAppName.html(optApps);
+ optQueryLogsClassPath.html(optClassPaths);
+
+ if (currentAppName != null)
+ optQueryLogsAppName.val(currentAppName);
+
+ if (currentClassPath != null)
+ optQueryLogsClassPath.val(currentClassPath);
+
+ if (appsList == null) {
+ frmQueryLogs.show();
+ loader.hide();
+ }
+ else {
+ optQueryLogsAppName.prop('disabled', false);
+ optQueryLogsClassPath.prop('disabled', false);
+ }
+
+ appsList = apps;
+ },
+ error: function () {
+ if (appsList == null) {
+ frmQueryLogs.show();
+ tableQueryLogs.show();
+ }
+ else {
+ optQueryLogsAppName.prop('disabled', false);
+ optQueryLogsClassPath.prop('disabled', false);
+ }
+ },
+ invalidToken: function () {
+ showPageLogin();
+ },
+ objLoaderPlaceholder: loader
+ });
+}
+
+function queryLogs(pageNumber) {
+ var btn = $("#btnQueryLogs");
+ var divQueryLogsLoader = $("#divQueryLogsLoader");
+ var tableQueryLogs = $("#tableQueryLogs");
+
+ var name = $("#optQueryLogsAppName").val();
+ if (name == null) {
+ showAlert("warning", "Missing!", "Please install a DNS App that does query logging to view its logged data here.");
+ $("#optQueryLogsAppName").focus();
+ return false;
+ }
+
+ var classPath = $("#optQueryLogsClassPath").val();
+ if (classPath == null) {
+ showAlert("warning", "Missing!", "Please select a Class Path to query logs.");
+ $("#optQueryLogsClassPath").focus();
+ return false;
+ }
+
+ var entriesPerPage = $("#optQueryLogsEntriesPerPage").val();
+
+ var start = $("#txtQueryLogStart").val();
+ if (start != "")
+ start = moment(start).format("YYYY-MM-DD HH:mm:ss");
+
+ var end = $("#txtQueryLogEnd").val();
+ if (end != "")
+ end = moment(end).format("YYYY-MM-DD HH:mm:ss");
+
+ var clientIpAddress = $("#txtQueryLogClientIpAddress").val();
+ var protocol = $("#optQueryLogsProtocol").val();
+ var responseType = $("#optQueryLogsResponseType").val();
+ var rcode = $("#optQueryLogsResponseCode").val();
+ var qname = $("#txtQueryLogQName").val();
+ var qtype = $("#optQueryLogQType").val();
+ var qclass = $("#optQueryLogQClass").val();
+
+ tableQueryLogs.hide();
+ divQueryLogsLoader.show();
+
+ btn.button('loading');
+
+ HTTPRequest({
+ url: "/api/queryLogs?token=" + token + "&name=" + encodeURIComponent(name) + "&classPath=" + encodeURIComponent(classPath) + "&pageNumber=" + pageNumber + "&entriesPerPage=" + entriesPerPage +
+ "&start=" + encodeURIComponent(start) + "&end=" + encodeURIComponent(end) + "&clientIpAddress=" + encodeURIComponent(clientIpAddress) + "&protocol=" + protocol + "&responseType=" + responseType + "&rcode=" + rcode +
+ "&qname=" + encodeURIComponent(qname) + "&qtype=" + qtype + "&qclass=" + qclass,
+ success: function (responseJSON) {
+ var tableHtml = "";
+
+ for (var i = 0; i < responseJSON.response.entries.length; i++) {
+ tableHtml += "" + responseJSON.response.entries[i].rowNumber + " " +
+ moment(responseJSON.response.entries[i].timestamp).local().format("YYYY-MM-DD HH:mm:ss") + " " +
+ responseJSON.response.entries[i].clientIpAddress + " " +
+ responseJSON.response.entries[i].protocol + " " +
+ responseJSON.response.entries[i].responseType + " " +
+ responseJSON.response.entries[i].rcode + " " +
+ htmlEncode(responseJSON.response.entries[i].qname) + " " +
+ responseJSON.response.entries[i].qtype + " " +
+ responseJSON.response.entries[i].qclass + " " +
+ htmlEncode(responseJSON.response.entries[i].answer) + " "
+ }
+
+ var paginationHtml = "";
+
+ if (responseJSON.response.pageNumber > 1) {
+ paginationHtml += "« ";
+ paginationHtml += "‹ ";
+ }
+
+ var pageStart = responseJSON.response.pageNumber - 5;
+ if (pageStart < 1)
+ pageStart = 1;
+
+ var pageEnd = pageStart + 9;
+ if (pageEnd > responseJSON.response.totalPages) {
+ var endDiff = pageEnd - responseJSON.response.totalPages;
+ pageEnd = responseJSON.response.totalPages;
+
+ pageStart -= endDiff;
+ if (pageStart < 1)
+ pageStart = 1;
+ }
+
+ for (var i = pageStart; i <= pageEnd; i++) {
+ if (i == responseJSON.response.pageNumber)
+ paginationHtml += "" + i + " ";
+ else
+ paginationHtml += "" + i + " ";
+ }
+
+ if (responseJSON.response.pageNumber < responseJSON.response.totalPages) {
+ paginationHtml += "› ";
+ paginationHtml += "» ";
+ }
+
+ $("#tableQueryLogsBody").html(tableHtml);
+
+ if (responseJSON.response.entries.length > 0)
+ $("#tableQueryLogsFooterStatus").html(responseJSON.response.entries[0].rowNumber + "-" + responseJSON.response.entries[responseJSON.response.entries.length - 1].rowNumber + " (" + responseJSON.response.entries.length + ") of " + responseJSON.response.totalEntries + " logs");
+ else
+ $("#tableQueryLogsFooterStatus").html("0 logs");
+
+ $("#tableQueryLogsFooterPagination").html(paginationHtml);
+
+ btn.button('reset');
+ divQueryLogsLoader.hide();
+ tableQueryLogs.show();
+ },
+ error: function () {
+ btn.button('reset');
+ },
+ invalidToken: function () {
+ showPageLogin();
+ },
+ objLoaderPlaceholder: divQueryLogsLoader
+ });
+
+ return false;
+}
\ No newline at end of file