11from __future__ import annotations
22
3+ import copy
34import io
45import logging
5- import copy
66import os
77import re
88import sys
9+ import time
10+ from collections import defaultdict
911from dataclasses import dataclass , field
1012from datetime import datetime
1113from enum import Enum
12- from typing import ClassVar , cast , Dict , Optional
13-
14- import time
15- from collections import defaultdict
1614from threading import Lock
15+ from typing import ClassVar , cast
1716
1817from erskafka .ERSKafkaLogHandler import ERSKafkaLogHandler
1918from rich .console import Console , ConsoleRenderable
3534from daqpytools .logging .levels import level_to_ers_var , logging_log_level_to_str
3635from daqpytools .logging .utils import get_width
3736
37+
3838class FormattedRichHandler (RichHandler ):
3939 """RichHandler that formats log messages with time, aligned columns, and styles."""
4040
@@ -165,9 +165,8 @@ class HandlerType(Enum):
165165 def from_string (cls , s : str ) -> HandlerType | None :
166166 """Converts from a case-independent string to HandlerType."""
167167 try :
168- h = HandlerType (s .lower ())
169- return h
170- except :
168+ return HandlerType (s .lower ())
169+ except ValueError :
171170 msg = f"[red]{ s } [/red] is not a known handler type"
172171 log .warning (msg )
173172 return None
@@ -238,7 +237,11 @@ def _convert_str_to_handlertype(handler_str: str) -> tuple[HandlerType,
238237 protobuf configuration
239238 """
240239 if "erstrace" in handler_str :
241- log .debug ("ERSTrace is a C++ implementation, does not have an equivalent in Python" )
240+ msg = (
241+ "ERSTrace is a C++ implementation, "
242+ "does not have an equivalent in Python"
243+ )
244+ log .debug (msg )
242245 return None , None
243246
244247 if HandlerType .Protobufstream .value not in handler_str :
@@ -281,10 +284,11 @@ def get_base() -> set[HandlerType]:
281284class IssueRecord :
282285 """Tracks throttling state for a unique issue (identified by file: line)."""
283286
284- def __init__ (self ):
287+ def __init__ (self ) -> None :
288+ """C'tor."""
285289 self .reset ()
286290
287- def reset (self ):
291+ def reset (self ) -> None :
288292 """Reset all counters and timestamps."""
289293 self .last_occurrence : float = 0.0
290294 self .last_report : float = 0.0
@@ -295,11 +299,16 @@ def reset(self):
295299
296300
297301class BaseHandlerFilter (logging .Filter ):
298- def __init__ (self ):
302+ """Base filter that hold the logic on choosing if a handler should emit
303+ based on what HandlersTypes are supplied to it.
304+ """
305+ def __init__ (self ) -> None :
306+ """C'tor."""
299307 super ().__init__ ()
300308
301- def get_allowed (self , record ) -> list | None :
302- # TODO/future: kafka protobufs should validate url/port match before transmitting
309+ def get_allowed (self , record : logging .LogRecord ) -> list | None :
310+ """Parses the record to obtain the set of Handlers that allows transmission."""
311+ # TODO/future: kafkaprotobufs should validate url/port match before transmitting
303312
304313 # Handle the ERS case, requires more processing
305314 if getattr (record , "stream" , None ) == StreamType .ERS :
@@ -327,7 +336,7 @@ class HandleIDFilter(BaseHandlerFilter):
327336 if the current handler (defined by the handler_id) is within the set of
328337 allowed handlers.
329338 """
330- def __init__ (self , handler_id : Union [ HandlerType , List [HandlerType ] ]) -> None :
339+ def __init__ (self , handler_id : HandlerType | list [HandlerType ]) -> None :
331340 """Initialises HandleIDFilter with the handler_id, to identify what
332341 kind of handler this filter is.
333342 """
@@ -346,11 +355,11 @@ def filter(self, record: logging.LogRecord) -> bool:
346355 return bool (self .handler_ids & set (allowed ))
347356
348357class ThrottleFilter (BaseHandlerFilter ):
349- """
350- Advanced logging filter with escalating throttle thresholds.
358+ """Advanced logging filter with escalating throttle thresholds.
351359
352360 Args:
353- initial_threshold: Number of initial occurrences to let through immediately (default: 30)
361+ initial_threshold: Number of initial occurrences
362+ to let through immediately (default: 30)
354363 time_limit: Time window in seconds for resetting state (default: 30)
355364 name: Optional filter name
356365
@@ -368,16 +377,16 @@ class ThrottleFilter(BaseHandlerFilter):
368377 ... logger.error("Repeated error message")
369378 """
370379
371- def __init__ (self , initial_threshold : int = 30 , time_limit : int = 30 ):
380+ def __init__ (self , initial_threshold : int = 30 , time_limit : int = 30 ) -> None :
381+ """C'tor."""
372382 super ().__init__ ()
373383 self .initial_threshold = initial_threshold
374384 self .time_limit = time_limit
375- self .issue_map : Dict [str , IssueRecord ] = defaultdict (IssueRecord )
385+ self .issue_map : dict [str , IssueRecord ] = defaultdict (IssueRecord )
376386 self .mutex = Lock () # Ensures thread safety
377387
378388 def filter (self , record : logging .LogRecord ) -> bool :
379- """
380- Determine if a log record should be emitted.
389+ """Determine if a log record should be emitted.
381390
382391 Args:
383392 record: The log record to filter
@@ -401,8 +410,7 @@ def filter(self, record: logging.LogRecord) -> bool:
401410 return self ._throttle (issue_record , record )
402411
403412 def _throttle (self , rec : IssueRecord , record : logging .LogRecord ) -> bool :
404- """
405- Apply throttling logic to determine if record should be emitted.
413+ """Apply throttling logic to determine if record should be emitted.
406414
407415 Args:
408416 rec: The issue record tracking state for this unique issue
@@ -432,30 +440,28 @@ def _throttle(self, rec: IssueRecord, record: logging.LogRecord) -> bool:
432440 return not reported
433441
434442 # Step 3: Check if we hit the escalating threshold
435- elif rec .suppressed_counter >= rec .threshold :
443+ if rec .suppressed_counter >= rec .threshold :
436444 rec .threshold = rec .threshold * 10 # Escalate: 10 -> 100 -> 1000 ...
437445 rec .last_occurrence = current_time
438446 rec . last_occurrence_formatted = self ._format_timestamp (current_time )
439447 self ._report_suppression (rec , record )
440448 return False # Don't emit the original record
441449
442450 # Step 4: Check if enough time passed since last report
443- elif current_time - rec .last_report > self .time_limit :
451+ if current_time - rec .last_report > self .time_limit :
444452 rec .last_occurrence = current_time
445453 rec . last_occurrence_formatted = self ._format_timestamp (current_time )
446454 self ._report_suppression (rec , record )
447455 return False # Don't emit the original record
448456
449457 # Step 5: Suppress silently
450- else :
451- rec .suppressed_counter += 1
452- rec .last_occurrence = current_time
453- rec . last_occurrence_formatted = self ._format_timestamp (current_time )
454- return False
458+ rec .suppressed_counter += 1
459+ rec .last_occurrence = current_time
460+ rec . last_occurrence_formatted = self ._format_timestamp (current_time )
461+ return False
455462
456- def _report_suppression (self , rec : IssueRecord , record : logging .LogRecord ):
457- """
458- Create and emit a suppression notice.
463+ def _report_suppression (self , rec : IssueRecord , record : logging .LogRecord ) -> None :
464+ """Create and emit a suppression notice.
459465
460466 Args:
461467 rec: The issue record with suppression count
@@ -486,8 +492,7 @@ def _report_suppression(self, rec: IssueRecord, record: logging.LogRecord):
486492
487493 @staticmethod
488494 def _format_timestamp (timestamp : float ) -> str :
489- """
490- Format timestamp in ISO format with microseconds.
495+ """Format timestamp in ISO format with microseconds.
491496
492497 Args:
493498 timestamp: Unix timestamp
0 commit comments