Skip to content

Instantly share code, notes, and snippets.

@santakdalai90
Last active November 17, 2025 05:16
Show Gist options
  • Select an option

  • Save santakdalai90/e0893c86928a230901acf0e6f28c5e93 to your computer and use it in GitHub Desktop.

Select an option

Save santakdalai90/e0893c86928a230901acf0e6f28c5e93 to your computer and use it in GitHub Desktop.
JSON Log Viewer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Filterable JSON Log Viewer</title>
<style>
/* --- General Reset & Structure --- */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f2f5; /* Very light, soft background */
color: #333;
line-height: 1.6;
}
h1 {
color: #1e3a8a; /* Dark blue primary color */
margin-bottom: 5px;
}
/* --- Main Containers & Input Area --- */
#log-header {
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
#controls {
background-color: #ffffff;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
/* --- Form Elements --- */
.control-group {
margin-right: 25px;
display: inline-block;
}
input[type="text"], select, #fileInput {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
transition: border-color 0.3s;
}
input[type="text"]:focus, select:focus {
border-color: #3b82f6; /* Blue focus highlight */
outline: none;
}
button {
background-color: #3b82f6; /* Primary blue button */
color: white;
border: none;
padding: 8px 15px;
cursor: pointer;
border-radius: 4px;
font-weight: 600;
transition: background-color 0.3s;
}
button:hover {
background-color: #2563eb;
}
hr {
border: none;
border-top: 1px solid #eee;
margin: 15px 0;
}
/* --- Table Styles --- */
#logTableContainer {
margin-top: 20px;
overflow-x: auto;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
#logTable {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
}
#logTable th, #logTable td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #f0f0f0; /* Very light border */
}
#logTable th {
background-color: #eef2ff; /* Very light blue header background */
color: #1e3a8a;
cursor: pointer;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85em;
}
#logTable tr:hover {
background-color: #f7f9fc;
}
/* --- Log Level Tag Styling --- */
.level-tag {
padding: 4px 10px;
border-radius: 16px;
font-size: 0.75em;
font-weight: 700;
color: white;
text-transform: uppercase;
display: inline-block;
}
.level-info { background-color: #3b82f6; } /* Blue */
.level-warn { background-color: #f59e0b; color: #333;} /* Amber/Yellow */
.level-error { background-color: #ef4444; } /* Red */
.level-debug { background-color: #9ca3af; } /* Grey */
.level-unknown { background-color: #4b5563; } /* Dark Grey */
/* --- Message Column Styles --- */
.message-cell {
white-space: pre-wrap;
font-family: monospace;
font-size: 0.8em;
color: #555;
}
.error-message {
color: #ef4444;
font-weight: bold;
}
</style>
</head>
<body>
<div id="log-header">
<h1>Filterable JSON Log Viewer 📊</h1>
<p>Upload a file where each line is a single JSON log entry to view and filter logs below.</p>
<input type="file" id="fileInput" accept=".log,.txt,text/plain" />
<button onclick="processFile()">Process Log File</button>
</div>
<div id="controls" style="display: none;">
<div class="control-group">
<label for="levelFilter">Filter by Level:</label>
<select id="levelFilter" onchange="applyFilters()">
<option value="all">All Levels</option>
</select>
</div>
<div class="control-group">
<label for="searchBox">Search Message:</label>
<input type="text" id="searchBox" placeholder="Enter text to search" onkeyup="applyFilters()">
</div>
</div>
<div id="logTableContainer">
<p>Upload a file to view logs here.</p>
</div>
<script>
// Store all parsed log entries globally for easy filtering/searching
let allLogs = [];
function processFile() {
const inputElement = document.getElementById('fileInput');
const outputElement = document.getElementById('logTableContainer');
const file = inputElement.files[0];
outputElement.innerHTML = ''; // Clear previous output
allLogs = []; // Reset logs
if (!file) {
outputElement.innerHTML = '<p class="error-message">Please select a file to process.</p>';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const fileContent = e.target.result;
const logLines = fileContent.split('\n');
logLines.forEach((line, index) => {
const trimmedLine = line.trim();
if (trimmedLine === '') return;
try {
const logObject = JSON.parse(trimmedLine);
allLogs.push({
time: logObject.time || 'N/A',
level: (logObject.level || 'unknown').toLowerCase(),
message: trimmedLine,
originalIndex: index
});
} catch (error) {
allLogs.push({
time: `Line ${index + 1}`,
level: 'unknown',
message: `Error parsing JSON: ${trimmedLine}`,
originalIndex: index
});
}
});
buildTable(allLogs);
populateLevelFilter();
document.getElementById('controls').style.display = 'block';
};
reader.readAsText(file);
}
function buildTable(logs) {
const container = document.getElementById('logTableContainer');
if (logs.length === 0) {
container.innerHTML = '<p>No log entries found or processed.</p>';
return;
}
let tableHTML = `
<table id="logTable">
<thead>
<tr>
<th onclick="sortTable(0)">Time</th>
<th onclick="sortTable(1)">Level</th>
<th onclick="sortTable(2)">Message</th>
</tr>
</thead>
<tbody>
`;
logs.forEach(log => {
const levelClass = `level-${log.level}`;
tableHTML += `
<tr>
<td>${log.time}</td>
<td><span class="level-tag ${levelClass}">${log.level}</span></td>
<td class="message-cell">${log.message}</td>
</tr>
`;
});
tableHTML += `
</tbody>
</table>
`;
container.innerHTML = tableHTML;
}
function populateLevelFilter() {
const filter = document.getElementById('levelFilter');
filter.innerHTML = '<option value="all">All Levels</option>';
const uniqueLevels = [...new Set(allLogs.map(log => log.level))].sort();
uniqueLevels.forEach(level => {
const option = document.createElement('option');
option.value = level;
option.textContent = level.charAt(0).toUpperCase() + level.slice(1);
filter.appendChild(option);
});
}
function applyFilters() {
const levelFilterValue = document.getElementById('levelFilter').value;
const searchBoxValue = document.getElementById('searchBox').value.toLowerCase();
const filteredLogs = allLogs.filter(log => {
const levelMatch = (levelFilterValue === 'all' || log.level === levelFilterValue);
const searchMatch = log.message.toLowerCase().includes(searchBoxValue);
return levelMatch && searchMatch;
});
buildTable(filteredLogs);
}
function sortTable(columnIndex) {
const tbody = document.getElementById('logTable').querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isAscending = tbody.dataset.sortOrder !== 'asc';
tbody.dataset.sortOrder = isAscending ? 'asc' : 'desc';
rows.sort((rowA, rowB) => {
const cellA = rowA.cells[columnIndex].textContent.toLowerCase();
const cellB = rowB.cells[columnIndex].textContent.toLowerCase();
let comparison = 0;
if (cellA > cellB) {
comparison = 1;
} else if (cellA < cellB) {
comparison = -1;
}
return isAscending ? comparison : comparison * -1;
});
rows.forEach(row => tbody.appendChild(row));
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment