source: src/mlx/logger.py@ 1036:bc8c41f33fba

python3
Last change on this file since 1036:bc8c41f33fba was 921:11f9fb4fb178, checked in by István Váradi <ivaradi@…>, 6 years ago

Fixed ordering for classes that used cmp (re #347).

File size: 14.5 KB
Line 
1
2from .fs import sendMessage
3from . import const
4from . import util
5
6import sys
7import time
8import bisect
9from functools import total_ordering
10
11#--------------------------------------------------------------------------------------
12
13## @package mlx.logger
14#
15# The module for the logger.
16#
17# While the program itself is "logger", it contains an internal logger, which
18# maintains the textual log containing information on the various events and is
19# the reason why the program is called "logger".
20#
21# The log is made up of lines containing an optional timestamp and the text of
22# the message. A line can be updated after having been put into the log by
23# referring to its index.
24#
25# The logger object also maintains a separate set of faults and ensures that
26# one fault type has only one score, even if that fault has been reported
27# multiple times.
28
29#--------------------------------------------------------------------------------------
30
31class Logger(object):
32 """The class with the interface to log the various events.
33
34 It contains a list of entries ordered by their timestamps and their ever
35 increasing IDs."""
36
37 @total_ordering
38 class Entry(object):
39 """An entry in the log."""
40
41 # The ID of the next entry to be created
42 _nextID = 1
43
44 def __init__(self, timestamp, text, showTimestamp = True,
45 faultID = None, faultScore = 0, id = None):
46 """Construct the entry."""
47 if id is None:
48 self._id = self._nextID
49 Logger.Entry._nextID += 1
50 else:
51 self._id = id
52
53 self._timestamp = timestamp
54 self._text = text
55 self._showTimestamp = showTimestamp
56
57 self._faultID = faultID
58 self._faultScore = faultScore
59
60 @property
61 def id(self):
62 """Get the ID of the entry."""
63 return self._id
64
65 @property
66 def timestamp(self):
67 """Get the timestamp of this entry."""
68 return self._timestamp
69
70 @property
71 def timestampString(self):
72 """Get the timestamp string of this entry.
73
74 It returns None, if the timestamp of the entry is not visible."""
75 return util.getTimestampString(self._timestamp) \
76 if self._showTimestamp else None
77
78 @property
79 def text(self):
80 """Get the text of this entry."""
81 return self._text
82
83 @property
84 def isFault(self):
85 """Determine if this is a log entry about a fault."""
86 return self._faultID is not None
87
88 @property
89 def faultID(self):
90 """Get the fault ID of the entry.
91
92 It may be None, if the entry is not a fault entry."""
93 return self._faultID
94
95 @property
96 def faultScore(self):
97 """Get the fault score of the entry, if it is a fault."""
98 return self._faultScore
99
100 def copy(self, timestamp = None, clearTimestamp = False, text = None,
101 faultID = None, faultScore = None, clearFault = False):
102 """Create a copy of this entry with the given values changed."""
103 return Logger.Entry(None if clearTimestamp
104 else self._timestamp if timestamp is None
105 else timestamp,
106
107 self._text if text is None else text,
108
109 showTimestamp = self._showTimestamp,
110
111 faultID =
112 None if clearFault
113 else self._faultID if faultID is None
114 else faultID,
115
116 faultScore =
117 None if clearFault
118 else self._faultScore if faultScore is None
119 else faultScore,
120
121 id = self._id)
122
123 def __eq__(self, other):
124 """Equality comparison"""
125 return self._timestamp == other.timestamp and \
126 self._id == other._id
127
128 def __ne__(self, other):
129 """Non-equality comparison"""
130 return self._timestamp != other.timestamp or \
131 self._id != other._id
132
133 def __lt__(self, other):
134 """Less-than comparison"""
135 return self._timestamp < other.timestamp or \
136 (self._timestamp == other.timestamp and
137 self._id < other._id)
138
139 class Fault(object):
140 """Information about a fault.
141
142 It contains the list of log entries that belong to this fault. The list
143 is ordered so that the first element contains the entry with the
144 highest score, so that it should be easy to find the actual score."""
145 def __init__(self, entry):
146 """Construct the fault info with the given log entry as its only
147 one."""
148 self._entries = [entry]
149
150 @property
151 def score(self):
152 """Get the score of this fault, i.e. the score of the entry with
153 the highest score."""
154 return self._entries[0].faultScore if self._entries else 0
155
156 def addEntry(self, entry):
157 """Add an entry to this fault.
158
159 The entries will be sorted."""
160 entries = self._entries
161 entries.append(entry)
162 entries.sort(key = lambda entry: entry.faultScore, reverse = True)
163
164 def removeEntry(self, entry):
165 """Remove the given entry.
166
167 Returns True if at least one entry remains, False otherwise."""
168 entries = self._entries
169 for index in range(0, len(entries)):
170 if entry is entries[index]:
171 del entries[index]
172 break
173
174 return len(entries)>0
175
176 def getLatestEntry(self):
177 """Get the entry with the highest score."""
178 return self._entries[0]
179
180 # FIXME: shall we use const.stage2string() instead?
181 _stages = { const.STAGE_BOARDING : "Boarding",
182 const.STAGE_PUSHANDTAXI : "Pushback and Taxi",
183 const.STAGE_TAKEOFF : "Takeoff",
184 const.STAGE_RTO : "RTO",
185 const.STAGE_CLIMB : "Climb",
186 const.STAGE_CRUISE : "Cruise",
187 const.STAGE_DESCENT : "Descent",
188 const.STAGE_LANDING : "Landing",
189 const.STAGE_TAXIAFTERLAND : "Taxi",
190 const.STAGE_PARKING : "Parking",
191 const.STAGE_GOAROUND : "Go-Around",
192 const.STAGE_END : "End" }
193
194 NO_GO_SCORE = 10000
195
196 NO_SCORE = 9999
197
198 def __init__(self, output):
199 """Construct the logger."""
200 self._entries = {}
201 self._lines = []
202
203 self._faults = {}
204
205 self._output = output
206
207 @property
208 def lines(self):
209 """Get the lines of the log."""
210 return [(entry.timestampString, entry.text) for entry in self._lines]
211
212 @property
213 def faultLineIndexes(self):
214 """Get the sorted array of the indexes of those log lines that contain
215 a fault."""
216 faultLineIndexes = []
217 lines = self._lines
218 for index in range(0, len(lines)):
219 if lines[index].isFault:
220 faultLineIndexes.append(index)
221 return faultLineIndexes
222
223 def reset(self):
224 """Reset the logger.
225
226 The faults logged so far will be cleared."""
227 self._entries = {}
228 self._lines = []
229 self._faults = {}
230
231 def message(self, timestamp, msg):
232 """Put a simple textual message into the log with the given timestamp.
233
234 Returns an ID of the message so that it could be referred to later."""
235 return self._addEntry(Logger.Entry(timestamp, msg))
236
237 def untimedMessage(self, msg):
238 """Put an untimed message into the log."""
239 timestamp = self._lines[-1].timestamp if self._lines else 0
240 return self._addEntry(Logger.Entry(timestamp, msg,
241 showTimestamp = False))
242
243 def debug(self, msg):
244 """Log a debug message."""
245 print("[DEBUG]", msg)
246
247 def stage(self, timestamp, stage):
248 """Report a change in the flight stage."""
249 s = Logger._stages[stage] if stage in Logger._stages else "<Unknown>"
250 self.message(timestamp, "--- %s ---" % (s,))
251 if stage==const.STAGE_END:
252 self.untimedMessage("Rating: %.0f" % (self.getRating(),))
253 else:
254 messageType = \
255 const.MESSAGETYPE_INFLIGHT if stage in \
256 [const.STAGE_CLIMB, const.STAGE_CRUISE, \
257 const.STAGE_DESCENT, const.STAGE_LANDING] \
258 else const.MESSAGETYPE_INFORMATION
259 sendMessage(messageType, "Flight stage: " + s, 3)
260
261 def fault(self, faultID, timestamp, what, score,
262 updatePrevious = False, updateID = None):
263 """Report a fault.
264
265 faultID as a unique ID for the given kind of fault. If another fault of
266 this ID has been reported earlier, it will be reported again only if
267 the score is greater than last time. This ID can be, e.g. the checker
268 the report comes from.
269
270 If updatePrevious is True, and an instance of the given fault is
271 already in the log, only that instance will be updated with the new
272 timestamp and score. If there are several instances, the latest one
273 (with the highest score) will be updated. If updatePrevious is True,
274 and the new score is not greater than the latest one, the ID of the
275 latest one is returned.
276
277 If updateID is given, the log entry with the given ID will be
278 'upgraded' to be a fault with the given data.
279
280 Returns an ID of the fault, or -1 if it was not logged."""
281 fault = self._faults[faultID] if faultID in self._faults else None
282
283 text = "%s (NO GO)" % (what) if score==Logger.NO_GO_SCORE \
284 else "%s" % (what,) if score==Logger.NO_SCORE \
285 else "%s (%.1f)" % (what, score)
286
287 if score==Logger.NO_SCORE:
288 score = 0
289
290 if fault is not None and score<=fault.score:
291 return fault.getLatestEntry().id if updatePrevious else -1
292
293 if updatePrevious and fault is not None:
294 latestEntry = fault.getLatestEntry()
295 id = latestEntry.id
296 newEntry = latestEntry.copy(timestamp = timestamp,
297 text = text,
298 faultScore = score)
299 self._updateEntry(id, newEntry)
300 if latestEntry.isFault:
301 self._output.updateFault(id, newEntry.timestampString, text)
302 else:
303 self._output.addFault(id, newEntry.timestampString, text)
304 elif updateID is not None:
305 id = updateID
306 oldEntry = self._entries[id]
307 newEntry = oldEntry.copy(timestamp = timestamp,
308 text = text, faultID = faultID,
309 faultScore = score)
310 self._updateEntry(id, newEntry)
311 if oldEntry.isFault:
312 self._output.updateFault(id, newEntry.timestampString, text)
313 else:
314 self._output.addFault(id, newEntry.timestampString, text)
315 else:
316 entry = Logger.Entry(timestamp, text, faultID = faultID,
317 faultScore = score)
318 id = self._addEntry(entry)
319 self._output.addFault(id, entry.timestampString, text)
320
321 if updateID is None:
322 (messageType, duration) = (const.MESSAGETYPE_NOGO, 10) \
323 if score==Logger.NO_GO_SCORE \
324 else (const.MESSAGETYPE_FAULT, 5)
325 sendMessage(messageType, text, duration)
326
327 return id
328
329 def noGo(self, faultID, timestamp, what):
330 """Report a No-Go fault."""
331 return self.fault(faultID, timestamp, what, Logger.NO_GO_SCORE)
332
333 def getRating(self):
334 """Get the rating of the flight so far."""
335 totalScore = 100
336 for fault in self._faults.values():
337 score = fault.score
338 if score==Logger.NO_GO_SCORE:
339 return -score
340 else:
341 totalScore -= score
342 return totalScore
343
344 def updateLine(self, id, line):
345 """Update the line with the given ID with the given string.
346
347 Note, that it does not change the status of the line as a fault!"""
348 self._updateEntry(id, self._entries[id].copy(text = line))
349
350 def clearFault(self, id, text):
351 """Update the line with the given ID to contain the given string,
352 and clear its fault state."""
353 newEntry = self._entries[id].copy(text = text, clearFault = True)
354 self._updateEntry(id, newEntry)
355 self._output.clearFault(id)
356
357 def _addEntry(self, entry):
358 """Add the given entry to the log.
359
360 @return the ID of the new entry."""
361 assert entry.id not in self._entries
362
363 self._entries[entry.id] = entry
364
365 if not self._lines or entry>self._lines[-1]:
366 index = len(self._lines)
367 self._lines.append(entry)
368 else:
369 index = bisect.bisect_left(self._lines, entry)
370 self._lines.insert(index, entry)
371
372 if entry.isFault:
373 self._addFault(entry)
374
375 self._output.insertFlightLogLine(index, entry.timestampString,
376 entry.text, entry.isFault)
377
378 return entry.id
379
380 def _updateEntry(self, id, newEntry):
381 """Update the entry with the given ID from the given new entry."""
382 self._removeEntry(id)
383 self._addEntry(newEntry)
384
385 def _removeEntry(self, id):
386 """Remove the entry with the given ID."""
387 assert id in self._entries
388
389 entry = self._entries[id]
390 del self._entries[id]
391
392 for index in range(len(self._lines)-1, -1, -1):
393 if self._lines[index] is entry:
394 break
395 del self._lines[index]
396
397 if entry.isFault:
398 faultID = entry.faultID
399 fault = self._faults[faultID]
400 if not fault.removeEntry(entry):
401 del self._faults[faultID]
402
403 self._output.removeFlightLogLine(index)
404
405 def _addFault(self, entry):
406 """Add the given fault entry to the fault with the given ID."""
407 faultID = entry.faultID
408 if faultID in self._faults:
409 self._faults[faultID].addEntry(entry)
410 else:
411 self._faults[faultID] = Logger.Fault(entry)
412
413#--------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.