A thread-safe logger that duplicates stdout to both console and file, timestamps all output lines, and supports real-time subscriber streaming.
| 6 | from queue import Queue, Empty |
| 7 | |
| 8 | class ServerTee: |
| 9 | """ |
| 10 | A thread-safe logger that duplicates stdout to both console and file, |
| 11 | timestamps all output lines, and supports real-time subscriber streaming. |
| 12 | """ |
| 13 | |
| 14 | def __init__(self, filename, mode='a'): |
| 15 | self.file = open(filename, mode, buffering=1) # line-buffered |
| 16 | self.stdout = sys.stdout |
| 17 | self.lock = Lock() |
| 18 | self.subscribers = [] |
| 19 | self.buffer = "" # buffer for partial lines |
| 20 | sys.stdout = self # redirect global stdout |
| 21 | |
| 22 | def write(self, message): |
| 23 | with self.lock: |
| 24 | self.buffer += message |
| 25 | while '\n' in self.buffer: |
| 26 | line, self.buffer = self.buffer.split('\n', 1) |
| 27 | # Skip completely empty lines |
| 28 | if not line.strip(): |
| 29 | continue |
| 30 | timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| 31 | message_with_timestamp = f"{timestamp} - {line}\n" |
| 32 | |
| 33 | # Write to original stdout |
| 34 | self.stdout.write(message_with_timestamp) |
| 35 | self.stdout.flush() |
| 36 | |
| 37 | # Write to log file |
| 38 | self.file.write(message_with_timestamp) |
| 39 | self.file.flush() |
| 40 | |
| 41 | # Notify subscribers |
| 42 | self.notify_subscribers(message_with_timestamp) |
| 43 | |
| 44 | def flush(self): |
| 45 | with self.lock: |
| 46 | if self.buffer: |
| 47 | line = self.buffer.strip() |
| 48 | if line: |
| 49 | timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| 50 | message_with_timestamp = f"{timestamp} - {line}\n" |
| 51 | self.stdout.write(message_with_timestamp) |
| 52 | self.file.write(message_with_timestamp) |
| 53 | self.notify_subscribers(message_with_timestamp) |
| 54 | self.buffer = "" |
| 55 | self.stdout.flush() |
| 56 | self.file.flush() |
| 57 | |
| 58 | def close(self): |
| 59 | with self.lock: |
| 60 | self.flush() |
| 61 | sys.stdout = self.stdout # restore original stdout |
| 62 | self.file.close() |
| 63 | |
| 64 | # ------------------- |
| 65 | # Subscriber methods |