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.filters = False |
98 |
|
99 | self.filter_time_start = -1 |
100 | self.filter_time_end = -1 |
101 | self.filter_verbosity = -1 |
102 | self.filter_tag = -1 |
103 |
|
104 | self.update_callbacks = [] |
105 |
|
106 | def update(self): |
107 | for callback in self.update_callbacks: |
108 | callback() |
109 |
|
110 | def onupdate(self, callback): |
111 | self.update_callbacks.append(callback) |
112 |
|
113 | def size(self): |
114 | self.fd.seek(0, os.SEEK_END) # go to end of file and get position |
115 | newsize = self.fd.tell() |
116 | self.fd.seek(self.position) # return to previous position |
117 |
|
118 | is_diff = self.file_size is not newsize |
119 | self.file_size = newsize |
120 | return is_diff |
121 |
|
122 | def pos(self): |
123 | p = self.fd.tell() |
124 | self.position = p |
125 | return p |
126 |
|
127 | def seek(self, position): |
128 | self.position = position |
129 | self.fd.seek(self.position) |
130 |
|
131 | def read(self, byte_count): |
132 | return self.fd.read(byte_count) |
133 |
|
134 | def fetch_item(self, position): |
135 | previouspos = self.pos() |
136 | self.seek(position) |
137 | timestamp = ulonglong(self.read(8)) |
138 | verbosity = uchar(self.read(1)) |
139 | tag_size = uchar(self.read(1)) |
140 | tag = self.read(tag_size).decode("utf-8") |
141 | message_size = ushort(self.read(2)) |
142 | message = self.read(message_size).decode("utf-8") |
143 | self.seek(previouspos) |
144 | return (timestamp, verbosity, tag, message) |
145 |
|
146 | def parse_block(self, in_byte): |
147 | seekable = self.seekable |
148 | if in_byte == STATEMENT_START: # the byte indicates a statement's start, begin interpreting |
149 | if seekable: |
150 | this_position = self.pos() # identify and save the seeker position of this statement |
151 |
|
152 | try: |
153 | block = self.read(10) |
154 | timestamp = ulonglong(block[:8]) |
155 | verbosity = block[8] |
156 | tag = self.read(block[9]) |
157 | |
158 | append = True |
159 |
|
160 | if self.filters: |
161 | if self.filter_time_start is not -1 and append: |
162 | append = timestamp > self.filter_time_start |
163 |
|
164 | if self.filter_time_end is not -1 and append: |
165 | append = timestamp < self.filter_time_end |
166 |
|
167 | if self.filter_verbosity is not -1 and append: |
168 | append = verbosity in self.filter_verbosity |
169 |
|
170 | if self.filter_tag is not -1 and append: |
171 | tag = tag.decode("utf-8") |
172 | append = tag == self.filter_tag |
173 |
|
174 | message_size = ushort(self.read(2)) |
175 | if seekable: |
176 | self.read(message_size) |
177 | while self.read(1)[0] is not STATEMENT_END and self.pos() < self.file_size: |
178 | self.bad_bytes += 1 |
179 | else: |
180 | message = self.read(message_size).decode("utf-8") |
181 | while self.read(1)[0] is not STATEMENT_END: |
182 | self.bad_bytes += 1 |
183 |
|
184 | if append == True: |
185 | self.statement_count += 1 |
186 | if self.current_event is not None: |
187 | if seekable: |
188 | self.current_event.pushed.append(this_position) |
189 | else: |
190 | self.current_event.pushed.append((timestamp, verbosity, tag, message)) |
191 | else: |
192 | if seekable: |
193 | self.top_level.append(this_position) |
194 | else: |
195 | self.top_level.append((timestamp, verbosity, tag, message)) |
196 | except: |
197 | return -1 |
198 |
|
199 | elif in_byte == EVENT_START: # the byte indicates an event's start, create an event |
200 | new_event = EventProto() |
201 | if self.current_event is not None: # we're already inside an event, set the new event's parent to match |
202 | new_event.parent = self.current_event |
203 | self.current_event = new_event |
204 |
|
205 |
|
206 | elif in_byte == EVENT_END: # end of event |
207 | if self.current_event is not None: |
208 | if len(self.current_event.pushed) > 1: |
209 | self.event_count += 1 |
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.seekable and self.position == 0: # if it's the start of the file, grab version and timestamp |
231 | self.version = self.read(1)[0] |
232 | self.timestamp = ulonglong(self.read(8)) |
233 |
|
234 | if self.pos() < self.file_size: # if the seeker is before EOF |
235 | while self.pos() < self.file_size: # while the seeker is before EOF |
236 | in_byte = self.read(1)[0] # read 1 byte |
237 | self.parse_block(in_byte) # parse block based on byte read |
238 | self.update() |
239 |
|
240 |
|