from datetime import datetime, timedelta import sys from PyQt6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QDateEdit, QGridLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QListWidgetItem, QFrame, QFileDialog, QMessageBox ) from parser import ApacheLogEntry DATA_ROLE = 1001 LINES_PER_PAGE = 100 LABELS = [ "Host", "Date", "Time", "Timezone", "Status code", "Method", "Path", "Size", ] class LogViewer(QWidget): def __init__(self): self.value_labels = {} super().__init__() self.init() self.current_page = 0 self.total_pages = 0 self.file_lines = [] def init(self): self.setWindowTitle("Log Viewer") self.setGeometry(100, 100, 900, 500) mainLayout = QVBoxLayout() topLayout = QHBoxLayout() self.pathLabel = QLabel("Select working file...") self.openButton = QPushButton("Open") self.openButton.clicked.connect(self.open_file_dialog) topLayout.addWidget(self.pathLabel) topLayout.addWidget(self.openButton) filterLayout = QHBoxLayout() self.fromDateEdit = QDateEdit() self.fromDateEdit.setDate(datetime.now() - timedelta(days=7)) self.toDateEdit = QDateEdit() self.toDateEdit.setDate(datetime.now()) self.filterButton = QPushButton("Filter") self.filterButton.clicked.connect(self.apply_date_filter) filterLayout.addWidget(QLabel("From")) filterLayout.addWidget(self.fromDateEdit) filterLayout.addWidget(QLabel("To")) filterLayout.addWidget(self.toDateEdit) filterLayout.addWidget(self.filterButton) middleLayout = QHBoxLayout() self.logListWidget = QListWidget() self.logListWidget.itemClicked.connect(self.select_log) middleLayout.addWidget(self.logListWidget, 2) self.detailsFrame = QFrame() detailsLayout = QVBoxLayout(self.detailsFrame) self.create_details_widget(detailsLayout) middleLayout.addWidget(self.detailsFrame, 2) bottomLayout = QHBoxLayout() self.prevButton = QPushButton("Previous") self.nextButton = QPushButton("Next") self.prevButton.clicked.connect(self.show_previous_page) self.nextButton.clicked.connect(self.show_next_page) self.pageLabel = QLabel("") bottomLayout.addWidget(self.prevButton) bottomLayout.addStretch() bottomLayout.addWidget(self.pageLabel) bottomLayout.addStretch() bottomLayout.addWidget(self.nextButton) mainLayout.addLayout(topLayout) mainLayout.addLayout(filterLayout) mainLayout.addLayout(middleLayout) mainLayout.addLayout(bottomLayout) self.setLayout(mainLayout) def reset_ui(self): self.pathLabel.setText("Select working file...") self.prevButton.setEnabled(False) self.nextButton.setEnabled(True) self.pageLabel.setText("") self.logListWidget.clear() for label in LABELS: self.value_labels[label].setText("") def apply_date_filter(self): from_date = self.fromDateEdit.date().toPyDate() to_date = self.toDateEdit.date().toPyDate() if from_date > to_date: self.show_error("Date 'From' must not be after date 'To'") self.filtered_lines = [] for line in self.file_lines: log = ApacheLogEntry.from_log(line) if not log: self.show_parse_error(line) continue date = datetime.strptime(log.timestamp.strftime("%Y-%m-%d"), "%Y-%m-%d").date() if from_date <= date: if date <= to_date: self.filtered_lines.append(line) else: break self.total_pages = (len(self.filtered_lines) - 1) // LINES_PER_PAGE + 1 self.current_page = 0 self.update_list() def select_log(self, item): log = item.data(DATA_ROLE) self.value_labels["Host"].setText(log.host) self.value_labels["Date"].setText(log.timestamp.strftime("%Y-%m-%d")) self.value_labels["Time"].setText(log.timestamp.strftime("%H:%M:%S")) self.value_labels["Timezone"].setText(log.timestamp.strftime("%z")) self.value_labels["Status code"].setText(str(log.status_code)) self.value_labels["Method"].setText(log.method) self.value_labels["Path"].setText(log.path) self.value_labels["Size"].setText(str(log.bytes_sent or "-")) def create_details_widget(self, layout): gridLayout = QGridLayout() for index, label in enumerate(LABELS): labelWidget = QLabel(f"{label}:") valueLabel = QLabel() gridLayout.addWidget(labelWidget, index, 0) gridLayout.addWidget(valueLabel, index, 1) self.value_labels[label] = valueLabel layout.addLayout(gridLayout) def open_file_dialog(self): fileName, _ = QFileDialog.getOpenFileName( self, "Open Log File", "", "All Files (*)", ) if fileName: self.pathLabel.setText(fileName) self.load_log_file(fileName) def load_log_file(self, file_path): self.file_lines = [] self.reset_ui() try: with open(file_path, "r") as file: self.file_lines = file.readlines() self.filtered_lines = self.file_lines except: self.show_error(f"could not open file {file_path}") return self.total_pages = (len(self.file_lines) - 1) // LINES_PER_PAGE + 1 if self.total_pages == 1: self.nextButton.setEnabled(False) self.current_page = 0 self.update_list() def update_list(self): self.logListWidget.clear() start_index = self.current_page * LINES_PER_PAGE end_index = start_index + LINES_PER_PAGE self.pageLabel.setText(f"{self.current_page + 1} / {max(1, self.total_pages)}") for line in self.filtered_lines[start_index:end_index]: parsed_data = ApacheLogEntry.from_log(line.strip()) if not parsed_data: self.show_parse_error(line) continue item = QListWidgetItem(line.strip()) item.setData(DATA_ROLE, parsed_data) self.logListWidget.addItem(item) def show_next_page(self): if self.current_page < self.total_pages - 1: self.current_page += 1 self.update_list() self.prevButton.setEnabled(True) if self.current_page == self.total_pages - 1: self.nextButton.setEnabled(False) def show_previous_page(self): if self.current_page > 0: self.current_page -= 1 self.update_list() self.nextButton.setEnabled(True) if self.current_page == 0: self.prevButton.setEnabled(False) def show_error(self, text): message_box = QMessageBox() message_box.setIcon(QMessageBox.Icon.Critical) message_box.setText("Error") message_box.setWindowTitle("Error") message_box.setInformativeText(text) message_box.exec() def show_parse_error(self, line): self.show_error(f"Failed to parse data from line: {line}, ignoring.") def main(): app = QApplication(sys.argv) ex = LogViewer() ex.show() sys.exit(app.exec()) if __name__ == "__main__": main()