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 | total_events = 0 |
63 | total_statements = 0 |
64 |
|
65 | seekable = True |
66 | file_size = 0 |
67 | position = 0 |
68 |
|
69 | version = 0 |
70 | timestamp = 0 |
71 |
|
72 | bad_bytes = 0 |
73 |
|
74 | filter_time_start = -1 |
75 | filter_time_end = -1 |
76 | filter_verbosity = -1 |
77 | filter_tag = -1 |
78 |
|
79 | update_callbacks = [] |
80 |
|
81 | def __init__(self, fd, seekable=True): |
82 | self.fd = fd |
83 |
|
84 | self.top_level = [] |
85 |
|
86 | self.current_event = None |
87 |
|
88 | self.event_count = 0 |
89 | self.statement_count = 0 |
90 |
|
91 | self.total_events = 0 |
92 | self.total_statements = 0 |
93 |
|
94 | self.seekable = seekable |
95 | self.file_size = 0 |
96 | self.position = 0 |
97 |
|
98 | self.version = 0 |
99 | self.timestamp = 0 |
100 |
|
101 | self.bad_bytes = 0 |
102 |
|
103 | self.filters = False |
104 |
|
105 | self.filter_time_start = -1 |
106 | self.filter_time_end = -1 |
107 | self.filter_verbosity = -1 |
108 | self.filter_tag = -1 |
109 |
|
110 | self.update_callbacks = [] |
111 |
|
112 | def update(self): |
113 | for callback in self.update_callbacks: |
114 | callback() |
115 |
|
116 | def onupdate(self, callback): |
117 | self.update_callbacks.append(callback) |
118 |
|
119 | def size(self): |
120 | self.fd.seek(0, os.SEEK_END) # go to end of file and get position |
121 | newsize = self.fd.tell() |
122 | self.fd.seek(self.position) # return to previous position |
123 |
|
124 | is_diff = self.file_size is not newsize |
125 | self.file_size = newsize |
126 | return is_diff |
127 |
|
128 | def pos(self): |
129 | p = self.fd.tell() |
130 | self.position = p |
131 | return p |
132 |
|
133 | def seek(self, position): |
134 | self.position = position |
135 | self.fd.seek(self.position) |
136 |
|
137 | def read(self, byte_count): |
138 | return self.fd.read(byte_count) |
139 |
|
140 | def fetch_item(self, position): |
141 | previouspos = self.pos() |
142 | self.seek(position) |
143 | timestamp = ulonglong(self.read(8)) |
144 | verbosity = uchar(self.read(1)) |
145 | tag_size = uchar(self.read(1)) |
146 | tag = self.read(tag_size).decode("utf-8") |
147 | message_size = ushort(self.read(2)) |
148 | message = self.read(message_size).decode("utf-8") |
149 | self.seek(previouspos) |
150 | return (timestamp, verbosity, tag, message) |
151 |
|
152 | def parse_block(self, in_byte): |
153 | seekable = self.seekable |
154 | if in_byte == STATEMENT_START: # the byte indicates a statement's start, begin interpreting |
155 | if seekable: |
156 | this_position = self.pos() # identify and save the seeker position of this statement |
157 |
|
158 | try: |
159 | block = self.read(10) |
160 | timestamp = ulonglong(block[:8]) |
161 | verbosity = block[8] |
162 | tag = self.read(block[9]) |
163 | |
164 | append = True |
165 |
|
166 | if self.filters: |
167 | if self.filter_time_start is not -1 and append: |
168 | append = timestamp > self.filter_time_start |
169 |
|
170 | if self.filter_time_end is not -1 and append: |
171 | append = timestamp < self.filter_time_end |
172 |
|
173 | if self.filter_verbosity is not -1 and append: |
174 | append = verbosity in self.filter_verbosity |
175 |
|
176 | if self.filter_tag is not -1 and append: |
177 | tag = tag.decode("utf-8") |
178 | append = tag == self.filter_tag |
179 |
|
180 | message_size = ushort(self.read(2)) |
181 | if seekable: |
182 | self.read(message_size) |
183 | while self.read(1)[0] is not STATEMENT_END and self.pos() < self.file_size: |
184 | self.bad_bytes += 1 |
185 | else: |
186 | message = self.read(message_size).decode("utf-8") |
187 | while self.read(1)[0] is not STATEMENT_END: |
188 | self.bad_bytes += 1 |
189 |
|
190 | if append == True: |
191 | self.statement_count += 1 |
192 | if self.current_event is not None: |
193 | if seekable: |
194 | self.current_event.pushed.append(this_position) |
195 | else: |
196 | self.current_event.pushed.append((timestamp, verbosity, tag, message)) |
197 | else: |
198 | if seekable: |
199 | self.top_level.append(this_position) |
200 | else: |
201 | self.top_level.append((timestamp, verbosity, tag, message)) |
202 | self.total_statements += 1 |
203 | except: |
204 | return -1 |
205 |
|
206 | elif in_byte == EVENT_START: # the byte indicates an event's start, create an event |
207 | new_event = EventProto() |
208 | if self.current_event is not None: # we're already inside an event, set the new event's parent to match |
209 | new_event.parent = self.current_event |
210 | self.current_event = new_event |
211 |
|
212 |
|
213 | elif in_byte == EVENT_END: # end of event |
214 | if self.current_event is not None: |
215 | if len(self.current_event.pushed) > 1: |
216 | self.event_count += 1 |
217 | if self.current_event.parent is not None: |
218 | self.current_event.parent.pushed.append(self.current_event) |
219 | self.current_event = self.current_event.parent |
220 | else: |
221 | self.top_level.append(self.current_event) |
222 | self.current_event = None |
223 | else: # event is empty |
224 | if self.current_event.parent is not None: |
225 | self.current_event = self.current_event.parent |
226 | else: |
227 | self.current_event = None |
228 | self.total_events += 1 |
229 | else: |
230 | self.bad_bytes += 1 # event doesn't exist, bad byte |
231 | return -1 |
232 |
|
233 | else: # unknown byte |
234 | self.bad_bytes += 1 |
235 | return -1 |
236 |
|
237 | def scan(self): # scan for events and statements from self.position to the end of file |
238 | if not self.seekable: raise Exception("attempting to scan non-seekable stream") |
239 | if self.pos() == 0: # if it's the start of the file, grab version and timestamp |
240 | self.version = self.read(1)[0] |
241 | self.timestamp = ulonglong(self.read(8)) |
242 |
|
243 | while self.pos() < self.file_size: # while the seeker is before EOF |
244 | in_byte = self.read(1)[0] # read 1 byte |
245 | self.parse_block(in_byte) # parse block based on byte read |
246 | self.update() # trigger callbacks |
247 |
|
248 |
|