source: src/mlx/logger.py@ 634:b695e9445e24

Last change on this file since 634:b695e9445e24 was 634:b695e9445e24, checked in by István Váradi <ivaradi@…>, 8 years ago

A go-around is logged as a fault with no score and an explanation is required (re #266)

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