topguy commited on
Commit
6e44555
·
0 Parent(s):

First commit.

Browse files
Files changed (8) hide show
  1. Dockerfile +23 -0
  2. app.py +164 -0
  3. design.md +79 -0
  4. docker-compose.yml +7 -0
  5. filter_utils.py +56 -0
  6. launch.sh +3 -0
  7. requirements.txt +1 -0
  8. timestamp_utils.py +59 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.11-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy the requirements file into the container at /app
8
+ COPY requirements.txt .
9
+
10
+ # Install any needed packages specified in requirements.txt
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy the rest of the application's code into the container at /app
14
+ COPY . .
15
+
16
+ # Make port 7860 available to the world outside this container
17
+ EXPOSE 7860
18
+
19
+ # Define environment variable
20
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
21
+
22
+ # Run app.py when the container launches
23
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ from filter_utils import filter_lines
4
+
5
+ def add_filter(filters, filter_type, filter_value, case_sensitive):
6
+ if filter_value:
7
+ filters.append({"type": filter_type, "value": filter_value, "case_sensitive": case_sensitive})
8
+ return filters, ""
9
+
10
+ def remove_filter(filters, selected_filter):
11
+ if selected_filter:
12
+ parts = selected_filter.split(" - ")
13
+ if len(parts) == 3:
14
+ filter_type, filter_value, case_str = parts
15
+ case_sensitive = case_str == "Case Sensitive: True"
16
+ for f in filters:
17
+ if f["type"] == filter_type and f["value"] == filter_value and f["case_sensitive"] == case_sensitive:
18
+ filters.remove(f)
19
+ break
20
+ return filters, None
21
+
22
+ def apply_filters(file, filters):
23
+ if file is not None:
24
+ with open(file.name) as f:
25
+ lines = f.readlines()
26
+
27
+ if not filters:
28
+ return "".join(lines)
29
+
30
+ processed_lines = lines
31
+ for f in filters:
32
+ filter_type = f["type"]
33
+ value = f["value"]
34
+ case = f["case_sensitive"]
35
+
36
+ if filter_type == "Include Text":
37
+ processed_lines = filter_lines(processed_lines, include_text=value, case_sensitive=case)
38
+ elif filter_type == "Exclude Text":
39
+ processed_lines = filter_lines(processed_lines, exclude_text=value, case_sensitive=case)
40
+ elif filter_type == "Include Regex":
41
+ processed_lines = filter_lines(processed_lines, include_regex=value, case_sensitive=case)
42
+ elif filter_type == "Exclude Regex":
43
+ processed_lines = filter_lines(processed_lines, exclude_regex=value, case_sensitive=case)
44
+
45
+ return "".join(processed_lines)
46
+ return ""
47
+
48
+ def update_filter_list(filters):
49
+ filter_strings = [f'{f["type"]} - {f["value"]} - Case Sensitive: {f["case_sensitive"]}' for f in filters]
50
+ return gr.update(choices=filter_strings)
51
+
52
+ def save_filters(filters):
53
+ with open("filters.json", "w") as f:
54
+ json.dump(filters, f)
55
+ return "filters.json"
56
+
57
+ def load_filters(filter_file):
58
+ if filter_file is not None:
59
+ with open(filter_file.name) as f:
60
+ return json.load(f)
61
+ return []
62
+
63
+ def save_filtered_log(log_content):
64
+ if log_content:
65
+ with open("filtered_log.txt", "w") as f:
66
+ f.write(log_content)
67
+ return "filtered_log.txt"
68
+ return None
69
+
70
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
71
+ filters_state = gr.State([])
72
+
73
+ gr.Markdown("## Log File Viewer")
74
+
75
+ with gr.Row():
76
+ file_input = gr.File(label="Upload Log File")
77
+
78
+ gr.Markdown("### Filters")
79
+
80
+ with gr.Row():
81
+ filter_type = gr.Dropdown([
82
+ "Include Text", "Exclude Text", "Include Regex", "Exclude Regex"
83
+ ], label="Filter Type")
84
+ filter_value = gr.Textbox(label="Filter Value")
85
+ case_sensitive_checkbox = gr.Checkbox(label="Case Sensitive", value=True)
86
+ with gr.Column(scale=0):
87
+ add_filter_button = gr.Button("Add Filter")
88
+ save_filters_button = gr.Button("Save Filters")
89
+ load_filters_file = gr.UploadButton("Load Filters (.json)", file_types=[".json"])
90
+
91
+
92
+
93
+ with gr.Row():
94
+ applied_filters_list = gr.Radio(label="Applied Filters", interactive=True)
95
+ remove_filter_button = gr.Button("Remove Selected Filter")
96
+
97
+ with gr.Row():
98
+ save_filtered_log_button = gr.Button("Save Filtered Log")
99
+ text_output = gr.Textbox(label="Log Content", lines=20, max_lines=1000, interactive=False, elem_id="log_content")
100
+
101
+ # Event Handlers
102
+ add_filter_button.click(
103
+ add_filter,
104
+ inputs=[filters_state, filter_type, filter_value, case_sensitive_checkbox],
105
+ outputs=[filters_state, filter_value]
106
+ ).then(
107
+ update_filter_list,
108
+ inputs=filters_state,
109
+ outputs=applied_filters_list
110
+ ).then(
111
+ apply_filters,
112
+ inputs=[file_input, filters_state],
113
+ outputs=text_output
114
+ )
115
+
116
+ remove_filter_button.click(
117
+ remove_filter,
118
+ inputs=[filters_state, applied_filters_list],
119
+ outputs=[filters_state, applied_filters_list]
120
+ ).then(
121
+ update_filter_list,
122
+ inputs=filters_state,
123
+ outputs=applied_filters_list
124
+ ).then(
125
+ apply_filters,
126
+ inputs=[file_input, filters_state],
127
+ outputs=text_output
128
+ )
129
+
130
+ save_filters_button.click(
131
+ save_filters,
132
+ inputs=filters_state,
133
+ outputs=gr.File(label="Download Filter File")
134
+ )
135
+
136
+ load_filters_file.upload(
137
+ load_filters,
138
+ inputs=load_filters_file,
139
+ outputs=filters_state
140
+ ).then(
141
+ update_filter_list,
142
+ inputs=filters_state,
143
+ outputs=applied_filters_list
144
+ ).then(
145
+ apply_filters,
146
+ inputs=[file_input, filters_state],
147
+ outputs=text_output
148
+ )
149
+
150
+ file_input.upload(
151
+ apply_filters,
152
+ inputs=[file_input, filters_state],
153
+ outputs=text_output
154
+ )
155
+
156
+ save_filtered_log_button.click(
157
+ save_filtered_log,
158
+ inputs=text_output,
159
+ outputs=gr.File(label="Download Filtered Log")
160
+ )
161
+
162
+ if __name__ == "__main__":
163
+ demo.launch()
164
+ demo.launch()
design.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LogViewer Application Design
2
+
3
+ ## 1. Overview
4
+
5
+ LogViewer is a web-based application built with Python and Gradio that allows users to upload and inspect log files. It provides dynamic filtering capabilities to help users analyze log data by including or excluding lines based on text or regular expressions.
6
+
7
+ ## 2. Components
8
+
9
+ The application consists of two main Python files:
10
+
11
+ ### 2.1. `app.py`
12
+
13
+ This is the main application file that creates the user interface and handles all user interactions.
14
+
15
+ * **UI Structure:** The UI is built using `gradio.Blocks` with the `Soft` theme. It includes:
16
+ * A file upload component (`gr.File`) for loading log files.
17
+ * A large, non-interactive textbox (`gr.Textbox`) to display the processed log content.
18
+ * A section for adding new filters, containing:
19
+ * A dropdown (`gr.Dropdown`) to select the filter type (Include/Exclude Text, Include/Exclude Regex).
20
+ * A textbox (`gr.Textbox`) to input the filter pattern.
21
+ * A checkbox (`gr.Checkbox`) to control case sensitivity for the filter.
22
+ * An "Add Filter" button (`gr.Button`).
23
+ * "Save Filters" button (`gr.Button`) to download the current filter set as a JSON file.
24
+ * "Load Filters" button (`gr.UploadButton`) to upload a JSON file and apply saved filters.
25
+ * A section to display and manage active filters:
26
+ * A radio button group (`gr.Radio`) that lists the currently applied filters, showing their type, value, and case sensitivity.
27
+ * A "Remove Selected Filter" button (`gr.Button`).
28
+ * A "Save Filtered Log" button (`gr.Button`) to download the currently displayed filtered log content.
29
+
30
+ * **State Management:**
31
+ * A `gr.State` object (`filters_state`) is used to maintain the list of active filters as a list of dictionaries. Each dictionary represents a single filter.
32
+
33
+ * **Core Logic:**
34
+ * **`add_filter()`:** Adds a new filter dictionary to the `filters_state` list.
35
+ * **`remove_filter()`:** Removes a filter from the `filters_state` list based on the user's selection.
36
+ * **`apply_filters()`:** This is the main processing function. It reads the uploaded file and applies the sequence of filters stored in `filters_state` by calling the `filter_lines` utility function for each filter.
37
+ * **`update_filter_list()`:** Generates a list of strings from the `filters_state` list to display it in the UI.
38
+ * **`save_filters()`:** Saves the current `filters_state` to a JSON file.
39
+ * **`load_filters()`:** Loads filters from a JSON file into `filters_state`.
40
+ * **`save_filtered_log()`:** Saves the current content of the log display to a text file.
41
+
42
+ * **Event Handling:**
43
+ * Uploading a file triggers `apply_filters`.
44
+ * Clicking "Add Filter" triggers `add_filter`, then `update_filter_list`, and finally `apply_filters`.
45
+ * Clicking "Remove Selected Filter" triggers `remove_filter`, then `update_filter_list`, and finally `apply_filters`.
46
+ * Clicking "Save Filters" triggers `save_filters`.
47
+ * Uploading a file to "Load Filters" triggers `load_filters`, then `update_filter_list`, and finally `apply_filters`.
48
+ * Clicking "Save Filtered Log" triggers `save_filtered_log`.
49
+
50
+ ### 2.2. `filter_utils.py`
51
+
52
+ This module provides the core filtering logic.
53
+
54
+ * **`filter_lines()`:** A pure function that takes a list of text lines and applies a single filtering criterion (e.g., include text, exclude regex). It handles both plain text and regular expression matching, with an option for case sensitivity. It returns a new list of filtered lines.
55
+
56
+ ### 2.3. `timestamp_utils.py`
57
+
58
+ This module provides utilities for parsing and filtering log lines based on timestamps.
59
+
60
+ * **`parse_timestamp()`:** Attempts to parse a timestamp from a log line. Designed to be extensible for various timestamp formats.
61
+ * **`get_timestamp_range()`:** Extracts the earliest and latest timestamps from a list of log lines.
62
+ * **`filter_by_time_range()`:** Filters log lines to include only those within a specified time range, based on parsed timestamps.
63
+
64
+ ## 3. User Interaction Flow
65
+
66
+ 1. The user opens the application in their browser.
67
+ 2. The user uploads a log file using the file upload component. The log content is immediately displayed.
68
+ 3. To filter the log, the user selects a filter type, enters a value, and clicks "Add Filter".
69
+ 4. The filter is added to the "Applied Filters" list, and the log view is automatically updated to reflect the new filter.
70
+ 5. The user can add multiple filters sequentially. Each new filter is applied to the result of the previous ones.
71
+ 6. To remove a filter, the user selects it from the "Applied Filters" radio group.
72
+ 7. The user then clicks the "Remove Selected Filter" button.
73
+ 8. The filter is removed from the list, and the log view is updated to reflect the change.
74
+ 9. The user can save the current set of filters by clicking the "Save Filters" button, which will prompt a file download.
75
+ 10. The user can load a previously saved set of filters by clicking the "Load Filters" button and selecting a JSON file.
76
+
77
+ 11. The user can save the currently displayed filtered log content by clicking the "Save Filtered Log" button, which will prompt a file download.
78
+
79
+ This design allows for a flexible and interactive way to analyze log files by building a chain of filters.
docker-compose.yml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ services:
2
+ logviewer:
3
+ build: .
4
+ ports:
5
+ - "7860:7860"
6
+ volumes:
7
+ - .:/app
filter_utils.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import re
3
+
4
+ def filter_lines(lines, include_text=None, exclude_text=None, include_regex=None, exclude_regex=None, case_sensitive=True):
5
+ """
6
+ Filters a list of text lines based on include/exclude criteria for both plain text and regex.
7
+
8
+ Args:
9
+ lines (list): A list of strings, where each string is a line of text.
10
+ include_text (str, optional): Text that must be present in a line for it to be included.
11
+ exclude_text (str, optional): Text that must not be present in a line for it to be included.
12
+ include_regex (str, optional): A regex pattern that a line must match to be included.
13
+ exclude_regex (str, optional): A regex pattern that a line must not match to be included.
14
+ case_sensitive (bool, optional): If False, all text comparisons will be case-insensitive. Defaults to True.
15
+
16
+ Returns:
17
+ list: A new list of strings containing the filtered lines.
18
+ """
19
+ filtered = lines
20
+
21
+ flags = 0 if case_sensitive else re.IGNORECASE
22
+
23
+ # Include text filter
24
+ if include_text:
25
+ if case_sensitive:
26
+ filtered = [line for line in filtered if include_text in line]
27
+ else:
28
+ filtered = [line for line in filtered if include_text.lower() in line.lower()]
29
+
30
+ # Exclude text filter
31
+ if exclude_text:
32
+ if case_sensitive:
33
+ filtered = [line for line in filtered if exclude_text not in line]
34
+ else:
35
+ filtered = [line for line in filtered if exclude_text.lower() not in line.lower()]
36
+
37
+ # Include regex filter
38
+ if include_regex:
39
+ try:
40
+ pattern = re.compile(include_regex, flags)
41
+ filtered = [line for line in filtered if pattern.search(line)]
42
+ except re.error as e:
43
+ # Handle invalid regex gracefully, maybe log the error or return an empty list
44
+ print(f"Invalid include regex: {e}")
45
+ return []
46
+
47
+ # Exclude regex filter
48
+ if exclude_regex:
49
+ try:
50
+ pattern = re.compile(exclude_regex, flags)
51
+ filtered = [line for line in filtered if not pattern.search(line)]
52
+ except re.error as e:
53
+ print(f"Invalid exclude regex: {e}")
54
+ return []
55
+
56
+ return filtered
launch.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/bash
2
+ source ~/.virtualenvs/LogViewer/bin/activate
3
+ python "/media/topguy/T7 Shield/AIPlayGround/LogViewer/app.py"
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ gradio
timestamp_utils.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import re
3
+
4
+ def parse_timestamp(log_line: str) -> datetime.datetime | None:
5
+ """
6
+ Attempts to parse a timestamp from the beginning of a log line.
7
+ This is a placeholder and should be extended to handle various timestamp formats.
8
+ """
9
+ # Example: YYYY-MM-DD HH:MM:SS.milliseconds or YYYY-MM-DDTHH:MM:SS,SSS
10
+ # This regex is a starting point and might need to be adjusted for specific log formats.
11
+ match = re.match(r'^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[.,]\d{3})', log_line)
12
+ if match:
13
+ try:
14
+ # Try parsing with milliseconds
15
+ return datetime.datetime.strptime(match.group(1).replace('T', ' ').replace(',', '.'), '%Y-%m-%d %H:%M:%S.%f')
16
+ except ValueError:
17
+ pass
18
+
19
+ # Add more parsing attempts for other common formats here
20
+ # Example: "MMM DD HH:MM:SS" (e.g., "Jun 28 10:30:00")
21
+ match = re.match(r'^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}', log_line)
22
+ if match:
23
+ try:
24
+ # For this format, we might need to assume the current year or pass it in
25
+ # For simplicity, let's just return None for now if it's not a full datetime
26
+ pass
27
+ except ValueError:
28
+ pass
29
+
30
+ return None
31
+
32
+ def get_timestamp_range(log_lines: list[str]) -> tuple[datetime.datetime | None, datetime.datetime | None]:
33
+ """
34
+ Parses timestamps from a list of log lines and returns the earliest and latest timestamps found.
35
+ """
36
+ min_ts = None
37
+ max_ts = None
38
+ for line in log_lines:
39
+ ts = parse_timestamp(line)
40
+ if ts:
41
+ if min_ts is None or ts < min_ts:
42
+ min_ts = ts
43
+ if max_ts is None or ts > max_ts:
44
+ max_ts = ts
45
+ return min_ts, max_ts
46
+
47
+ def filter_by_time_range(log_lines: list[str], start_time: datetime.datetime | None, end_time: datetime.datetime | None) -> list[str]:
48
+ """
49
+ Filters log lines to include only those within a specified time range.
50
+ Lines without a parseable timestamp are excluded.
51
+ """
52
+ filtered_lines = []
53
+ for line in log_lines:
54
+ ts = parse_timestamp(line)
55
+ if ts:
56
+ if (start_time is None or ts >= start_time) and \
57
+ (end_time is None or ts <= end_time):
58
+ filtered_lines.append(line)
59
+ return filtered_lines