├── .gitignore ├── README.md ├── examples └── general.log ├── requirements.txt ├── setup.py ├── src ├── tests │ ├── do_follow.py │ └── test_record.py └── textualog │ ├── __init__.py │ ├── __main__.py │ ├── __version__.py │ ├── emojis.py │ ├── loader.py │ ├── log.py │ ├── renderables │ ├── __init__.py │ ├── logrecord.py │ ├── namespace_tree.py │ └── shortcuts.py │ ├── styles.py │ ├── system.py │ ├── unicodes.py │ └── widgets │ ├── __init__.py │ ├── details.py │ ├── footer.py │ ├── help.py │ ├── levels.py │ ├── namespaces.py │ ├── recordinfo.py │ └── records.py └── textualog.png /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /dist/ 3 | textualog.egg-info 4 | textual.log 5 | textualog.log 6 | test.log 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Textualog 2 | 3 | Display, filter and search logging messages in the terminal. 4 | 5 | ![screenshot](textualog.png) 6 | 7 | This project is powered by [rich](https://github.com/Textualize/rich) and [textual](https://github.com/Textualize/textual). 8 | 9 | Some of the ideas and code in this project are based on: 10 | 11 | * [kaskade](https://github.com/sauljabin/kaskade) 12 | * textual example code, e.g. code_viewer 13 | * [cutelog](https://github.com/busimus/cutelog/) 14 | 15 | ## Installation 16 | 17 | The easiest way to install the package is by running the `pip` command in the Python virtual environment of your project: 18 | ``` 19 | $ python -m pip install [--upgrade] textualog 20 | ``` 21 | 22 | ## Usage 23 | 24 | The `textualog` app should have been installed in your environment, then run the following command: 25 | ``` 26 | $ textualog --log 27 | ``` 28 | In the `examples` directory of this project, you can find an example log file to inspect and play with. 29 | 30 | The main view is divided in three panels, (1) a _Records_ panel that displays all the logging records in a colored view, (2) a _Record Info_ panel that displays more details about the selected logging message (a message can be selected by a mouse click), and (3) a _Levels_ panel that displays the standard logging levels. Logging levels can be switched on or off with a key press, d=debug, i=info, w=warning, e=error, c=critical. When you click inside the _Record Info_ panel, the main view will change in a _Record Details_ view that displays all information associated with the selected logging message. This view is mainly used when the logging message has extra multi-line information attached, and depending on the amount of information, this view is scrollable. When the selected logging message contains extra information, the _Record Info_ panel will have an asterisk in the title. Use the Escape key to return to the main view. 31 | 32 | The app can be terminated with the 'q' key or by pressing CTRL-C. If you need a little help on the keyboard shortcuts, press the '?' key to present the _Info Help_ panel on the right side of the terminal. Also here use the Escape key to hide the help panel again. 33 | 34 | Pressing the 'n' key will slide in a _Namespaces_ panel on the left side of the Terminal. **This panel is currently not functional**. The idea is to allow the user to filter the logging messages by selecting one or more namespaces. 35 | 36 | 37 | ## Log file formats 38 | 39 | The current support is for a key-value type of log file. The log line shall have a fixed format, which is what I 40 | currently use in my main other projects. The following key=value pairs shall be there in the given order: 41 | 42 | * `level=` 43 | * `ts=<'%Y-%m-%dT%H:%M:%S,%f'>` 44 | * `process=` 45 | * `process_id=` 46 | * `caller=` 47 | * `msg=` 48 | 49 | In the future other formats can be supported by implementing a plugin class. Planned formats are the JSON format, ... 50 | 51 | ## Roadmap 52 | 53 | - [x] Display message details including extra lines that contain further information like e.g. traceback info. 54 | - [ ] Implement search functionality to search for strings or regular expressions and position the screen at the first match 55 | - [ ] Start work on filtering log messages based on their namespace 56 | -------------------------------------------------------------------------------- /examples/general.log: -------------------------------------------------------------------------------- 1 | level=WARNING ts=2022-05-02T11:17:55,575790 process=log_cs process_id=24424 caller=egse.logger.log_cs:137 msg="Logger terminated." 2 | level=DEBUG ts=2022-05-02T11:18:38,035953 process=MainProcess process_id=25044 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 3 | level=DEBUG ts=2022-05-02T11:18:38,047156 process=MainProcess process_id=25044 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 4 | level=DEBUG ts=2022-05-02T11:18:38,288972 process=MainProcess process_id=25044 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 5 | level=DEBUG ts=2022-05-02T11:18:38,294179 process=MainProcess process_id=25044 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 6 | level=DEBUG ts=2022-05-02T11:18:38,499357 process=MainProcess process_id=25044 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 7 | level=DEBUG ts=2022-05-02T11:18:38,519802 process=MainProcess process_id=25044 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 8 | level=DEBUG ts=2022-05-02T11:18:38,589416 process=storage_cs process_id=25044 caller=egse.storage.storage_cs:191 msg="location = /Users/rik/data/CSL" 9 | level=DEBUG ts=2022-05-02T11:18:38,594301 process=storage_cs process_id=25044 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/services.yaml." 10 | level=DEBUG ts=2022-05-02T11:18:38,607076 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_monitoring_frequency', cmd='{delay}', device_method=None" 11 | level=DEBUG ts=2022-05-02T11:18:38,607819 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_hk_frequency', cmd='{delay}', device_method=None" 12 | level=DEBUG ts=2022-05-02T11:18:38,607925 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_logging_level', cmd='{name} {level}', device_method=None" 13 | level=DEBUG ts=2022-05-02T11:18:38,607996 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='quit_server', cmd='', device_method=None" 14 | level=DEBUG ts=2022-05-02T11:18:38,608076 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_process_status', cmd='', device_method=None" 15 | level=DEBUG ts=2022-05-02T11:18:38,608165 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_cs_module', cmd='', device_method=None" 16 | level=INFO ts=2022-05-02T11:18:38,608405 process=storage_cs process_id=25044 caller=egse.protocol:318 msg="Binding to tcp://*:6102" 17 | level=INFO ts=2022-05-02T11:18:38,608859 process=storage_cs process_id=25044 caller=egse.protocol:318 msg="Binding to tcp://*:6101" 18 | level=DEBUG ts=2022-05-02T11:18:38,609252 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='start_observation', cmd='{obsid}', device_method=" 19 | level=DEBUG ts=2022-05-02T11:18:38,609404 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='end_observation', cmd='{obsid}', device_method=" 20 | level=DEBUG ts=2022-05-02T11:18:38,609504 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='get_obsid', cmd='', device_method=" 21 | level=DEBUG ts=2022-05-02T11:18:38,609568 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='save', cmd='{packet}', device_method=" 22 | level=DEBUG ts=2022-05-02T11:18:38,609624 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='read', cmd='{item}', device_method=" 23 | level=DEBUG ts=2022-05-02T11:18:38,609736 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='register', cmd='{client_info}', device_method=" 24 | level=DEBUG ts=2022-05-02T11:18:38,609822 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='unregister', cmd='{client_info}', device_method=" 25 | level=DEBUG ts=2022-05-02T11:18:38,609897 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='get_registry_names', cmd='', device_method=" 26 | level=DEBUG ts=2022-05-02T11:18:38,609961 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='cycle_daily_files', cmd='', device_method=" 27 | level=DEBUG ts=2022-05-02T11:18:38,610035 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='get_storage_location', cmd='', device_method=" 28 | level=DEBUG ts=2022-05-02T11:18:38,610106 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='new_registration', cmd='{item}, {use_counter}', device_method=" 29 | level=DEBUG ts=2022-05-02T11:18:38,610187 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='get_filenames', cmd='{item}', device_method=" 30 | level=DEBUG ts=2022-05-02T11:18:38,610251 process=storage_cs process_id=25044 caller=egse.protocol:532 msg="Creating StorageCommand command with name='get_disk_usage', cmd='', device_method=" 31 | level=DEBUG ts=2022-05-02T11:18:38,610328 process=storage_cs process_id=25044 caller=egse.storage.storage_cs:46 msg="Binding ZeroMQ socket to tcp://*:6100" 32 | level=INFO ts=2022-05-02T11:18:38,610366 process=storage_cs process_id=25044 caller=egse.protocol:318 msg="Binding to tcp://*:6100" 33 | level=DEBUG ts=2022-05-02T11:18:38,938693 process=MainProcess process_id=25057 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 34 | level=DEBUG ts=2022-05-02T11:18:39,855356 process=MainProcess process_id=25057 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 35 | level=DEBUG ts=2022-05-02T11:18:39,866923 process=MainProcess process_id=25057 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 36 | level=DEBUG ts=2022-05-02T11:18:40,071363 process=MainProcess process_id=25057 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 37 | level=DEBUG ts=2022-05-02T11:18:40,077462 process=MainProcess process_id=25057 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 38 | level=DEBUG ts=2022-05-02T11:18:40,288326 process=MainProcess process_id=25057 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 39 | level=DEBUG ts=2022-05-02T11:18:40,294831 process=MainProcess process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 40 | level=DEBUG ts=2022-05-02T11:18:40,304415 process=MainProcess process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 41 | level=DEBUG ts=2022-05-02T11:18:40,323134 process=confman_cs process_id=25057 caller=egse.confman.confman_cs:215 msg="location = /Users/rik/git/plato-cgse-conf/data/CSL/conf" 42 | level=DEBUG ts=2022-05-02T11:18:40,325478 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/services.yaml." 43 | level=DEBUG ts=2022-05-02T11:18:40,333522 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_monitoring_frequency', cmd='{delay}', device_method=None" 44 | level=DEBUG ts=2022-05-02T11:18:40,333834 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_hk_frequency', cmd='{delay}', device_method=None" 45 | level=DEBUG ts=2022-05-02T11:18:40,333918 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_logging_level', cmd='{name} {level}', device_method=None" 46 | level=DEBUG ts=2022-05-02T11:18:40,333973 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='quit_server', cmd='', device_method=None" 47 | level=DEBUG ts=2022-05-02T11:18:40,334032 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_process_status', cmd='', device_method=None" 48 | level=DEBUG ts=2022-05-02T11:18:40,334101 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_cs_module', cmd='', device_method=None" 49 | level=INFO ts=2022-05-02T11:18:40,334315 process=confman_cs process_id=25057 caller=egse.protocol:318 msg="Binding to tcp://*:6002" 50 | level=INFO ts=2022-05-02T11:18:40,334583 process=confman_cs process_id=25057 caller=egse.protocol:318 msg="Binding to tcp://*:6001" 51 | level=DEBUG ts=2022-05-02T11:18:40,338739 process=storage_cs process_id=25044 caller=egse.storage.persistence:1957 msg="Opening file /Users/rik/data/CSL/obsid-table.txt in mode 'a'" 52 | level=INFO ts=2022-05-02T11:18:40,338928 process=storage_cs process_id=25044 caller=egse.storage:833 msg="Storage successfully registered obsid" 53 | level=INFO ts=2022-05-02T11:18:40,339174 process=confman_cs process_id=25057 caller=egse.confman:414 msg="Storage successfully registered obsid" 54 | level=INFO ts=2022-05-02T11:18:40,339434 process=confman_cs process_id=25057 caller=egse.confman:183 msg="Populating cache with Setup Info." 55 | level=WARNING ts=2022-05-02T11:18:40,339630 process=confman_cs process_id=25057 caller=egse.confman:439 msg="SetupInfo cache is not populated, loading Setup 0 from /Users/rik/git/plato-cgse-conf/data/CSL/conf" 56 | level=DEBUG ts=2022-05-02T11:18:40,341291 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00000_200909_120000.yaml." 57 | level=DEBUG ts=2022-05-02T11:18:40,341781 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00066_210902_093852.yaml." 58 | level=DEBUG ts=2022-05-02T11:18:40,342795 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 59 | level=DEBUG ts=2022-05-02T11:18:40,381812 process=MainProcess process_id=25064 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 60 | level=INFO ts=2022-05-02T11:18:40,388781 process=confman_cs process_id=25057 caller=egse.confman:445 msg="Setup loaded: /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00000_200909_120000.yaml" 61 | level=DEBUG ts=2022-05-02T11:18:40,395308 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='start_observation', cmd='{function_info}', device_method=" 62 | level=DEBUG ts=2022-05-02T11:18:40,402747 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 63 | level=DEBUG ts=2022-05-02T11:18:40,402836 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='end_observation', cmd='', device_method=" 64 | level=DEBUG ts=2022-05-02T11:18:40,409374 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00044_210611_123028.yaml." 65 | level=DEBUG ts=2022-05-02T11:18:40,409496 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='get_obsid', cmd='', device_method=" 66 | level=DEBUG ts=2022-05-02T11:18:40,409748 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='load_setup', cmd='{id}', device_method=" 67 | level=DEBUG ts=2022-05-02T11:18:40,409830 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='get_setup', cmd='*', device_method=" 68 | level=DEBUG ts=2022-05-02T11:18:40,409881 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='reload_setups', cmd='', device_method=" 69 | level=DEBUG ts=2022-05-02T11:18:40,410121 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='list_setups', cmd='**', device_method=" 70 | level=DEBUG ts=2022-05-02T11:18:40,410188 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='submit_setup', cmd='{setup} {description}', device_method=" 71 | level=DEBUG ts=2022-05-02T11:18:40,410265 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='get_devices_for_current_setup', cmd='', device_method=" 72 | level=DEBUG ts=2022-05-02T11:18:40,410328 process=confman_cs process_id=25057 caller=egse.protocol:532 msg="Creating ConfigurationManagerCommand command with name='get_setup_for_obsid', cmd='{obsid}', device_method=" 73 | level=DEBUG ts=2022-05-02T11:18:40,452239 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 74 | level=DEBUG ts=2022-05-02T11:18:40,459990 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00045_210630_130334.yaml." 75 | level=DEBUG ts=2022-05-02T11:18:40,508187 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 76 | level=DEBUG ts=2022-05-02T11:18:40,515384 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00078_210917_084123.yaml." 77 | level=DEBUG ts=2022-05-02T11:18:40,526313 process=confman_cs process_id=25057 caller=egse.confman.confman_cs:43 msg="Binding ZeroMQ socket to tcp://*:6000" 78 | level=INFO ts=2022-05-02T11:18:40,532359 process=confman_cs process_id=25057 caller=egse.protocol:318 msg="Binding to tcp://*:6000" 79 | level=DEBUG ts=2022-05-02T11:18:40,579881 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 80 | level=DEBUG ts=2022-05-02T11:18:40,588391 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00025_201013_081304.yaml." 81 | level=DEBUG ts=2022-05-02T11:18:40,601473 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 82 | level=DEBUG ts=2022-05-02T11:18:40,608073 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00018_201008_145328.yaml." 83 | level=DEBUG ts=2022-05-02T11:18:40,619750 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 84 | level=DEBUG ts=2022-05-02T11:18:40,626662 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00040_210609_091842.yaml." 85 | level=DEBUG ts=2022-05-02T11:18:40,666397 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 86 | level=DEBUG ts=2022-05-02T11:18:40,673143 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00085_210927_075650.yaml." 87 | level=DEBUG ts=2022-05-02T11:18:40,744580 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 88 | level=DEBUG ts=2022-05-02T11:18:40,754029 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00075_210913_094520.yaml." 89 | level=DEBUG ts=2022-05-02T11:18:40,821532 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 90 | level=DEBUG ts=2022-05-02T11:18:40,828522 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00076_210916_064922.yaml." 91 | level=DEBUG ts=2022-05-02T11:18:40,892028 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 92 | level=DEBUG ts=2022-05-02T11:18:40,898875 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00080_210917_105245.yaml." 93 | level=DEBUG ts=2022-05-02T11:18:40,995609 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 94 | level=DEBUG ts=2022-05-02T11:18:41,002513 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00093_211025_183920.yaml." 95 | level=DEBUG ts=2022-05-02T11:18:41,092496 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 96 | level=DEBUG ts=2022-05-02T11:18:41,101370 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00039_210609_083303.yaml." 97 | level=DEBUG ts=2022-05-02T11:18:41,144888 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 98 | level=DEBUG ts=2022-05-02T11:18:41,151928 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00017_201008_084632.yaml." 99 | level=DEBUG ts=2022-05-02T11:18:41,163294 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 100 | level=DEBUG ts=2022-05-02T11:18:41,169632 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00050_210727_123334.yaml." 101 | level=DEBUG ts=2022-05-02T11:18:41,211570 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 102 | level=DEBUG ts=2022-05-02T11:18:41,219197 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00013_200929_140341.yaml." 103 | level=DEBUG ts=2022-05-02T11:18:41,223721 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 104 | level=DEBUG ts=2022-05-02T11:18:41,231395 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00032_210514_132034.yaml." 105 | level=DEBUG ts=2022-05-02T11:18:41,281232 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 106 | level=DEBUG ts=2022-05-02T11:18:41,288997 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00020_201009_085413.yaml." 107 | level=DEBUG ts=2022-05-02T11:18:41,301137 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 108 | level=DEBUG ts=2022-05-02T11:18:41,308193 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00037_210608_123028.yaml." 109 | level=DEBUG ts=2022-05-02T11:18:41,323529 process=MainProcess process_id=25064 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 110 | level=DEBUG ts=2022-05-02T11:18:41,336902 process=MainProcess process_id=25064 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 111 | level=DEBUG ts=2022-05-02T11:18:41,351006 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 112 | level=DEBUG ts=2022-05-02T11:18:41,358538 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00028_201028_155259.yaml." 113 | level=DEBUG ts=2022-05-02T11:18:41,373885 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 114 | level=DEBUG ts=2022-05-02T11:18:41,382064 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00067_210903_073713.yaml." 115 | level=DEBUG ts=2022-05-02T11:18:41,443083 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 116 | level=DEBUG ts=2022-05-02T11:18:41,450353 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00021_201009_122234.yaml." 117 | level=DEBUG ts=2022-05-02T11:18:41,462782 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 118 | level=DEBUG ts=2022-05-02T11:18:41,469991 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00081_210922_142259.yaml." 119 | level=DEBUG ts=2022-05-02T11:18:41,549984 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 120 | level=DEBUG ts=2022-05-02T11:18:41,561988 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00087_211012_102853.yaml." 121 | level=DEBUG ts=2022-05-02T11:18:41,579466 process=MainProcess process_id=25064 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 122 | level=DEBUG ts=2022-05-02T11:18:41,586046 process=MainProcess process_id=25064 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 123 | level=DEBUG ts=2022-05-02T11:18:41,661524 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 124 | level=DEBUG ts=2022-05-02T11:18:41,670040 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00035_210608_064406.yaml." 125 | level=DEBUG ts=2022-05-02T11:18:41,709907 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 126 | level=DEBUG ts=2022-05-02T11:18:41,717764 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00082_210923_094458.yaml." 127 | level=DEBUG ts=2022-05-02T11:18:41,786930 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 128 | level=DEBUG ts=2022-05-02T11:18:41,794322 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00064_210901_145926.yaml." 129 | level=DEBUG ts=2022-05-02T11:18:41,823906 process=MainProcess process_id=25064 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 130 | level=DEBUG ts=2022-05-02T11:18:41,830731 process=MainProcess process_id=25064 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 131 | level=DEBUG ts=2022-05-02T11:18:41,840991 process=MainProcess process_id=25064 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 132 | level=DEBUG ts=2022-05-02T11:18:41,853066 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 133 | level=DEBUG ts=2022-05-02T11:18:41,853224 process=MainProcess process_id=25064 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/dpu/dpu.yaml." 134 | level=DEBUG ts=2022-05-02T11:18:41,860472 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00096_220114_090151.yaml." 135 | level=DEBUG ts=2022-05-02T11:18:41,955000 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 136 | level=DEBUG ts=2022-05-02T11:18:41,962170 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00031_210325_084258.yaml." 137 | level=DEBUG ts=2022-05-02T11:18:41,974194 process=dpu_cs process_id=25064 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/procman/procman.yaml." 138 | level=DEBUG ts=2022-05-02T11:18:41,978448 process=MainProcess process_id=25082 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 139 | level=DEBUG ts=2022-05-02T11:18:41,984772 process=procman_cs process_id=25064 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/services.yaml." 140 | level=DEBUG ts=2022-05-02T11:18:41,994469 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_monitoring_frequency', cmd='{delay}', device_method=None" 141 | level=DEBUG ts=2022-05-02T11:18:41,994802 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_hk_frequency', cmd='{delay}', device_method=None" 142 | level=DEBUG ts=2022-05-02T11:18:41,994870 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_logging_level', cmd='{name} {level}', device_method=None" 143 | level=DEBUG ts=2022-05-02T11:18:41,994927 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='quit_server', cmd='', device_method=None" 144 | level=DEBUG ts=2022-05-02T11:18:41,994988 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_process_status', cmd='', device_method=None" 145 | level=DEBUG ts=2022-05-02T11:18:41,995033 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_cs_module', cmd='', device_method=None" 146 | level=INFO ts=2022-05-02T11:18:41,995170 process=procman_cs process_id=25064 caller=egse.protocol:318 msg="Binding to tcp://*:6202" 147 | level=INFO ts=2022-05-02T11:18:41,996767 process=procman_cs process_id=25064 caller=egse.protocol:318 msg="Binding to tcp://*:6201" 148 | level=DEBUG ts=2022-05-02T11:18:41,997617 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 149 | level=DEBUG ts=2022-05-02T11:18:42,004223 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00055_210805_135047.yaml." 150 | level=DEBUG ts=2022-05-02T11:18:42,051488 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 151 | level=DEBUG ts=2022-05-02T11:18:42,059658 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00049_210705_071924.yaml." 152 | level=DEBUG ts=2022-05-02T11:18:42,101689 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 153 | level=DEBUG ts=2022-05-02T11:18:42,108391 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00052_210729_124639.yaml." 154 | level=DEBUG ts=2022-05-02T11:18:42,151034 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 155 | level=DEBUG ts=2022-05-02T11:18:42,158179 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00009_200917_083806.yaml." 156 | level=DEBUG ts=2022-05-02T11:18:42,161378 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 157 | level=DEBUG ts=2022-05-02T11:18:42,168577 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00061_210817_123710.yaml." 158 | level=DEBUG ts=2022-05-02T11:18:42,219433 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 159 | level=DEBUG ts=2022-05-02T11:18:42,226816 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00008_170620_151080.yaml." 160 | level=DEBUG ts=2022-05-02T11:18:42,235359 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 161 | level=DEBUG ts=2022-05-02T11:18:42,243852 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00065_210901_185558.yaml." 162 | level=DEBUG ts=2022-05-02T11:18:42,332660 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 163 | level=DEBUG ts=2022-05-02T11:18:42,339893 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00077_210916_090245.yaml." 164 | level=DEBUG ts=2022-05-02T11:18:42,402988 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 165 | level=DEBUG ts=2022-05-02T11:18:42,410042 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00051_210729_092421.yaml." 166 | level=DEBUG ts=2022-05-02T11:18:42,453000 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 167 | level=DEBUG ts=2022-05-02T11:18:42,460004 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00084_210925_193952.yaml." 168 | level=DEBUG ts=2022-05-02T11:18:42,466576 process=storage_cs process_id=25044 caller=egse.storage.persistence:1852 msg="Opening file /Users/rik/data/CSL/daily/20220502/20220502_CSL_CM.csv in mode 'a'" 169 | level=INFO ts=2022-05-02T11:18:42,466755 process=storage_cs process_id=25044 caller=egse.storage:833 msg="Storage successfully registered CM" 170 | level=DEBUG ts=2022-05-02T11:18:42,527343 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 171 | level=DEBUG ts=2022-05-02T11:18:42,529067 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='get_cm_proxy', cmd='', device_method=" 172 | level=DEBUG ts=2022-05-02T11:18:42,529344 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='get_devices', cmd='', device_method=" 173 | level=DEBUG ts=2022-05-02T11:18:42,529468 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='get_core', cmd='', device_method=" 174 | level=DEBUG ts=2022-05-02T11:18:42,529563 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='start_egse', cmd='', device_method=" 175 | level=DEBUG ts=2022-05-02T11:18:42,529616 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='start_cs', cmd='{process_name} {sim_mode}', device_method=" 176 | level=DEBUG ts=2022-05-02T11:18:42,529745 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='shut_down_egse', cmd='', device_method=" 177 | level=DEBUG ts=2022-05-02T11:18:42,529843 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='shut_down_cs', cmd='{process_name}', device_method=" 178 | level=DEBUG ts=2022-05-02T11:18:42,529929 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='start_fitsgen', cmd='', device_method=" 179 | level=DEBUG ts=2022-05-02T11:18:42,530020 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='stop_fitsgen', cmd='', device_method=" 180 | level=DEBUG ts=2022-05-02T11:18:42,530075 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='start_fov_hk', cmd='', device_method=" 181 | level=DEBUG ts=2022-05-02T11:18:42,530126 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='stop_fov_hk', cmd='', device_method=" 182 | level=DEBUG ts=2022-05-02T11:18:42,530216 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='start_n_fee_hk', cmd='', device_method=" 183 | level=DEBUG ts=2022-05-02T11:18:42,530316 process=procman_cs process_id=25064 caller=egse.protocol:532 msg="Creating ProcessManagerCommand command with name='stop_n_fee_hk', cmd='', device_method=" 184 | level=DEBUG ts=2022-05-02T11:18:42,530436 process=procman_cs process_id=25064 caller=egse.procman.procman_cs:56 msg="Binding ZeroMQ socket to tcp://*:6200" 185 | level=INFO ts=2022-05-02T11:18:42,530515 process=procman_cs process_id=25064 caller=egse.protocol:318 msg="Binding to tcp://*:6200" 186 | level=DEBUG ts=2022-05-02T11:18:42,535908 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00012_200929_140221.yaml." 187 | level=DEBUG ts=2022-05-02T11:18:42,540255 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 188 | level=DEBUG ts=2022-05-02T11:18:42,546510 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00090_211021_104056.yaml." 189 | level=DEBUG ts=2022-05-02T11:18:42,572085 process=storage_cs process_id=25044 caller=egse.storage.persistence:1852 msg="Opening file /Users/rik/data/CSL/daily/20220502/20220502_CSL_PM.csv in mode 'a'" 190 | level=INFO ts=2022-05-02T11:18:42,572233 process=storage_cs process_id=25044 caller=egse.storage:833 msg="Storage successfully registered PM" 191 | level=DEBUG ts=2022-05-02T11:18:42,628859 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 192 | level=DEBUG ts=2022-05-02T11:18:42,635779 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00041_210610_085309.yaml." 193 | level=DEBUG ts=2022-05-02T11:18:42,675367 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 194 | level=DEBUG ts=2022-05-02T11:18:42,681804 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00014_200929_150911.yaml." 195 | level=DEBUG ts=2022-05-02T11:18:42,686296 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 196 | level=DEBUG ts=2022-05-02T11:18:42,692573 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00063_210825_132550.yaml." 197 | level=DEBUG ts=2022-05-02T11:18:42,750965 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 198 | level=DEBUG ts=2022-05-02T11:18:42,757934 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00072_210909_100248.yaml." 199 | level=DEBUG ts=2022-05-02T11:18:42,820165 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 200 | level=DEBUG ts=2022-05-02T11:18:42,826706 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00079_210917_100241.yaml." 201 | level=DEBUG ts=2022-05-02T11:18:42,887522 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 202 | level=DEBUG ts=2022-05-02T11:18:42,890331 process=MainProcess process_id=25082 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 203 | level=DEBUG ts=2022-05-02T11:18:42,894233 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00094_211103_104402.yaml." 204 | level=DEBUG ts=2022-05-02T11:18:42,901534 process=MainProcess process_id=25082 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 205 | level=DEBUG ts=2022-05-02T11:18:42,980542 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 206 | level=DEBUG ts=2022-05-02T11:18:42,987391 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00089_211020_134011.yaml." 207 | level=DEBUG ts=2022-05-02T11:18:43,066370 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 208 | level=DEBUG ts=2022-05-02T11:18:43,073681 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00074_210910_083400.yaml." 209 | level=DEBUG ts=2022-05-02T11:18:43,115007 process=MainProcess process_id=25082 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 210 | level=DEBUG ts=2022-05-02T11:18:43,120639 process=MainProcess process_id=25082 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 211 | level=DEBUG ts=2022-05-02T11:18:43,136772 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 212 | level=DEBUG ts=2022-05-02T11:18:43,143972 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00011_200929_135616.yaml." 213 | level=DEBUG ts=2022-05-02T11:18:43,147203 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 214 | level=DEBUG ts=2022-05-02T11:18:43,153271 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00043_210610_100757.yaml." 215 | level=DEBUG ts=2022-05-02T11:18:43,192460 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 216 | level=DEBUG ts=2022-05-02T11:18:43,199004 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00010_200929_135120.yaml." 217 | level=DEBUG ts=2022-05-02T11:18:43,201919 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 218 | level=DEBUG ts=2022-05-02T11:18:43,207841 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00092_211025_144142.yaml." 219 | level=DEBUG ts=2022-05-02T11:18:43,294624 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 220 | level=DEBUG ts=2022-05-02T11:18:43,301933 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00083_210923_094458.yaml." 221 | level=DEBUG ts=2022-05-02T11:18:43,326329 process=MainProcess process_id=25082 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 222 | level=DEBUG ts=2022-05-02T11:18:43,346071 process=MainProcess process_id=25082 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 223 | level=DEBUG ts=2022-05-02T11:18:43,354221 process=MainProcess process_id=25082 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/synoptics/syn.yaml." 224 | level=DEBUG ts=2022-05-02T11:18:43,365392 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 225 | level=DEBUG ts=2022-05-02T11:18:43,372009 process=syn_cs process_id=25082 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/services.yaml." 226 | level=DEBUG ts=2022-05-02T11:18:43,372086 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00054_210802_134730.yaml." 227 | level=DEBUG ts=2022-05-02T11:18:43,380440 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_monitoring_frequency', cmd='{delay}', device_method=None" 228 | level=DEBUG ts=2022-05-02T11:18:43,380761 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_hk_frequency', cmd='{delay}', device_method=None" 229 | level=DEBUG ts=2022-05-02T11:18:43,380828 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='set_logging_level', cmd='{name} {level}', device_method=None" 230 | level=DEBUG ts=2022-05-02T11:18:43,380878 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='quit_server', cmd='', device_method=None" 231 | level=DEBUG ts=2022-05-02T11:18:43,380925 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_process_status', cmd='', device_method=None" 232 | level=DEBUG ts=2022-05-02T11:18:43,380965 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating ServiceCommand command with name='get_cs_module', cmd='', device_method=None" 233 | level=INFO ts=2022-05-02T11:18:43,381090 process=syn_cs process_id=25082 caller=egse.protocol:318 msg="Binding to tcp://*:6206" 234 | level=INFO ts=2022-05-02T11:18:43,382640 process=syn_cs process_id=25082 caller=egse.protocol:318 msg="Binding to tcp://*:6205" 235 | level=ERROR ts=2022-05-12T16:50:52,683203 process=MainProcess process_id=44977 caller=egse.export_register_map:180 msg="[Errno 45] Unable to open file (unable to lock file, errno = 45, error message = 'Operation not supported')" 236 | Traceback (most recent call last): 237 | File "create_hdf5_report.py", line 174, in cli 238 | row = extract_register_values(Path(filename)) 239 | File "create_hdf5_report.py", line 62, in extract_register_values 240 | with h5.get_file(filename, mode='r') as hdf5_file: 241 | File "/Users/rik/git/plato-common-egse/src/egse/h5.py", line 137, in get_file 242 | return h5py.File(filename, mode) 243 | File "/Users/rik/git/plato-common-egse/venv38/lib/python3.8/site-packages/h5py/_hl/files.py", line 507, in __init__ 244 | fid = make_fid(name, mode, userblock_size, fapl, fcpl, swmr=swmr) 245 | File "/Users/rik/git/plato-common-egse/venv38/lib/python3.8/site-packages/h5py/_hl/files.py", line 220, in make_fid 246 | fid = h5f.open(name, flags, fapl=fapl) 247 | File "h5py/_objects.pyx", line 54, in h5py._objects.with_phil.wrapper 248 | File "h5py/_objects.pyx", line 55, in h5py._objects.with_phil.wrapper 249 | File "h5py/h5f.pyx", line 106, in h5py.h5f.open 250 | OSError: [Errno 45] Unable to open file (unable to lock file, errno = 45, error message = 'Operation not supported') 251 | 252 | level=ERROR ts=2022-05-12T16:50:52,688099 process=MainProcess process_id=44977 caller=egse.export_register_map:180 msg="[Errno 45] Unable to open file (unable to lock file, errno = 45, error message = 'Operation not supported')" 253 | Traceback (most recent call last): 254 | File "create_hdf5_report.py", line 174, in cli 255 | row = extract_register_values(Path(filename)) 256 | File "create_hdf5_report.py", line 62, in extract_register_values 257 | with h5.get_file(filename, mode='r') as hdf5_file: 258 | File "/Users/rik/git/plato-common-egse/src/egse/h5.py", line 137, in get_file 259 | return h5py.File(filename, mode) 260 | File "/Users/rik/git/plato-common-egse/venv38/lib/python3.8/site-packages/h5py/_hl/files.py", line 507, in __init__ 261 | fid = make_fid(name, mode, userblock_size, fapl, fcpl, swmr=swmr) 262 | File "/Users/rik/git/plato-common-egse/venv38/lib/python3.8/site-packages/h5py/_hl/files.py", line 220, in make_fid 263 | fid = h5f.open(name, flags, fapl=fapl) 264 | File "h5py/_objects.pyx", line 54, in h5py._objects.with_phil.wrapper 265 | File "h5py/_objects.pyx", line 55, in h5py._objects.with_phil.wrapper 266 | File "h5py/h5f.pyx", line 106, in h5py.h5f.open 267 | OSError: [Errno 45] Unable to open file (unable to lock file, errno = 45, error message = 'Operation not supported') 268 | 269 | level=DEBUG ts=2022-05-02T11:18:43,393227 process=storage_cs process_id=25044 caller=egse.storage.persistence:1852 msg="Opening file /Users/rik/data/CSL/daily/20220502/20220502_CSL_SYN-HK.csv in mode 'a'" 270 | level=INFO ts=2022-05-02T11:18:43,393386 process=storage_cs process_id=25044 caller=egse.storage:833 msg="Storage successfully registered SYN-HK" 271 | level=INFO ts=2022-05-02T11:18:43,393923 process=syn_cs process_id=25082 caller=egse.storage:177 msg="Storage successfully registered SYN-HK" 272 | level=DEBUG ts=2022-05-02T11:18:43,399791 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating SynopticsManagerCommand command with name='store_common_synoptics', cmd='{common_hk}', device_method=" 273 | level=DEBUG ts=2022-05-02T11:18:43,399927 process=syn_cs process_id=25082 caller=egse.protocol:532 msg="Creating SynopticsManagerCommand command with name='store_th_synoptics', cmd='{th_hk}', device_method=" 274 | level=DEBUG ts=2022-05-02T11:18:43,400006 process=syn_cs process_id=25082 caller=egse.synoptics.syn_cs:50 msg="Binding ZeroMQ socket to tcp://*:6204" 275 | level=INFO ts=2022-05-02T11:18:43,400050 process=syn_cs process_id=25082 caller=egse.protocol:318 msg="Binding to tcp://*:6204" 276 | level=DEBUG ts=2022-05-02T11:18:43,418715 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 277 | level=DEBUG ts=2022-05-02T11:18:43,425603 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00048_210630_141747.yaml." 278 | level=DEBUG ts=2022-05-02T11:18:43,439043 process=storage_cs process_id=25044 caller=egse.storage.persistence:1852 msg="Opening file /Users/rik/data/CSL/daily/20220502/20220502_CSL_SYN.csv in mode 'a'" 279 | level=INFO ts=2022-05-02T11:18:43,439201 process=storage_cs process_id=25044 caller=egse.storage:833 msg="Storage successfully registered SYN" 280 | level=DEBUG ts=2022-05-02T11:18:45,054679 process=confman_cs process_id=25057 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/.cgse/local_settings.yaml." 281 | level=INFO ts=2022-05-02T11:18:45,061461 process=confman_cs process_id=25057 caller=egse.confman:203 msg="SetupInfo cache populated." 282 | level=DEBUG ts=2022-05-02T11:20:09,223713 process=MainProcess process_id=25215 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 283 | level=DEBUG ts=2022-05-02T11:20:10,113269 process=MainProcess process_id=25215 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 284 | level=DEBUG ts=2022-05-02T11:20:10,124303 process=MainProcess process_id=25215 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 285 | level=DEBUG ts=2022-05-02T11:20:10,330212 process=MainProcess process_id=25215 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 286 | level=DEBUG ts=2022-05-02T11:20:10,335579 process=MainProcess process_id=25215 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 287 | level=DEBUG ts=2022-05-02T11:20:10,532927 process=MainProcess process_id=25215 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 288 | level=DEBUG ts=2022-05-02T11:20:10,539587 process=MainProcess process_id=25215 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 289 | level=DEBUG ts=2022-05-02T11:20:10,549858 process=MainProcess process_id=25215 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 290 | level=DEBUG ts=2022-05-02T11:20:37,896738 process=MainProcess process_id=25261 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 291 | level=DEBUG ts=2022-05-02T11:20:38,824870 process=MainProcess process_id=25261 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 292 | level=DEBUG ts=2022-05-02T11:20:38,835749 process=MainProcess process_id=25261 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 293 | level=DEBUG ts=2022-05-02T11:20:39,052927 process=MainProcess process_id=25261 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 294 | level=DEBUG ts=2022-05-02T11:20:39,058312 process=MainProcess process_id=25261 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 295 | level=DEBUG ts=2022-05-02T11:20:39,263443 process=MainProcess process_id=25261 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 296 | level=DEBUG ts=2022-05-02T11:20:39,269893 process=MainProcess process_id=25261 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 297 | level=DEBUG ts=2022-05-02T11:20:39,279393 process=MainProcess process_id=25261 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 298 | level=DEBUG ts=2022-05-02T11:21:31,817863 process=MainProcess process_id=25349 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 299 | level=DEBUG ts=2022-05-02T11:21:32,719475 process=MainProcess process_id=25349 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 300 | level=DEBUG ts=2022-05-02T11:21:32,730718 process=MainProcess process_id=25349 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 301 | level=DEBUG ts=2022-05-02T11:21:32,938959 process=MainProcess process_id=25349 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 302 | level=DEBUG ts=2022-05-02T11:21:32,944485 process=MainProcess process_id=25349 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 303 | level=DEBUG ts=2022-05-02T11:21:33,145412 process=MainProcess process_id=25349 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 304 | level=DEBUG ts=2022-05-02T11:21:33,151842 process=MainProcess process_id=25349 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 305 | level=DEBUG ts=2022-05-02T11:21:33,161542 process=MainProcess process_id=25349 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 306 | level=DEBUG ts=2022-05-02T11:21:44,165822 process=MainProcess process_id=25385 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 307 | level=DEBUG ts=2022-05-02T11:21:45,090342 process=MainProcess process_id=25385 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 308 | level=DEBUG ts=2022-05-02T11:21:45,102065 process=MainProcess process_id=25385 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 309 | level=DEBUG ts=2022-05-02T11:21:45,311119 process=MainProcess process_id=25385 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 310 | level=DEBUG ts=2022-05-02T11:21:45,316589 process=MainProcess process_id=25385 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 311 | level=DEBUG ts=2022-05-02T11:21:45,519640 process=MainProcess process_id=25385 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 312 | level=DEBUG ts=2022-05-02T11:21:45,526550 process=MainProcess process_id=25385 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 313 | level=DEBUG ts=2022-05-02T11:21:45,536400 process=MainProcess process_id=25385 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 314 | level=INFO ts=2022-05-02T11:21:45,558418 process=confman_cs process_id=25057 caller=egse.confman:597 msg="New Setup loaded from /Users/rik/git/plato-cgse-conf/data/CSL/conf/SETUP_CSL_00096_220114_090151.yaml" 315 | level=DEBUG ts=2022-05-02T11:21:57,165239 process=MainProcess process_id=25414 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 316 | level=DEBUG ts=2022-05-02T11:21:58,077293 process=MainProcess process_id=25414 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 317 | level=DEBUG ts=2022-05-02T11:21:58,089890 process=MainProcess process_id=25414 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 318 | level=DEBUG ts=2022-05-02T11:21:58,296766 process=MainProcess process_id=25414 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 319 | level=DEBUG ts=2022-05-02T11:21:58,302228 process=MainProcess process_id=25414 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 320 | level=DEBUG ts=2022-05-02T11:21:58,496283 process=MainProcess process_id=25414 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 321 | level=DEBUG ts=2022-05-02T11:21:58,502661 process=MainProcess process_id=25414 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 322 | level=DEBUG ts=2022-05-02T11:21:58,512081 process=MainProcess process_id=25414 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 323 | level=DEBUG ts=2022-05-02T11:22:45,588637 process=MainProcess process_id=25494 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 324 | level=DEBUG ts=2022-05-02T11:22:46,078007 process=MainProcess process_id=25494 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 325 | level=DEBUG ts=2022-05-02T11:22:46,088396 process=MainProcess process_id=25494 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 326 | level=DEBUG ts=2022-05-02T11:22:46,297088 process=MainProcess process_id=25494 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 327 | level=DEBUG ts=2022-05-02T11:22:46,302634 process=MainProcess process_id=25494 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 328 | level=DEBUG ts=2022-05-02T11:22:46,503805 process=MainProcess process_id=25494 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 329 | level=DEBUG ts=2022-05-02T11:22:46,510259 process=MainProcess process_id=25494 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 330 | level=DEBUG ts=2022-05-02T11:22:46,519900 process=MainProcess process_id=25494 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 331 | level=DEBUG ts=2022-05-02T11:23:05,935365 process=MainProcess process_id=25528 caller=egse.resource:205 msg="Resources have been defined: DEFAULT_RESOURCES={'icons': '/icons', 'images': '/images', 'lib': '/lib', 'styles': '/styles', 'data': '/data', 'aeudata': '/aeu/arbdata'}" 332 | level=DEBUG ts=2022-05-02T11:23:06,422069 process=MainProcess process_id=25528 caller=egse.dsi.esl:88 msg="Locating shared library EtherSpaceLink_v34_86.dylib in dir 'lib/macOS'" 333 | level=DEBUG ts=2022-05-02T11:23:06,432696 process=MainProcess process_id=25528 caller=egse.config:245 msg="Common-EGSE location is automatically determined: /Users/rik/git/plato-common-egse." 334 | level=DEBUG ts=2022-05-02T11:23:06,639052 process=MainProcess process_id=25528 caller=egse.dsi.esl:92 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/EtherSpaceLink_v34_86.dylib" 335 | level=DEBUG ts=2022-05-02T11:23:06,644599 process=MainProcess process_id=25528 caller=egse.dsi.rmap:107 msg="Locating shared library ESL-RMAP_v34_86.dylib in dir 'lib/macOS'" 336 | level=DEBUG ts=2022-05-02T11:23:06,845539 process=MainProcess process_id=25528 caller=egse.dsi.rmap:111 msg="Loading shared library: /Users/rik/git/plato-common-egse/build/lib/egse/lib/macOS/ESL-RMAP_v34_86.dylib" 337 | level=DEBUG ts=2022-05-02T11:23:06,851853 process=MainProcess process_id=25528 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/storage/storage.yaml." 338 | level=DEBUG ts=2022-05-02T11:23:06,861203 process=MainProcess process_id=25528 caller=egse.settings:147 msg="Parsing YAML configuration file /Users/rik/git/plato-common-egse/src/egse/confman/confman.yaml." 339 | level=DEBUG ts=2022-05-02T11:23:06,861304 process=dpu.processor process_id=33551 caller=egse.dpu:1505 msg="Register Map for N-FEE 340 | reg_0_config: 341 | v_start=0 342 | v_end=4509 343 | reg_1_config: 344 | charge_injection_width=0 345 | charge_injection_gap=0 346 | reg_2_config: 347 | parallel_toi_period=875 348 | parallel_clk_overlap=250 349 | ccd_readout_order=228 350 | reg_3_config: 351 | n_final_dump=0 352 | h_end=2294 353 | charge_injection_en=0 354 | tri_level_clk_en=0 355 | img_clk_dir=0 356 | reg_clk_dir=0 357 | reg_4_config: 358 | packet_size=32140 359 | int_sync_period=6250 360 | reg_5_config: 361 | Trap_Pumping_Dwell_counter=12500 362 | sync_sel=0 363 | sensor_sel=3 364 | digitise_en=1 365 | DG_en=0 366 | ccd_read_en=1 367 | conv_dly=15 368 | High_precision_HK_en=0 369 | reg_6_config: 370 | ccd1_win_list_ptr=0 371 | reg_7_config: 372 | ccd1_pktorder_list_ptr=0 373 | reg_8_config: 374 | ccd1_win_list_length=0 375 | ccd1_win_size_x=0 376 | ccd1_win_size_y=0 377 | reg_8_config_reserved=0 378 | reg_9_config: 379 | ccd2_win_list_ptr=0 380 | reg_10_config: 381 | ccd2_pktorder_list_ptr=0 382 | reg_11_config: 383 | ccd2_win_list_length=0 384 | ccd2_win_size_x=0 385 | ccd2_win_size_y=0 386 | reg_11_config_reserved=0 387 | reg_12_config: 388 | ccd3_win_list_ptr=0 389 | reg_13_config: 390 | ccd3_pktorder_list_ptr=0 391 | reg_14_config: 392 | ccd3_win_list_length=0 393 | ccd3_win_size_x=0 394 | ccd3_win_size_y=0 395 | reg_14_config_reserved=0 396 | reg_15_config: 397 | ccd4_win_list_ptr=0 398 | reg_16_config: 399 | ccd4_pktorder_list_ptr=0 400 | reg_17_config: 401 | ccd4_win_list_length=0 402 | ccd4_win_size_x=0 403 | ccd4_win_size_y=0 404 | reg_17_config_reserved=0 405 | reg_18_config: 406 | ccd_vod_config=3276 407 | ccd1_vrd_config=3685 408 | ccd2_vrd_config=101 409 | reg_19_config: 410 | ccd2_vrd_config=14 411 | ccd3_vrd_config=3685 412 | ccd4_vrd_config=3685 413 | ccd_vgd_config=14 414 | reg_20_config: 415 | ccd_vgd_config=207 416 | ccd_vog_config=410 417 | ccd_ig_hi_config=0 418 | reg_21_config: 419 | ccd_ig_lo_config=0 420 | trk_hld_hi=4 421 | trk_hld_lo=14 422 | cont_rst_on=0 423 | cont_cds_on=0 424 | ccd_mode_config=0 425 | reg_21_config_reserved=0 426 | clear_error_flag=0 427 | reg_22_config: 428 | r_cfg1=7 429 | r_cfg2=11 430 | cdsclp_lo=9 431 | reg_22_config_reserved=0 432 | reg_23_config: 433 | ccd1_last_Epacket=0 434 | ccd1_last_Fpacket=0 435 | ccd2_last_Epacket=0 436 | reg_23_config_reserved=0 437 | reg_24_config: 438 | ccd2_last_Fpacket=0 439 | ccd3_last_Epacket=0 440 | ccd3_last_Fpacket=0 441 | reg_24_config_reserved=0 442 | reg_25_config: 443 | ccd4_last_Epacket=0 444 | ccd4_last_Fpacket=0 445 | Surface_Inversion_counter=100 446 | reg_25_config_reserved=0 447 | reg_26_config: 448 | Readout_pause_counter=2000 449 | Trap_Pumping_Shuffle_counter=1000 450 | " 451 | level=WARNING ts=2022-05-02T19:02:32,738949 process=confman_cs process_id=25057 caller=egse.confman.confman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 452 | level=WARNING ts=2022-05-02T19:02:33,155803 process=syn_cs process_id=25082 caller=egse.synoptics.syn_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 453 | level=WARNING ts=2022-05-02T19:02:31,966084 process=procman_cs process_id=25064 caller=egse.procman.procman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 454 | level=WARNING ts=2022-05-02T19:03:59,830339 process=syn_cs process_id=25082 caller=egse.synoptics.syn_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 455 | level=WARNING ts=2022-05-02T19:03:59,835593 process=procman_cs process_id=25064 caller=egse.procman.procman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 456 | level=WARNING ts=2022-05-02T19:03:59,840218 process=confman_cs process_id=25057 caller=egse.confman.confman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 457 | level=ERROR ts=2022-05-02T19:03:59,934126 process=ogse_cs process_id=63885 caller=egse.collimator.fcul.ogse_cs:88 msg="Caught Exception: invalid literal for int() with base 10: '0.0'" 458 | Traceback (most recent call last): 459 | File "/Users/rik/git/plato-common-egse/src/egse/collimator/fcul/ogse_cs.py", line 81, in start 460 | control_server.serve() 461 | File "/Users/rik/git/plato-common-egse/src/egse/control.py", line 261, in serve 462 | storage_manager and self.register_to_storage_manager() 463 | File "/Users/rik/git/plato-common-egse/src/egse/control.py", line 374, in register_to_storage_manager 464 | "column_names": list(self.device_protocol.get_housekeeping().keys()), 465 | File "/Users/rik/git/plato-common-egse/src/egse/collimator/fcul/ogse_protocol.py", line 45, in get_housekeeping 466 | ogse_status = self.device.status() 467 | File "/Users/rik/git/plato-common-egse/src/egse/mixin.py", line 329, in command_wrapper 468 | response = process_response(response) 469 | File "/Users/rik/git/plato-common-egse/src/egse/collimator/fcul/ogse.py", line 239, in decode_status_command 470 | att_index = int(att_index[1:-1]) # cut off the leading '#' and the trailing ',' 471 | ValueError: invalid literal for int() with base 10: '0.0' 472 | 473 | level=WARNING ts=2022-05-02T19:04:03,204096 process=syn_cs process_id=25082 caller=egse.synoptics.syn_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 474 | level=WARNING ts=2022-05-02T19:04:02,867278 process=procman_cs process_id=25064 caller=egse.procman.procman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 475 | level=WARNING ts=2022-05-02T19:04:03,029600 process=confman_cs process_id=25057 caller=egse.confman.confman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 476 | level=WARNING ts=2022-05-02T19:04:11,632327 process=syn_cs process_id=25082 caller=egse.synoptics.syn_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 477 | level=WARNING ts=2022-05-02T19:04:11,629427 process=procman_cs process_id=25064 caller=egse.procman.procman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 478 | level=WARNING ts=2022-05-02T19:04:11,633167 process=confman_cs process_id=25057 caller=egse.confman.confman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 479 | level=WARNING ts=2022-05-02T19:04:15,380978 process=syn_cs process_id=25082 caller=egse.synoptics.syn_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 480 | level=WARNING ts=2022-05-02T19:04:15,380965 process=procman_cs process_id=25064 caller=egse.procman.procman_cs:353 msg="Couldn't connect to the Storage manager to store housekeeping: Proxy is not connected when entering the context." 481 | level=ERROR ts=2022-05-12T07:50:56,043197 process=ogse_cs process_id=3367 caller=egse.collimator.fcul.ogse_cs:88 msg="Caught Exception: 'Setup' object has no attribute 'gse'" 482 | Traceback (most recent call last): 483 | File "/Users/rik/git/plato-common-egse/src/egse/collimator/fcul/ogse_cs.py", line 80, in start 484 | control_server = OGSEControlServer() 485 | File "/Users/rik/git/plato-common-egse/src/egse/collimator/fcul/ogse_cs.py", line 33, in __init__ 486 | self.device_protocol = OGSEProtocol(self) 487 | File "/Users/rik/git/plato-common-egse/src/egse/collimator/fcul/ogse_protocol.py", line 24, in __init__ 488 | self.fwc_calibration = GlobalState.setup.gse.ogse.calibration.fwc_calibration 489 | File "/Users/rik/git/plato-common-egse/src/egse/setup.py", line 261, in __getattribute__ 490 | value = object.__getattribute__(self, key) 491 | AttributeError: 'Setup' object has no attribute 'gse' 492 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rich 2 | textual 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import sys 4 | from shutil import rmtree 5 | 6 | from setuptools import setup, Command 7 | 8 | # Package meta-data. 9 | 10 | NAME = "textualog" 11 | PACKAGE_NAME = "textualog" 12 | DESCRIPTION = "Display, filter and search logging messages in the terminal." 13 | URL = "https://github.com/rhuygen/textualog" 14 | EMAIL = "rik.huygen@kuleuven.be" 15 | AUTHOR = "Rik Huygen" 16 | REQUIRES_PYTHON = '>=3.8.0' 17 | VERSION = None 18 | 19 | # The directory containing this file 20 | 21 | HERE = pathlib.Path(__file__).parent 22 | 23 | # The directory containing the source code 24 | 25 | SRC = HERE / "src" 26 | 27 | # The text of the README file 28 | 29 | README = (HERE / "README.md").read_text() 30 | 31 | # Load the package's __version__.py module as a dictionary. 32 | 33 | about = {} 34 | if VERSION is None: 35 | with open(os.path.join(SRC, PACKAGE_NAME, '__version__.py')) as f: 36 | exec(f.read(), about) 37 | VERSION = about['__version__'] 38 | 39 | 40 | class UploadCommand(Command): 41 | """Support setup.py upload.""" 42 | 43 | description = 'Build and publish the package to PyPI.' 44 | user_options = [] 45 | 46 | @staticmethod 47 | def status(s): 48 | """Prints things in bold.""" 49 | print('\033[1m{0}\033[0m'.format(s)) 50 | 51 | def initialize_options(self): 52 | pass 53 | 54 | def finalize_options(self): 55 | pass 56 | 57 | def run(self): 58 | try: 59 | self.status('Removing previous builds…') 60 | rmtree(os.path.join(HERE, 'dist')) 61 | except OSError: 62 | pass 63 | 64 | self.status("Building Source and Wheel (universal) distribution…") 65 | os.system(f"{sys.executable} setup.py sdist bdist_wheel --universal") 66 | 67 | self.status('Uploading the package to PyPI via Twine…') 68 | os.system('twine upload dist/*') 69 | 70 | self.status('Pushing git tags…') 71 | os.system(f"git tag v{VERSION}") 72 | os.system('git push --tags') 73 | 74 | sys.exit() 75 | 76 | 77 | class UploadTestCommand(Command): 78 | """Support setup.py upload-test.""" 79 | 80 | description = 'Build and publish the package to Test PyPI.' 81 | user_options = [] 82 | 83 | @staticmethod 84 | def status(s): 85 | """Prints things in bold.""" 86 | print('\033[1m{0}\033[0m'.format(s)) 87 | 88 | def initialize_options(self): 89 | pass 90 | 91 | def finalize_options(self): 92 | pass 93 | 94 | def run(self): 95 | try: 96 | self.status('Removing previous builds…') 97 | rmtree(os.path.join(HERE, 'dist')) 98 | except OSError: 99 | pass 100 | 101 | self.status("Building Source and Wheel (universal) distribution…") 102 | os.system(f"{sys.executable} setup.py sdist bdist_wheel --universal") 103 | 104 | self.status('Uploading the package to PyPI via Twine…') 105 | os.system('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') 106 | 107 | sys.exit() 108 | 109 | 110 | # This call to setup() does all the work 111 | 112 | setup( 113 | name=NAME, 114 | version=VERSION, 115 | description=DESCRIPTION, 116 | long_description=README, 117 | long_description_content_type="text/markdown", 118 | url=URL, 119 | author=AUTHOR, 120 | author_email=EMAIL, 121 | python_requires=REQUIRES_PYTHON, 122 | license="MIT", 123 | classifiers=[ 124 | "License :: OSI Approved :: MIT License", 125 | "Environment :: Console", 126 | "Topic :: Utilities", 127 | "Development Status :: 3 - Alpha", 128 | "Programming Language :: Python :: 3", 129 | "Programming Language :: Python :: 3.8", 130 | ], 131 | packages=["textualog", "textualog.renderables", "textualog.widgets"], 132 | package_dir={"": "src"}, 133 | package_data={"": ["textualog.png", "examples/*.log"]}, 134 | include_package_data=True, 135 | install_requires=["rich", "textual"], 136 | entry_points={ 137 | "console_scripts": [ 138 | "textualog=textualog.__main__:main", 139 | ] 140 | }, 141 | # $ setup.py publish support. 142 | cmdclass={ 143 | 'upload': UploadCommand, 144 | 'upload_test': UploadTestCommand, 145 | }, 146 | ) 147 | -------------------------------------------------------------------------------- /src/tests/do_follow.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from textualog.log import setup_logging 4 | from textualog.system import do_every 5 | 6 | setup_logging("test.log") 7 | 8 | MODULE_LOGGER = logging.getLogger() 9 | 10 | 11 | def fake_log_messages(): 12 | MODULE_LOGGER.debug("A periodic debug message for testing.") 13 | MODULE_LOGGER.info("A periodic info message for testing.") 14 | MODULE_LOGGER.warning("A periodic warning message for testing.") 15 | MODULE_LOGGER.error("A periodic error message for testing.") 16 | MODULE_LOGGER.critical("A periodic critical message for testing.") 17 | try: 18 | raise FileNotFoundError("The file no-name.txt doesn't exist") 19 | except FileNotFoundError as exc: 20 | MODULE_LOGGER.error(f"Caught an exception: {exc}", exc_info=True) 21 | 22 | 23 | if __name__ == "__main__": 24 | do_every(1.0, fake_log_messages) 25 | -------------------------------------------------------------------------------- /src/tests/test_record.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from textualog.renderables.logrecord import LogRecord 5 | 6 | 7 | def test_simple_construction(): 8 | 9 | msg = "A simple log message" 10 | record = LogRecord(msg) 11 | 12 | assert record.created <= time.time() 13 | assert record.msg == msg 14 | assert record.level == logging.INFO 15 | -------------------------------------------------------------------------------- /src/textualog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhuygen/textualog/c3f5f22d37745e40e5331ca345bffa0ef2beadf8/src/textualog/__init__.py -------------------------------------------------------------------------------- /src/textualog/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | import threading 5 | from pathlib import Path 6 | 7 | from textual import events 8 | from textual.app import App 9 | from textual.keys import Keys 10 | from textual.reactive import Reactive 11 | from textual.widgets import Header 12 | from textual.widgets import ScrollView 13 | 14 | from . import __version__ 15 | from .loader import KeyValueLoader 16 | from .log import setup_logging 17 | from .renderables.namespace_tree import EntryClick 18 | from .system import do_every 19 | from .widgets.details import Details 20 | from .widgets.footer import Footer 21 | from .widgets.help import Help 22 | from .widgets.levels import Levels 23 | from .widgets.namespaces import Namespaces 24 | from .widgets.recordinfo import RecordInfo 25 | from .widgets.records import Records 26 | 27 | logging.basicConfig(level=logging.ERROR) 28 | 29 | MODULE_LOGGER = logging.getLogger("Textual") 30 | 31 | 32 | class TextualLog(App): 33 | 34 | show_help = Reactive(False) 35 | show_namespaces = Reactive(False) 36 | show_details = Reactive(False) 37 | 38 | # The namespace_tree is just for demonstration purposes. The namespace should be a 39 | # tree like structure with proper navigation and the possibility to add and remove nodes. 40 | 41 | namespace_tree = { 42 | "egse": { 43 | "system": "system", 44 | "decorators": {"x": 1, "y": 2}, 45 | } 46 | } 47 | 48 | def __init__(self, filename: str = None, **kwargs): 49 | super().__init__(**kwargs) 50 | self.filename = filename 51 | self.cursor = 0 52 | self.loader = None 53 | self.details_widget = None 54 | self.follow = False 55 | 56 | async def on_mount(self, event: events.Mount) -> None: 57 | """ 58 | Call after terminal goes in to application mode. 59 | """ 60 | 61 | self.namespaces = Namespaces("Name space", self.namespace_tree) 62 | self.namespaces.layout_offset_x = -40 63 | 64 | self.help_widget = Help() 65 | self.help_widget.visible = False 66 | 67 | self.details_widget = Details() 68 | self.details_scroll_view = ScrollView(self.details_widget) 69 | self.details_scroll_view.visible = False 70 | 71 | self.header = Header() 72 | self.footer = Footer() 73 | 74 | await self.view.dock(self.header, edge="top") 75 | await self.view.dock(self.footer, edge="bottom") 76 | await self.view.dock(self.namespaces, edge="left", size=40, z=1) 77 | await self.view.dock(self.help_widget, edge="right", size=40, z=1) 78 | await self.view.dock(self.details_scroll_view, z=0) 79 | grid = await self.view.dock_grid(edge="left", name="left") 80 | 81 | grid.add_column(size=30, name="left") 82 | grid.add_column(fraction=1, name="right", min_size=100) 83 | 84 | grid.add_row(fraction=1, name="top") 85 | grid.add_row(fraction=1, name="middle") 86 | grid.add_row(size=7, name="bottom") 87 | 88 | grid.add_areas( 89 | area1="left-start|right-end,top-start|middle-end", 90 | area2="left,bottom", 91 | area3="right,bottom", 92 | ) 93 | 94 | self.levels = Levels() 95 | self.records = Records() 96 | self.record_info = RecordInfo() 97 | 98 | grid.place( 99 | area1=self.records, 100 | area2=self.levels, 101 | area3=self.record_info, 102 | ) 103 | 104 | if self.filename: 105 | self.loader = KeyValueLoader(self.filename) 106 | self.loader.load() 107 | self.loader.process(0, 500, None) 108 | self.footer.log_size = self.loader.size() 109 | 110 | # The height of the self.records view is not yet known, so we take a large enough number 111 | 112 | self.records.update(self.loader.get_records(0, 500, None)) 113 | 114 | # self.set_interval(2.0, self.collect_data) 115 | 116 | self._reload_thread = threading.Thread( 117 | target=do_every, args=(2.0, self.collect_data)) 118 | self._reload_thread.daemon = True 119 | self._reload_thread.start() 120 | 121 | # @timer(level=logging.DEBUG) 122 | def collect_data(self): 123 | if not self.follow or self.loader is None: 124 | return 125 | 126 | self.loader.load() 127 | 128 | # The height of the text area of the Records panel 129 | 130 | height = self.records.size.height - 2 131 | size = self.loader.size() 132 | 133 | self.cursor = max(0, size - height) 134 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 135 | self.records.refresh(layout=True) 136 | self.cursor = self.loader.offset # the cursor/offset might have changed 137 | self.footer.log_size = size 138 | 139 | async def on_load(self) -> None: 140 | """ 141 | Sent before going in to application mode. This method is called before on_mount(). 142 | Bind keys here. 143 | """ 144 | await self.bind("q", "quit", "Quit") 145 | await self.bind("?", "toggle_help", "Help") 146 | 147 | async def on_key(self, event) -> None: 148 | 149 | if self.loader is None: 150 | return 151 | 152 | # The height of the text area of the Records panel 153 | 154 | height = self.records.size.height - 2 155 | size = self.loader.size() 156 | 157 | self.app.sub_title = f"Key pressed: {event.key}" 158 | 159 | if event.key == "d": 160 | self.levels.debug_level = not self.levels.debug_level 161 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 162 | elif event.key == "i": 163 | self.levels.info_level = not self.levels.info_level 164 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 165 | elif event.key == "w": 166 | self.levels.warning_level = not self.levels.warning_level 167 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 168 | elif event.key == "e": 169 | self.levels.error_level = not self.levels.error_level 170 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 171 | elif event.key == "c": 172 | self.levels.critical_level = not self.levels.critical_level 173 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 174 | elif event.key in "nN": 175 | self.show_namespaces = not self.show_namespaces 176 | elif event.key == "r": 177 | self.loader.load() 178 | self.footer.log_size = self.loader.size() 179 | elif event.key == "f": 180 | self.follow = not self.follow 181 | self.header.style = "white on dark_red" if self.follow else "white on dark_green" 182 | # self.collect_data() 183 | elif event.key == Keys.Escape: 184 | self.show_help = False 185 | self.show_namespaces = False 186 | self.show_details = False 187 | elif event.key == Keys.Down: 188 | self.cursor = min(size, self.cursor + 1) 189 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels, +1)) 190 | self.footer.log_offset = self.cursor = self.loader.offset 191 | elif event.key == Keys.Up: 192 | self.cursor = max(0, self.cursor - 1) 193 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels, -1)) 194 | self.footer.log_offset = self.cursor = self.loader.offset 195 | elif event.key == Keys.PageDown: 196 | self.cursor = min(size, self.cursor+(height-1)) 197 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 198 | self.footer.log_offset = self.cursor = self.loader.offset 199 | elif event.key == Keys.PageUp: 200 | self.cursor = max(0, self.cursor-(height-1)) 201 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 202 | self.footer.log_offset = self.cursor = self.loader.offset 203 | elif event.key == Keys.End: 204 | self.cursor = max(0, size - height) 205 | self.footer.log_offset = self.cursor 206 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 207 | elif event.key == Keys.Home: 208 | self.cursor = 0 209 | self.footer.log_offset = self.cursor 210 | self.records.replace(self.loader.get_records(self.cursor, height, self.levels)) 211 | 212 | self.records.refresh(layout=True, repaint=True) 213 | 214 | async def watch_show_namespaces(self, show_namespaces: bool) -> None: 215 | """Called when show_namespaces changes.""" 216 | self.namespaces.animate("layout_offset_x", 0 if show_namespaces else -40) 217 | 218 | async def watch_show_help(self, show_help: bool) -> None: 219 | """Called when show_help changes.""" 220 | self.help_widget.visible = show_help 221 | 222 | async def watch_show_details(self, show_details: bool) -> None: 223 | """Called when show_details changes.""" 224 | self.details_widget.refresh(layout=True) 225 | self.details_scroll_view.visible = show_details 226 | 227 | async def action_toggle_help(self) -> None: 228 | """Called when bound help key is pressed.""" 229 | self.show_help = not self.show_help 230 | 231 | async def handle_entry_click(self, message: EntryClick) -> None: 232 | """A message sent by the namespace tree when an entry is clicked.""" 233 | 234 | self.app.sub_title = f"{message.key}" 235 | self.records.refresh(layout=True) 236 | 237 | 238 | def main(): 239 | from rich.traceback import install 240 | install(show_locals=False) 241 | 242 | parser = argparse.ArgumentParser( 243 | description="Textual Log Viewer, display, filter and search log files", 244 | formatter_class=argparse.RawTextHelpFormatter, 245 | ) 246 | 247 | parser.add_argument( 248 | "--version", 249 | "-v", 250 | action="version", 251 | version=_get_version_text(), 252 | help="display version information", 253 | ) 254 | 255 | parser.add_argument( 256 | "--log", 257 | "-l", 258 | type=str, 259 | default=None, 260 | help="the full path to the log file that you want to follow", 261 | ) 262 | 263 | parser.add_argument( 264 | "--debug", 265 | "-d", 266 | action="store_true", 267 | default=False, 268 | help="send debugging information to the debug log files", 269 | ) 270 | 271 | args = parser.parse_args() 272 | 273 | if args.log and not Path(args.log).exists(): 274 | raise FileNotFoundError(f"No such file {args.log}") 275 | 276 | log_filename = "textual.log" if args.debug else None 277 | 278 | if args.debug: 279 | setup_logging("textualog.log") 280 | 281 | TextualLog.run(title="Textual Log Viewer", log=log_filename, filename=args.log) 282 | 283 | 284 | def _get_version_text(): 285 | python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" 286 | 287 | return "\n".join( 288 | [ 289 | f"textualog {__version__.__version__} [Python {python_version}]", 290 | "Copyright © 2022 Rik Huygen", 291 | ] 292 | ) 293 | 294 | 295 | if __name__ == "__main__": 296 | main() 297 | -------------------------------------------------------------------------------- /src/textualog/__version__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The one and only place for the version number. 3 | """ 4 | VERSION = (0, 1, 2) 5 | 6 | __version__ = '.'.join(map(str, VERSION)) 7 | -------------------------------------------------------------------------------- /src/textualog/emojis.py: -------------------------------------------------------------------------------- 1 | THINKING_FACE = ":thinking_face:" 2 | FIRE = ":fire:" 3 | INFO = "[blue]:information:[/]" 4 | CHECK = "✓" # "✔️" "✅" 5 | UNCHECK = "" # "✖️" "❎" 6 | LOG_BOOK = "📓" 7 | -------------------------------------------------------------------------------- /src/textualog/loader.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from itertools import islice 3 | from typing import List 4 | from typing import Optional 5 | 6 | from rich.text import Text 7 | 8 | from .renderables.logrecord import LevelName 9 | from .renderables.logrecord import LogRecord 10 | from .widgets.levels import Levels 11 | 12 | DEFAULT_NUM_LINES = 100 13 | MAX_NUM_LINES = 100_000 14 | 15 | MODULE_LOGGER = logging.getLogger("Textual.loader") 16 | 17 | 18 | # KeyValueLoader is a class that loads log files that have a key-value format. 19 | # The following key-value pairs are expected in this order: 20 | # 21 | # level= 22 | # ts= in the format %Y-%m-%dT%H:%M:%S,%f e.g. 2022-04-08T10:52:20,371211 23 | # process= 24 | # process_id= 25 | # caller=: 26 | # msg="" 27 | 28 | class KeyValueLoader: 29 | def __init__(self, filename: str): 30 | self.filename = filename 31 | self._lines: List[str] = ... 32 | """The original lines read from the log file.""" 33 | self._size = 0 34 | """The number of lines in the log file.""" 35 | self._records = [] 36 | """Processed lines""" 37 | self._offset = 0 38 | """The line number of the first record, i.e. which line in the log file.""" 39 | 40 | def load(self): 41 | """Loads the complete log file in a list of strings.""" 42 | with open(self.filename, 'r') as fd: 43 | self._lines = fd.read().split('\n') 44 | 45 | self._size = len(self._lines) 46 | 47 | # This should really be __len__ 48 | def size(self) -> int: 49 | """Returns the total number of lines in the log file.""" 50 | return self._size 51 | 52 | @property 53 | def offset(self): 54 | return self._offset 55 | 56 | def process(self, 57 | start: int = 0, num_lines: int = DEFAULT_NUM_LINES, levels: Levels = None, 58 | direction: int = 0): 59 | """Process a number of lines and creates a list of Records for those lines.""" 60 | 61 | # * could keep track of those line that have been processed -> no need to process again 62 | # * sub_messages are e.g. Traceback or multiline messages 63 | 64 | MODULE_LOGGER.info(f"Before backtracking: {start=}, {num_lines=}") 65 | 66 | # if start falls in the middle of a sub_message, 67 | # * we go back until the actual log message if direction is -1 68 | # * go forward until the actual log message if direction is +1 69 | 70 | if direction > 0: 71 | while start < self._size and not self._lines[start].startswith("level="): 72 | start += 1 73 | elif direction < 0: 74 | while start > 0 and not self._lines[start].startswith("level="): 75 | start -= 1 76 | else: 77 | ... 78 | 79 | MODULE_LOGGER.info(f"After backtracking: {start=}, {num_lines=}") 80 | 81 | self._offset = start 82 | 83 | sub_message: List[str] = [] 84 | in_sub_message = False 85 | records = [] 86 | record: Optional[LogRecord] = None 87 | match_count = 0 88 | for count, line in enumerate(islice(self._lines, start, None)): 89 | if count > MAX_NUM_LINES: 90 | break 91 | if not line.startswith("level="): 92 | in_sub_message = True 93 | sub_message.append(line) 94 | continue 95 | if in_sub_message: 96 | if record is not None: 97 | record.extra = '\n'.join(sub_message) 98 | sub_message = [] 99 | in_sub_message = False 100 | 101 | level, ts, process, process_id, caller, msg = line.split(maxsplit=5) 102 | level = LevelName[level[6:]].value 103 | 104 | if levels is None or levels.is_on(level): 105 | 106 | record = LogRecord( 107 | level=level, 108 | ts=ts[3:], 109 | process=process[8:], 110 | process_id=process_id[11:], 111 | caller=caller[7:], 112 | msg=msg[4:].strip('"') # also removes the double quotes around the message 113 | ) 114 | 115 | records.append(record) 116 | match_count += 1 117 | 118 | if match_count >= num_lines: 119 | break 120 | 121 | if sub_message and record is not None: 122 | record.extra = '\n'.join(sub_message) 123 | 124 | self._records = records 125 | 126 | def __str__(self): 127 | return "\n".join(self._records) 128 | 129 | def reprocess(self, start: int, num_lines: int, levels: Levels): 130 | # start and stop are line numbers of the log file 131 | 132 | # item is the correct mapping of the requested lines in the self._records list. 133 | # 134 | # if not 0 < start - offset < len(self._records) -> reprocess the lines 135 | # if not 0 < start + num_lines - offset < len(self._records) -> reprocess the lines 136 | 137 | nr_records = len(self._records) 138 | 139 | if not 0 <= start - self._offset < nr_records or \ 140 | not 0 < start + num_lines - self._offset <= nr_records: 141 | self.process(start, num_lines, levels) 142 | 143 | def get_records(self, 144 | start: int = 0, 145 | num_lines: int = DEFAULT_NUM_LINES, 146 | levels: Levels = None, 147 | direction: int = 0) -> List[LogRecord]: 148 | 149 | self.process(start, num_lines, levels, direction) 150 | 151 | return self._records 152 | 153 | def get_text(self, start: int = 0, num_lines: int = DEFAULT_NUM_LINES, levels: Levels = None) -> Text: 154 | 155 | self.process(start, num_lines, levels) 156 | 157 | record: LogRecord 158 | text = Text() 159 | [ 160 | text.append(record.__rich__()).append('\n') 161 | for record in self._records 162 | ] 163 | return text 164 | 165 | 166 | if __name__ == "__main__": 167 | 168 | fn = '/Users/rik/Desktop/general.log' 169 | fn = '/Users/rik/data/CSL/log/general.log.2022-04-08' 170 | fn = '/Users/rik/data/CSL/log/general.log' 171 | 172 | loader = KeyValueLoader(fn) 173 | loader.load() 174 | loader.process(0, 20, None) 175 | 176 | import rich 177 | rich.print(loader.get_text(0, 20)) 178 | rich.print("---") 179 | rich.print(loader.get_text(2, 4)) 180 | rich.print("---") 181 | rich.print(loader.get_text(18, 2)) 182 | rich.print("---") 183 | rich.print(loader.get_text(20, 2)) 184 | -------------------------------------------------------------------------------- /src/textualog/log.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | LOG_FORMAT_KEY_VALUE = ( 5 | "level=%(levelname)s " 6 | "ts=%(asctime)s " 7 | "process=%(processName)s " 8 | "process_id=%(process)s " 9 | "caller=%(name)s:%(lineno)s " 10 | "msg=\"%(message)s\"" 11 | ) 12 | 13 | LOG_FORMAT_DATE = "%Y-%m-%dT%H:%M:%S,%f" 14 | 15 | 16 | class DateTimeFormatter(logging.Formatter): 17 | 18 | def formatTime(self, record, datefmt=None): 19 | converted_time = datetime.datetime.fromtimestamp(record.created) 20 | if datefmt: 21 | return converted_time.strftime(datefmt) 22 | formatted_time = converted_time.strftime("%Y-%m-%dT%H:%M:%S") 23 | return f"{formatted_time}.{record.msecs:03.0f}" 24 | 25 | 26 | def setup_logging(filename: str): 27 | file_formatter = DateTimeFormatter(fmt=LOG_FORMAT_KEY_VALUE, datefmt=LOG_FORMAT_DATE) 28 | file_handler = logging.FileHandler(filename=filename) 29 | file_handler.formatter = file_formatter 30 | file_handler.level = logging.DEBUG 31 | try: 32 | # The first default handler is the StreamHandler 33 | logging.getLogger().handlers[0].level = logging.ERROR 34 | except IndexError: 35 | pass 36 | logging.getLogger().addHandler(file_handler) 37 | logging.getLogger().level = logging.DEBUG 38 | -------------------------------------------------------------------------------- /src/textualog/renderables/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhuygen/textualog/c3f5f22d37745e40e5331ca345bffa0ef2beadf8/src/textualog/renderables/__init__.py -------------------------------------------------------------------------------- /src/textualog/renderables/logrecord.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import time 4 | from enum import Enum 5 | from typing import Union 6 | 7 | import rich 8 | from rich.text import Text 9 | 10 | 11 | class LevelColor(Enum): 12 | DEBUG = "white" 13 | INFO = "green" 14 | WARNING = "orange3" 15 | ERROR = "red" 16 | CRITICAL = "bright_magenta" 17 | 18 | 19 | class LevelColorSelected(Enum): 20 | DEBUG = "grey93 on grey46" 21 | INFO = "green3 on dark_green" 22 | WARNING = "orange1 on dark_orange3" 23 | ERROR = "orchid2 on red3" 24 | CRITICAL = "magenta1 on dark_magenta" 25 | 26 | 27 | class LevelName(Enum): 28 | DEBUG = logging.DEBUG 29 | INFO = logging.INFO 30 | WARNING = logging.WARNING 31 | ERROR = logging.ERROR 32 | CRITICAL = logging.CRITICAL 33 | 34 | 35 | class LogRecord: 36 | def __init__( 37 | self, 38 | msg: str, 39 | level: int = logging.INFO, 40 | created: float = None, 41 | ts: str = None, 42 | process: str = None, 43 | caller: str = None, 44 | process_id: int = None, 45 | selected: bool = False, 46 | extra: str = None, 47 | **kwargs, 48 | ): 49 | self.msg = msg 50 | self.level = level 51 | self.ts = ts 52 | self.created = created or to_timestamp(self.ts) if self.ts else time.time() 53 | self.process = process 54 | self.caller = caller 55 | self.process_id = process_id 56 | self.selected = selected 57 | self.extra = extra 58 | 59 | def __str__(self) -> str: 60 | text = ( 61 | f"level={self.level} " 62 | f"ts={format_datetime(datetime.datetime.fromtimestamp(self.created))} " 63 | f"msg=\"{self.msg}\"" 64 | ) 65 | return text 66 | 67 | def get_text(self) -> Text: 68 | level = LevelName(self.level).name 69 | color = LevelColorSelected[level].value if self.selected else LevelColor[level].value 70 | return Text( 71 | f"{format_datetime(from_timestamp(self.created))} " 72 | f"{LevelName(self.level).name:>8s}" 73 | f"{'*' if self.extra else ' '}" 74 | f"{self.caller[:20]:<20s} " 75 | f"{self.msg}", 76 | style=f"{color}" 77 | ) 78 | 79 | def __rich__(self) -> Text: 80 | color = LevelColor[LevelName(self.level).name].value 81 | return Text( 82 | f"{format_datetime(from_timestamp(self.created))} " 83 | f"{LevelName(self.level).name} " 84 | f"{self.msg}", 85 | style=f"{color}" 86 | ) 87 | 88 | 89 | def format_datetime(dt: Union[str, datetime.datetime] = None, fmt: str = None, width: int = 6, precision: int = 3): 90 | """Format a datetime as YYYY-mm-ddTHH:MM:SS.μs+0000. 91 | 92 | If the given argument is not timezone aware, the last part, i.e. `+0000` will not be there. 93 | 94 | If no argument is given, the timestamp is generated as 95 | `datetime.datetime.now(tz=datetime.timezone.utc)`. 96 | 97 | The `dt` argument can also be a string with the following values: today, yesterday, tomorrow, 98 | and 'day before yesterday'. The format will then be '%Y%m%d' unless specified. 99 | 100 | Optionally, a format string can be passed in to customize the formatting of the timestamp. 101 | This format string will be used with the `strftime()` method and should obey those conventions. 102 | 103 | Example: 104 | >>> format_datetime(datetime.datetime(2020, 6, 13, 14, 45, 45, 696138)) 105 | '2020-06-13T14:45:45.696' 106 | >>> format_datetime(datetime.datetime(2020, 6, 13, 14, 45, 45, 696138), precision=6) 107 | '2020-06-13T14:45:45.696138' 108 | >>> format_datetime(datetime.datetime(2020, 6, 13, 14, 45, 59, 999501), precision=3) 109 | '2020-06-13T14:45:59.999' 110 | >>> format_datetime(datetime.datetime(2020, 6, 13, 14, 45, 59, 999501), precision=6) 111 | '2020-06-13T14:45:59.999501' 112 | >>> _ = format_datetime() 113 | ... 114 | >>> format_datetime("yesterday") 115 | '20220214' 116 | >>> format_datetime("yesterday", fmt="%d/%m/%Y") 117 | '14/02/2022' 118 | 119 | Args: 120 | dt (datetime): a datetime object or an agreed string like yesterday, tomorrow, ... 121 | fmt (str): a format string that is accepted by `strftime()` 122 | width (int): the width to use for formatting the microseconds 123 | precision (int): the precision for the microseconds 124 | Returns: 125 | a string representation of the current time in UTC, e.g. `2020-04-29T12:30:04.862+0000`. 126 | 127 | Raises: 128 | A ValueError will be raised when the given dt argument string is not understood. 129 | """ 130 | dt = dt or datetime.datetime.now(tz=datetime.timezone.utc) 131 | if isinstance(dt, str): 132 | fmt = fmt or "%Y%m%d" 133 | if dt.lower() == "yesterday": 134 | dt = datetime.date.today() - datetime.timedelta(days=1) 135 | elif dt.lower() == "today": 136 | dt = datetime.date.today() 137 | elif dt.lower() == "day before yesterday": 138 | dt = datetime.date.today() - datetime.timedelta(days=2) 139 | elif dt.lower() == "tomorrow": 140 | dt = datetime.date.today() + datetime.timedelta(days=1) 141 | else: 142 | raise ValueError(f"Unknown date passed as an argument: {dt}") 143 | 144 | if fmt: 145 | timestamp = dt.strftime(fmt) 146 | else: 147 | width = min(width, precision) 148 | timestamp = ( 149 | f"{dt.strftime('%Y-%m-%dT%H:%M')}:" 150 | f"{dt.second:02d}.{dt.microsecond//10**(6-precision):0{width}d}{dt.strftime('%z')}" 151 | ) 152 | 153 | return timestamp 154 | 155 | 156 | def from_timestamp(ts: float): 157 | return datetime.datetime.fromtimestamp(ts) 158 | 159 | 160 | def to_timestamp(ts: str): 161 | return datetime.datetime.strptime(ts, '%Y-%m-%dT%H:%M:%S,%f').timestamp() 162 | 163 | 164 | if __name__ == "__main__": 165 | 166 | for level in LevelName: 167 | record = LogRecord(level=level.value, 168 | msg=f"A simple {level.name} message should be printed in {LevelColor[level.name].value}.") 169 | print(record) 170 | rich.print(record) 171 | -------------------------------------------------------------------------------- /src/textualog/renderables/namespace_tree.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from functools import lru_cache 5 | from typing import Union 6 | 7 | import rich 8 | from rich.console import RenderableType 9 | from rich.text import Text 10 | from textual import events 11 | from textual._types import MessageTarget 12 | from textual.message import Message 13 | from textual.reactive import Reactive 14 | from textual.widgets import NodeID 15 | from textual.widgets import TreeClick 16 | from textual.widgets import TreeControl 17 | from textual.widgets import TreeNode 18 | 19 | 20 | @dataclass 21 | class NamespaceEntry: 22 | name: dict 23 | is_parent: bool 24 | 25 | def __str__(self): 26 | return f"{type(self.name)=}, {self.name=}, {self.is_parent=}" 27 | 28 | 29 | @rich.repr.auto 30 | class EntryClick(Message, bubble=True): 31 | def __init__(self, sender: MessageTarget, key: Union[dict, str]) -> None: 32 | self.key = key 33 | super().__init__(sender) 34 | 35 | 36 | class NamespaceTree(TreeControl[NamespaceEntry]): 37 | def __init__(self, data: dict = None, name: str = None): 38 | label = "log" 39 | data = NamespaceEntry(data, True) 40 | super().__init__(label, name=name, data=data) 41 | self.root.tree.guide_style = "black" 42 | 43 | has_focus: Reactive[bool] = Reactive(False) 44 | mouse_over: Reactive[bool] = Reactive(False) 45 | style: Reactive[str] = Reactive("") 46 | height: Reactive[int | None] = Reactive(None) 47 | 48 | def on_focus(self) -> None: 49 | self.has_focus = True 50 | 51 | def on_blur(self) -> None: 52 | self.has_focus = False 53 | 54 | async def watch_hover_node(self, hover_node: NodeID) -> None: 55 | for node in self.nodes.values(): 56 | node.tree.guide_style = ( 57 | "bold not dim red" if node.id == hover_node else "black" 58 | ) 59 | self.refresh(layout=True) 60 | 61 | def render_node(self, node: TreeNode[NamespaceEntry]) -> RenderableType: 62 | return self.render_tree_label( 63 | node, 64 | node.data.is_parent, 65 | node.expanded, 66 | node.is_cursor, 67 | node.id == self.hover_node, 68 | self.has_focus, 69 | ) 70 | 71 | @lru_cache(maxsize=1024 * 32) 72 | def render_tree_label( 73 | self, 74 | node: TreeNode[NamespaceEntry], 75 | is_parent: bool, 76 | expanded: bool, 77 | is_cursor: bool, 78 | is_hover: bool, 79 | has_focus: bool, 80 | ) -> RenderableType: 81 | meta = { 82 | "@click": f"click_label({node.id})", 83 | "tree_node": node.id, 84 | "cursor": node.is_cursor, 85 | } 86 | label = Text(node.label) if isinstance(node.label, str) else node.label 87 | if is_hover: 88 | label.stylize("underline") 89 | if is_parent: 90 | label.stylize("bold magenta") 91 | icon = "▼︎" if expanded else "▶︎" 92 | else: 93 | label.stylize("bright_green") 94 | icon = "" 95 | label.highlight_regex(r"\..*$", "green") 96 | 97 | if label.plain.startswith("."): 98 | label.stylize("dim") 99 | 100 | if is_cursor and has_focus: 101 | label.stylize("reverse") 102 | 103 | icon_label = Text(f"{icon} ", no_wrap=True, overflow="ellipsis") + label 104 | icon_label.apply_meta(meta) 105 | return icon_label 106 | 107 | async def on_mount(self, event: events.Mount) -> None: 108 | await self.load_tree(self.root) 109 | 110 | async def load_tree(self, node: TreeNode[NamespaceEntry]): 111 | try: 112 | keys = sorted(node.data.name.keys()) 113 | except AttributeError: 114 | keys = [] 115 | for key in keys: 116 | is_dir = isinstance(node.data.name[key], dict) 117 | await node.add(key, NamespaceEntry(node.data.name[key], is_dir)) 118 | node.loaded = True 119 | await node.expand() 120 | self.refresh(layout=True) 121 | 122 | async def handle_tree_click(self, message: TreeClick[NamespaceEntry]) -> None: 123 | namespace_entry = message.node.data 124 | if not namespace_entry.is_parent: 125 | await self.emit(EntryClick(self, namespace_entry.name)) 126 | else: 127 | await self.emit(EntryClick(self, namespace_entry.name)) 128 | if not message.node.loaded: 129 | await self.load_tree(message.node) 130 | await message.node.expand() 131 | else: 132 | await message.node.toggle() 133 | -------------------------------------------------------------------------------- /src/textualog/renderables/shortcuts.py: -------------------------------------------------------------------------------- 1 | from rich.table import Table 2 | from textual.keys import Keys 3 | 4 | from textualog.unicodes import DOWN 5 | from textualog.unicodes import END 6 | from textualog.unicodes import HOME 7 | from textualog.unicodes import PAGE_DOWN 8 | from textualog.unicodes import PAGE_UP 9 | from textualog.unicodes import UP 10 | 11 | 12 | class Shortcuts: 13 | shortcuts = { 14 | "navigation": { 15 | "help": "?", 16 | "navigate": f"{UP} {DOWN} {HOME} {END} {PAGE_UP} {PAGE_DOWN}", 17 | "close dialog": Keys.Escape, 18 | "quit": f"{Keys.ControlC} or q", 19 | "Show Namespaces": "n", 20 | "Follow (reload)": "f", 21 | }, 22 | "Toggle Logging Levels": { 23 | "DEBUG mode": "d", 24 | "INFO mode": "i", 25 | "WARNING mode": "w", 26 | "ERROR mode": "e", 27 | "CRITICAL mode": "c", 28 | }, 29 | } 30 | 31 | def __str__(self) -> str: 32 | return str(self.shortcuts) 33 | 34 | def __rich__(self) -> Table: 35 | table = Table(box=None, expand=False, show_footer=False, show_header=False) 36 | table.add_column(style="magenta bold") 37 | table.add_column(style="yellow bold") 38 | for category, shortcuts in self.shortcuts.items(): 39 | table.add_row(f"[blue bold]{category}[/]") 40 | for action, shortcut in shortcuts.items(): 41 | table.add_row(f"{action}:", f"{shortcut}") 42 | table.add_row() 43 | 44 | return table 45 | -------------------------------------------------------------------------------- /src/textualog/styles.py: -------------------------------------------------------------------------------- 1 | from rich import box 2 | 3 | BORDER_FOCUSED = "green" 4 | BORDER_ERROR = "red" 5 | BORDER_MOUSE_OVER = "grey100" 6 | BORDER = "grey82" 7 | BOX = box.SQUARE 8 | TABLE_BOX = box.SIMPLE_HEAD 9 | -------------------------------------------------------------------------------- /src/textualog/system.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import time 4 | 5 | MODULE_LOGGER = logging.getLogger("Textual") 6 | 7 | 8 | def timer(*, level: int = logging.INFO, precision: int = 4): 9 | """ 10 | Print the runtime of the decorated function. 11 | 12 | Args: 13 | level: the logging level for the time message [default=INFO] 14 | precision: the number of decimals for the time [default=3 (ms)] 15 | """ 16 | 17 | def actual_decorator(func): 18 | @functools.wraps(func) 19 | def wrapper_timer(*args, **kwargs): 20 | start_time = time.perf_counter() 21 | value = func(*args, **kwargs) 22 | end_time = time.perf_counter() 23 | run_time = end_time - start_time 24 | MODULE_LOGGER.log(level, f"Finished {func.__name__!r} in {run_time:.{precision}f} secs") 25 | return value 26 | 27 | return wrapper_timer 28 | return actual_decorator 29 | 30 | 31 | def do_every(period: float, func: callable, *args) -> None: 32 | """ 33 | 34 | This method executes a function periodically, taking into account 35 | that the function that is executed will take time also and using a 36 | simple `sleep()` will cause a drift. This method will not drift. 37 | 38 | You can use this function in combination with the threading module to execute the 39 | function in the background, but be careful as the function might not be thread safe. 40 | 41 | ``` 42 | timer_thread = threading.Thread(target=do_every, args=(10, func)) 43 | timer_thread.daemon = True 44 | timer_thread.start() 45 | ``` 46 | 47 | Args: 48 | period: a time interval between successive executions [seconds] 49 | func: the function to be executed 50 | *args: optional arguments to be passed to the function 51 | """ 52 | 53 | # Code from SO:https://stackoverflow.com/a/28034554/4609203 54 | # The max in the yield line serves to protect sleep from negative numbers in case the 55 | # function being called takes longer than the period specified. In that case it would 56 | # execute immediately and make up the lost time in the timing of the next execution. 57 | 58 | def g_tick(): 59 | next_time = time.time() 60 | while True: 61 | next_time += period 62 | yield max(next_time - time.time(), 0) 63 | 64 | g = g_tick() 65 | while True: 66 | time.sleep(next(g)) 67 | func(*args) 68 | -------------------------------------------------------------------------------- /src/textualog/unicodes.py: -------------------------------------------------------------------------------- 1 | APPROXIMATION = "\u2248" 2 | UP = "\u2191" 3 | DOWN = "\u2193" 4 | LEFT = "\u2190" 5 | RIGHT = "\u2192" 6 | HOME = "\u2196" 7 | END = "\u2198" 8 | PAGE_DOWN = "\u21A7" 9 | PAGE_UP = "\u21A5" 10 | RIGHT_TRIANGLE = "\u25B6" 11 | BIG_RIGHT_TRIANGLE = "\uE0B0" 12 | DOWN_TRIANGLE = "\u25BC" 13 | DIAMOND = "\u25C8" 14 | -------------------------------------------------------------------------------- /src/textualog/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhuygen/textualog/c3f5f22d37745e40e5331ca345bffa0ef2beadf8/src/textualog/widgets/__init__.py -------------------------------------------------------------------------------- /src/textualog/widgets/details.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from rich.panel import Panel 4 | from rich.text import Text 5 | from textual.widget import Widget 6 | from textual.reactive import Reactive 7 | 8 | from textualog import styles 9 | from textualog.renderables.logrecord import LogRecord 10 | 11 | 12 | class Details(Widget): 13 | 14 | record: Reactive[LogRecord] = Reactive(None) 15 | 16 | def __init__(self): 17 | super().__init__() 18 | self.record: Optional[LogRecord] = None 19 | 20 | def set(self, record: LogRecord): 21 | self.record = record 22 | 23 | def render(self) -> Panel: 24 | return Panel( 25 | self._generate_renderable(), 26 | title="[bold]Record Details[/]", 27 | border_style=styles.BORDER_FOCUSED, 28 | box=styles.BOX, 29 | title_align="left", 30 | padding=0, 31 | ) 32 | 33 | def _generate_renderable(self) -> Text: 34 | 35 | if self.record is None: 36 | return Text() 37 | 38 | record = Text(no_wrap=True) 39 | record.append(f"level = {self.record.level}\n") 40 | record.append(f"process = {self.record.process}\n") 41 | record.append(f"process ID = {self.record.process_id}\n") 42 | record.append(f"caller = {self.record.caller}\n") 43 | record.append(f"msg = {self.record.msg}\n") 44 | record.append(f"extra = {self.record.extra}\n") 45 | 46 | return record 47 | -------------------------------------------------------------------------------- /src/textualog/widgets/footer.py: -------------------------------------------------------------------------------- 1 | from rich.align import Align 2 | from rich.columns import Columns 3 | from rich.padding import Padding 4 | from textual.reactive import Reactive 5 | from textual.widgets import Footer 6 | 7 | 8 | class Footer(Footer): 9 | 10 | log_size = Reactive(0) 11 | log_offset = Reactive(0) 12 | 13 | def on_mount(self) -> None: 14 | self.layout_size = 1 15 | 16 | def render(self) -> Columns: 17 | log_size_text = Align.right( 18 | Padding( 19 | f"at {self.log_offset} in [bold]{self.log_size}[/] lines", pad=(0, 1, 0, 1), 20 | style="white on dark_green", 21 | expand=False, 22 | ) 23 | ) 24 | 25 | return Columns([super().render(), log_size_text], expand=True) 26 | -------------------------------------------------------------------------------- /src/textualog/widgets/help.py: -------------------------------------------------------------------------------- 1 | from rich.panel import Panel 2 | from textual.widget import Widget 3 | 4 | from textualog import styles 5 | from textualog.emojis import INFO 6 | from textualog.renderables.shortcuts import Shortcuts 7 | 8 | 9 | class Help(Widget): 10 | def render(self) -> Panel: 11 | return Panel( 12 | Shortcuts(), 13 | title=f"{INFO} [bold]help[/]", 14 | border_style=styles.BORDER_FOCUSED, 15 | box=styles.BOX, 16 | title_align="left", 17 | padding=0, 18 | ) 19 | -------------------------------------------------------------------------------- /src/textualog/widgets/levels.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from rich.panel import Panel 4 | from rich.table import Table 5 | from textual.reactive import Reactive 6 | from textual.widget import Widget 7 | 8 | from textualog import styles 9 | from textualog.emojis import CHECK 10 | from textualog.emojis import UNCHECK 11 | 12 | PANEL_SIZE = 5 13 | 14 | 15 | class Levels(Widget): 16 | 17 | debug_level: Reactive = Reactive(True) 18 | info_level: Reactive = Reactive(True) 19 | warning_level: Reactive = Reactive(True) 20 | error_level: Reactive = Reactive(True) 21 | critical_level: Reactive = Reactive(True) 22 | 23 | async def on_mount(self) -> None: 24 | self.layout_size = PANEL_SIZE 25 | 26 | def render(self) -> Panel: 27 | table = Table(box=None, expand=False, show_header=False, show_edge=False) 28 | table.add_column() 29 | table.add_column() 30 | 31 | table.add_row(CHECK if self.debug_level else UNCHECK, "DEBUG") 32 | table.add_row(CHECK if self.info_level else UNCHECK, "INFO") 33 | table.add_row(CHECK if self.warning_level else UNCHECK, "WARNING") 34 | table.add_row(CHECK if self.error_level else UNCHECK, "ERROR") 35 | table.add_row(CHECK if self.critical_level else UNCHECK, "CRITICAL") 36 | 37 | panel = Panel( 38 | table, 39 | title="[bold]Levels[/]", 40 | border_style=styles.BORDER, 41 | box=styles.BOX, 42 | title_align="left", 43 | padding=0, 44 | ) 45 | 46 | return panel 47 | 48 | def is_on(self, level: int): 49 | if level == logging.DEBUG: 50 | return self.debug_level 51 | elif level == logging.INFO: 52 | return self.info_level 53 | elif level == logging.WARNING: 54 | return self.warning_level 55 | elif level == logging.ERROR: 56 | return self.error_level 57 | elif level == logging.CRITICAL: 58 | return self.critical_level 59 | return False 60 | 61 | def __str__(self): 62 | return ( 63 | f"DEBUG={'ON' if self.debug_level else 'OFF'}, " 64 | f"INFO={'ON' if self.info_level else 'OFF'}, " 65 | f"WARNING={'ON' if self.warning_level else 'OFF'}, " 66 | f"ERROR={'ON' if self.error_level else 'OFF'}, " 67 | f"CRITICAL={'ON' if self.critical_level else 'OFF'}" 68 | ) 69 | -------------------------------------------------------------------------------- /src/textualog/widgets/namespaces.py: -------------------------------------------------------------------------------- 1 | from rich.console import RenderableType 2 | from rich.panel import Panel 3 | 4 | from textualog import styles 5 | from textualog.renderables.namespace_tree import NamespaceTree 6 | 7 | 8 | class Namespaces(NamespaceTree): 9 | def __init__(self, name=None, data: dict = None): 10 | self.tree: dict = data 11 | super().__init__(name=name, data=data) 12 | 13 | def render(self) -> RenderableType: 14 | panel = Panel( 15 | super().render(), 16 | title="[bold]Namespaces[/]", 17 | border_style=styles.BORDER, 18 | box=styles.BOX, 19 | title_align="left", 20 | padding=0, 21 | ) 22 | return panel 23 | -------------------------------------------------------------------------------- /src/textualog/widgets/recordinfo.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional 4 | 5 | from rich.panel import Panel 6 | from rich.text import Text 7 | from textual import events 8 | from textual.reactive import Reactive 9 | from textual.widget import Widget 10 | 11 | from .. import styles 12 | from ..renderables.logrecord import LogRecord 13 | 14 | PANEL_SIZE = 5 15 | 16 | 17 | class RecordInfo(Widget): 18 | 19 | record: Reactive[LogRecord] = Reactive(None) 20 | 21 | def __init__(self, height: int | None = None): 22 | super().__init__() 23 | self.record: Optional[LogRecord] = None 24 | 25 | async def on_mount(self) -> None: 26 | # self.layout_size = PANEL_SIZE 27 | # self.layout_fraction = 1 28 | ... 29 | 30 | async def on_click(self, event: events.Click) -> None: 31 | self.app.show_details = True 32 | 33 | def set(self, record: LogRecord): 34 | self.record = record 35 | self.app.details_widget.set(self.record) 36 | 37 | def render(self) -> Panel: 38 | 39 | return Panel( 40 | self._generate_renderable(), 41 | title=f"[bold]Record Info" 42 | f"{' *' if self.record is not None and self.record.extra else ''}" 43 | f"[/]", 44 | border_style=styles.BORDER_FOCUSED, 45 | box=styles.BOX, 46 | title_align="left", 47 | padding=0, 48 | # height=PANEL_SIZE, 49 | ) 50 | 51 | def _generate_renderable(self) -> Text: 52 | 53 | if self.record is None: 54 | return Text() 55 | 56 | record = Text(no_wrap=True) 57 | record.append(f"level = {self.record.level}\n") 58 | record.append(f"process = {self.record.process}\n") 59 | record.append(f"process ID = {self.record.process_id}\n") 60 | record.append(f"caller = {self.record.caller}\n") 61 | record.append(f"msg = {self.record.msg}\n") 62 | 63 | return record 64 | -------------------------------------------------------------------------------- /src/textualog/widgets/records.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | from typing import List 5 | from typing import Optional 6 | 7 | from rich.console import ConsoleRenderable 8 | from rich.panel import Panel 9 | from rich.text import Text 10 | from textual import events 11 | from textual.reactive import Reactive 12 | from textual.widget import Widget 13 | 14 | from .. import styles 15 | from ..renderables.logrecord import LogRecord 16 | 17 | PANEL_SIZE = 10 18 | 19 | 20 | class Records(Widget): 21 | 22 | height: Reactive[int | None] = Reactive(None) 23 | 24 | def __init__(self, height: int | None = None): 25 | super().__init__() 26 | self.height = height 27 | self.records: List[LogRecord] = [ 28 | # LogRecord(level=logging.INFO, 29 | # msg="The log messages will be displayed here as a list or table.") 30 | ] 31 | self._selected_idx: Optional[int] = None 32 | 33 | async def on_mount(self) -> None: 34 | # self.layout_size = PANEL_SIZE 35 | # self.layout_fraction = 1 36 | ... 37 | 38 | async def on_click(self, event: events.Click) -> None: 39 | 40 | if self._selected_idx is not None: 41 | self.records[self._selected_idx].selected = False 42 | 43 | idx = event.y - 1 # Records is a Panel with the header as the first line 44 | 45 | with contextlib.suppress(IndexError): 46 | record = self.records[idx] 47 | self.app.record_info.set(record) 48 | record.selected = True 49 | self._selected_idx = idx 50 | self.refresh(repaint=True) 51 | 52 | def render(self) -> Panel: 53 | return Panel( 54 | self._generate_renderable(), 55 | title="[bold]Records[/]", 56 | border_style=styles.BORDER_FOCUSED, 57 | box=styles.BOX, 58 | title_align="left", 59 | padding=0, 60 | height=self.height, 61 | ) 62 | 63 | def replace(self, records: List[LogRecord]): 64 | self.records = records 65 | self._selected_idx = None 66 | 67 | def update(self, records: List[LogRecord]): 68 | self.records.extend(records) 69 | self._selected_idx = None 70 | 71 | def _generate_renderable(self) -> ConsoleRenderable: 72 | text = Text(no_wrap=True) 73 | [ 74 | text.append(record.get_text()).append("\n") 75 | for record in self.records 76 | ] 77 | return text 78 | -------------------------------------------------------------------------------- /textualog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhuygen/textualog/c3f5f22d37745e40e5331ca345bffa0ef2beadf8/textualog.png --------------------------------------------------------------------------------