1 | # lognestmonster Copyright (c) 2019 Joshua 'joshuas3' Stockin |
2 | # <https://github.com/JoshuaS3/lognestmonster/>. |
3 |
|
4 |
|
5 | # This file is part of lognestmonster. |
6 |
|
7 | # lognestmonster is free software: you can redistribute it and/or modify |
8 | # it under the terms of the GNU General Public License as published by |
9 | # the Free Software Foundation, either version 3 of the License, or |
10 | # (at your option) any later version. |
11 |
|
12 | # lognestmonster is distributed in the hope that it will be useful, |
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | # GNU General Public License for more details. |
16 |
|
17 | # You should have received a copy of the GNU General Public License |
18 | # along with lognestmonster. If not, see <https://www.gnu.org/licenses/>. |
19 |
|
20 | import struct |
21 | import os |
22 |
|
23 | STATEMENT_START = 0 |
24 | STATEMENT_END = 1 |
25 | EVENT_START = 2 |
26 | EVENT_END = 3 |
27 |
|
28 | VERBOSITY_LEVELS = { |
29 | 0: "INIT", |
30 | 1: "DEBUG", |
31 | 2: "VERBOSE", |
32 | 3: "VERYVERBOSE", |
33 | 4: "WARNING", |
34 | 5: "ERROR" |
35 | } |
36 |
|
37 | def ulonglong(bytestr): |
38 | return struct.unpack("@Q", bytestr)[0] |
39 | def uchar(charv): |
40 | return struct.unpack("@B", charv)[0] |
41 | def ushort(shortv): |
42 | return struct.unpack("@H", shortv)[0] |
43 |
|
44 |
|
45 | class EventProto: |
46 | parent = None |
47 | pushed = [False] |
48 | def __init__(self): |
49 | self.parent = None |
50 | self.pushed = [False] |
51 |
|
52 | class Reader: |
53 | fd = None |
54 |
|
55 | top_level = [] |
56 |
|
57 | current_event = None |
58 |
|
59 | event_count = 0 |
60 | statement_count = 0 |
61 |
|
62 | seekable = True |
63 | file_size = 0 |
64 | position = 0 |
65 |
|
66 | version = 0 |
67 | timestamp = 0 |
68 |
|
69 | bad_bytes = 0 |
70 |
|
71 | filter_time_start = -1 |
72 | filter_time_end = -1 |
73 | filter_verbosity = -1 |
74 | filter_tag = -1 |
75 |
|
76 | update_callbacks = [] |
77 |
|
78 | def __init__(self, fd, seekable=True): |
79 | self.fd = fd |
80 |
|
81 | self.top_level = [] |
82 |
|
83 | self.current_event = None |
84 |
|
85 | self.event_count = 0 |
86 | self.statement_count = 0 |
87 |
|
88 | self.seekable = seekable |
89 | self.file_size = 0 |
90 | self.position = 0 |
91 |
|
92 | self.version = 0 |
93 | self.timestamp = 0 |
94 |
|
95 | self.bad_bytes = 0 |
96 |
|
97 | self.filter_time_start = -1 |
98 | self.filter_time_end = -1 |
99 | self.filter_verbosity = -1 |
100 | self.filter_tag = -1 |
101 |
|
102 | self.update_callbacks = [] |
103 |
|
104 | def update(self): |
105 | for callback in self.update_callbacks: |
106 | callback() |
107 |
|
108 | def onupdate(self, callback): |
109 | self.update_callbacks.append(callback) |
110 |
|
111 | def size(self): |
112 | self.fd.seek(0, os.SEEK_END) # go to end of file and get position |
113 | newsize = self.fd.tell() |
114 | self.fd.seek(self.position) # return to previous position |
115 |
|
116 | is_diff = self.file_size is not newsize |
117 | self.file_size = newsize |
118 | return is_diff |
119 |
|
120 | def pos(self): |
121 | p = self.fd.tell() |
122 | self.position = p |
123 | return p |
124 |
|
125 | def seek(self, position): |
126 | self.position = position |
127 | self.fd.seek(self.position) |
128 |
|
129 | def read(self, byte_count): |
130 | return self.fd.read(byte_count) |
131 |
|
132 | def fetch_item(self, position): |
133 | previouspos = self.pos() |
134 | self.seek(position) |
135 | timestamp = ulonglong(self.read(8)) |
136 | verbosity = uchar(self.read(1)) |
137 | tag_size = uchar(self.read(1)) |
138 | tag = self.read(tag_size).decode("utf-8") |
139 | message_size = ushort(self.read(2)) |
140 | message = self.read(message_size).decode("utf-8") |
141 | self.seek(previouspos) |
142 | return (timestamp, verbosity, tag_size, tag, message_size, message) |
143 |
|
144 | def parse_block(self, in_byte): |
145 | if in_byte == STATEMENT_START: # the byte indicates a statement's start, begin interpreting |
146 | if self.seekable: |
147 | this_position = self.position + 1 # identify and save the seeker position of this statement |
148 |
|
149 | try: |
150 | timestamp = ulonglong(self.read(8)) |
151 | verbosity = uchar(self.read(1)) |
152 | tag_size = uchar(self.read(1)) |
153 | tag = self.read(tag_size).decode("utf-8") |
154 | |
155 | append = True |
156 |
|
157 | if self.filter_time_start is not -1 and append: |
158 | append = timestamp > self.filter_time_start |
159 |
|
160 | if self.filter_time_end is not -1 and append: |
161 | append = timestamp < self.filter_time_end |
162 |
|
163 | if self.filter_verbosity is not -1 and append: |
164 | append = verbosity in self.filter_verbosity |
165 |
|
166 | if self.filter_tag is not -1 and append: |
167 | append = tag == self.filter_tag |
168 |
|
169 | message_size = ushort(self.read(2)) |
170 | if self.seekable: |
171 | self.read(message_size) |
172 | else: |
173 | message = self.read(message_size).decode("utf-8") |
174 | |
175 | if self.seekable: |
176 | while uchar(self.read(1)) is not STATEMENT_END and self.pos() < self.file_size: |
177 | self.bad_bytes += 1 |
178 | else: |
179 | while uchar(self.read(1)) is not STATEMENT_END: |
180 | self.bad_bytes += 1 |
181 |
|
182 | if append == True: |
183 | self.statement_count += 1 |
184 | self.update() |
185 | if self.current_event is not None: |
186 | if self.seekable: |
187 | self.current_event.pushed.append(this_position) |
188 | else: |
189 | self.current_event.pushed.append((timestamp, verbosity, tag_size, tag, message_size, message)) |
190 | else: |
191 | if self.seekable: |
192 | self.top_level.append(this_position) |
193 | else: |
194 | self.top_level.append((timestamp, verbosity, tag_size, tag, message_size, message)) |
195 | except: |
196 | return -1 |
197 |
|
198 | elif in_byte == EVENT_START: # the byte indicates an event's start, create an event |
199 | new_event = EventProto() |
200 | if self.current_event is not None: # we're already inside an event, set the new event's parent to match |
201 | new_event.parent = self.current_event |
202 | self.current_event = new_event |
203 |
|
204 |
|
205 | elif in_byte == EVENT_END: # end of event |
206 | if self.current_event is not None: |
207 | if len(self.current_event.pushed) > 1: |
208 | self.event_count += 1 |
209 | self.update() |
210 | if self.current_event.parent is not None: |
211 | self.current_event.parent.pushed.append(self.current_event) |
212 | self.current_event = self.current_event.parent |
213 | else: |
214 | self.top_level.append(self.current_event) |
215 | self.current_event = None |
216 | else: # event is empty |
217 | if self.current_event.parent is not None: |
218 | self.current_event = self.current_event.parent |
219 | else: |
220 | self.current_event = None |
221 | else: |
222 | self.bad_bytes += 1 # event doesn't exist, bad byte |
223 | return -1 |
224 |
|
225 | else: # unknown byte |
226 | self.bad_bytes += 1 |
227 | return -1 |
228 |
|
229 | def scan(self): # scan for events and statements from self.position to the end of file |
230 | if self.position == 0 and self.seekable: # if it's the start of the file, grab version and timestamp |
231 | self.version = uchar(self.read(1)) |
232 | self.timestamp = ulonglong(self.read(8)) |
233 |
|
234 | current_event = None |
235 |
|
236 | if self.pos() < self.file_size: # if the seeker is before EOF |
237 | while self.pos() < self.file_size: # while the seeker is before EOF |
238 | in_byte = uchar(self.read(1)) # read 1 byte |
239 | self.parse_block(in_byte) # parse block based on byte read |
240 |
|
241 |
|