This commit is contained in:
2026-04-27 18:57:25 +07:00
commit 6ae333eea5
7 changed files with 255 additions and 0 deletions

8
Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
# Use the lightest official nginx image
FROM nginx:alpine
# Copy static files to the nginx server directory
COPY . /usr/share/nginx/html
# Expose port 80
EXPOSE 80

6
css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

5
css/dataTables.bootstrap5.min.css vendored Normal file

File diff suppressed because one or more lines are too long

226
index.html Normal file
View File

@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log Parser & Viewer</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/dataTables.bootstrap5.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
padding: 20px;
}
#drop-zone {
border: 2px dashed #007bff;
border-radius: 10px;
padding: 40px;
text-align: center;
background: white;
transition: 0.3s;
cursor: pointer;
margin-bottom: 20px;
}
#drop-zone.dragover {
background: #e7f1ff;
border-color: #0056b3;
}
.stack-trace {
font-family: monospace;
font-size: 0.85rem;
color: #d63384;
white-space: pre-wrap;
display: block;
margin-top: 5px;
}
.lvl-ERR {
color: #dc3545;
font-weight: bold;
}
.lvl-INF {
color: #0d6efd;
}
.lvl-DBG {
color: #6c757d;
}
.lvl-TRC {
color: #6610f2;
}
.table-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
</style>
</head>
<body>
<div class="container-fluid">
<h2 class="mb-4">Log File Parser</h2>
<div id="drop-zone">
<h4>Drag & Drop log file here</h4>
<p class="text-muted">or click to select a file</p>
<input type="file" id="file-input" style="display: none;" accept=".log,.txt">
</div>
<div class="table-container shadow-sm">
<div class="row mb-3">
<div class="col-md-3">
<label class="form-label">Filter by Level</label>
<select id="level-filter" class="form-select">
<option value="">All Levels</option>
<option value="INF">INF (Info)</option>
<option value="ERR">ERR (Error)</option>
<option value="DBG">DBG (Debug)</option>
<option value="TRC">TRC (Trace)</option>
</select>
</div>
</div>
<table id="log-table" class="table table-hover table-bordered w-100">
<thead class="table-light">
<tr>
<th>Time</th>
<th>Level</th>
<th>ID</th>
<th>Tag</th>
<th>Message</th>
<th>Properties</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<script src="js/jquery-3.7.0.min.js"></script>
<script src="js/jquery.dataTables.min.js"></script>
<script src="js/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function () {
let table = $('#log-table').DataTable({
order: [[0, 'asc']],
pageLength: 25,
dom: '<"d-flex justify-content-between"lf>rtip',
columns: [
{ data: 'time', width: '10%' },
{
data: 'level',
width: '5%',
render: function (data) {
return `<span class="lvl-${data}">${data}</span>`;
}
},
{ data: 'id', width: '5%' },
{ data: 'tag', width: '8%' },
{
data: 'message',
render: function (data) {
// Split message and stack trace if exists
const parts = data.split('\n');
const mainMsg = parts.shift();
const stack = parts.join('\n');
return `<strong>${mainMsg}</strong>` + (stack ? `<span class="stack-trace">${stack}</span>` : '');
}
},
{ data: 'props', width: '20%' }
]
});
// Custom filtering for Log Level
$('#level-filter').on('change', function () {
table.column(1).search(this.value).draw();
});
// File Handling
const dropZone = $('#drop-zone');
const fileInput = $('#file-input');
dropZone.on('click', () => fileInput.click());
dropZone.on('dragover', (e) => {
e.preventDefault();
dropZone.addClass('dragover');
});
dropZone.on('dragleave', () => dropZone.removeClass('dragover'));
dropZone.on('drop', (e) => {
e.preventDefault();
dropZone.removeClass('dragover');
const file = e.originalEvent.dataTransfer.files[0];
if (file) handleFile(file);
});
fileInput.on('change', (e) => {
if (e.target.files[0]) handleFile(e.target.files[0]);
});
function handleFile(file) {
const reader = new FileReader();
reader.onload = function (e) {
const content = e.target.result;
const parsedData = parseLogs(content);
table.clear().rows.add(parsedData).draw();
};
reader.readAsText(file);
}
function parseLogs(text) {
const lines = text.split(/\r?\n/);
const logs = [];
// Regex to match: Time [LVL] ID | (TAG) Message (PROPS)
const logHeaderRegex = /^(\d{2}:\d{2}:\d{2}\.\d{3})\s\[(\w+)\]\s(\d+)\s\|\s\((.*?)\)\s(.*)$/;
lines.forEach(line => {
if (!line.trim()) return;
const match = line.match(logHeaderRegex);
if (match) {
let messagePart = match[5];
let props = "";
// Extract props inside parentheses at the end of the line
const propMatch = messagePart.match(/\(([^)]+:[^)]+)\)$/);
if (propMatch) {
props = propMatch[1];
messagePart = messagePart.replace(propMatch[0], "").trim();
}
logs.push({
time: match[1],
level: match[2],
id: match[3],
tag: match[4],
message: messagePart,
props: props
});
} else {
// If it doesn't match the header, it's likely a stack trace or multi-line message
if (logs.length > 0) {
logs[logs.length - 1].message += "\n" + line.trim();
}
}
});
return logs;
}
});
</script>
</body>
</html>

4
js/dataTables.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
js/jquery-3.7.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

4
js/jquery.dataTables.min.js vendored Normal file

File diff suppressed because one or more lines are too long