Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve logging #2

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ This repository contains software for remote control and monitoring of high volt
- Register of voltage and current monitor values of the channels of each device.
- Automatic multidevice raising of voltages and turning off following the standard protocol (raising or lowering all channels involved voltages simultaneously by steps).
- Trip recovery system to automatically detect, handle and recover a trip. It uses the multidevice raising of voltages to recover a trip. Also, a configurable cooldown time is applied before recovering the trip.
- Alert message to slack webhook when the CAEN device is in alarm state (to do so, copy your slack webhook in the function `send_slack_message` of [caengui.py](caengui.py)).
- Alert message to slack webhook (to do so, copy your slack webhook in the global variable SLACK_WEBHOOK_URL of [logger.py](logger.py)). You can select the logging level os the slack messages in the config menu bar. These are the logging levels logic:
* CRITICAL: unexpected error happens which require the user to fix.
* ERROR: expected error happens which require the users attention.
* WARNING: expected event as trips.
* INFO: information on the normal functioning of the program.
* DEBUG: debugging information.

It is recommended to set the slack logging level to warning if you want to receive messages when trips happen or error if you want to ignore the messages of trips.
- DAQ monitoring through the [feminos-daq](https://github.com/rest-for-physics/feminos-daq) prometheus metrics. As the current DAQ computer is different from the slow-control PC, an ssh connection is established. Make sure to have the necessary ssh key-pair user credentials installed (on the DAQ PC) for the SSH key-based authentication.
- Button to add the current run information (run number, date and voltages) to the Google Sheet run list. To configure the connection to the Google Sheet you should change the global variables at `utils.py`. Make sure to have the appropiate google service account credentials (json file) in the root directory.
- Auto and manual button to add the current run information (run number, run type, metadata in the output file name, voltages and electronic threshold in the .run file) to the Google Sheet run list. To configure the connection to the Google Sheet you should change the global variables at `utils.py`. Make sure to have the appropiate google service account credentials (json file) in the root directory.
- CAEN and Spellman SL30 simulators for testing without hardware.

## Usage
Expand Down Expand Up @@ -48,12 +55,12 @@ This repository contains software for remote control and monitoring of high volt
- `caengui.py`: GUI for CAEN HV devices.
- `spellmangui.py`: GUI for Spellman HV devices.
- `checksframe.py`: Implementation of the ChecksFrame class to display and manage the checks.
- `utilsgui.py`: Implementation of GUI utility classes such as ToolTip and PrintLogger.
- `utilsgui.py`: Implementation of GUI utility classes such as ToolTip and PrintToTextWidget.
- Device modules
- `spellmanClass.py`: Class for managing the Spellman HV supply.
- `simulators.py`: CAEN and Spellman device simulator classes.
- Support modules
- `check.py`: Implementation of the checks classes.
- `logger.py`: Implementation of the ChannelState class.
- `logger.py`: Implementation of the ChannelState class and logging helper functions and classes.
- `metrics_fetcher.py`: Implementation of MetricsFetcher and MetricsFetchcerSSH to extract the prometheus metrics of the [feminos-daq](https://github.com/rest-for-physics/feminos-daq) acquisition program.
- `utils.py`: Other useful functions. For now, it includes the necessary functions for adding rows to the Google Sheet run list.
41 changes: 22 additions & 19 deletions caengui.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
from check import Check
from checkframe import ChecksFrame
from utilsgui import ToolTip
from logger import ChannelState, LOG_DIR
from devicegui import DeviceGUI
from utils import send_slack_message

class CaenHVPSGUI(DeviceGUI):
def __init__(self, module, channel_names=None, checks=None, parent_frame=None, log=True, silence=False):
Expand Down Expand Up @@ -55,7 +53,10 @@ def __init__(self, module, channel_names=None, checks=None, parent_frame=None, l
if i >= len(channel_names):
channel_names[i] = f"Channel {i}"

super().__init__(module, channel_names, parent_frame, logging_enabled=log)
super().__init__(module, channel_names, parent_frame,
logging_enabled=log,
channel_state_save_previous=False,
)


def create_gui(self):
Expand Down Expand Up @@ -407,7 +408,7 @@ def values_from_description(description_: str | dict) -> list[str]:
cancel_button.grid(row=len(properties), column=0, padx=10, pady=10, sticky="e")

def apply_changes():
print("Setting:")
self.logger.debug("Setting:")
for p, entry in entries.items():
if entry.winfo_class() == "Menubutton":
value = entry.cget("text")
Expand All @@ -418,9 +419,8 @@ def apply_changes():
except ValueError:
pass
setattr(ch, p, value)
print(f" {p}\t-> {value}")
self.logger.debug(f" {p}\t-> {value}")
new_window.destroy()
print()

apply_button = tk.Button(
new_window,
Expand Down Expand Up @@ -497,7 +497,7 @@ def turn_off_multichannel(self):

def clear_alarm(self):
self.device.clear_alarm_signal()
self.alarm_detected = False
self.alarm_detected = ""

def toggle_channel(self, channel_number):
ch = self.device.channels[channel_number]
Expand Down Expand Up @@ -580,36 +580,39 @@ def update_alarm_indicators(self):

if any([v for k, v in bas.items()]):
if not self.alarm_detected:
self.alarm_detected = True
message = ""
for k, v in bas.items():
if v:
message += f"{k}"
if 'CH' in k:
message += f" ({self.channels_name[int(k[-1])]})"
message += ", "
self.alarm_detected = message
self.action_when_alarm()
else:
self.alarm_detected = False
self.alarm_detected = ""

if ilk:
if not self.ilk_detected:
self.ilk_detected = True
self.ilk_detected = "ILK"
self.action_when_interlock()
else:
self.ilk_detected = False
self.ilk_detected = ""

def action_when_alarm(self, board_alarm_status = None):
if board_alarm_status is None:
board_alarm_status = self.device.board_alarm_status.copy()
message = f"Alarm detected in module {self.device.name}:\n"
message = f"Alarm detected in module {self.device.name}:"
for k, v in board_alarm_status.items():
if v:
message += f" {k}"
message += f" {k}"
if 'CH' in k:
message += f" ({self.channels_name[int(k[-1])]})"
print(message)
if not self.silence_alarm:
threading.Thread(target=send_slack_message, args=(message,)).start() # to avoid blocking the GUI (it can be slow)
self.logger.warning(message)

def action_when_interlock(self):
message = f"Interlock detected in module {self.device.name}."
print(message)
if not self.silence_alarm:
threading.Thread(target=send_slack_message, args=(message,)).start() # to avoid blocking the GUI (it can be slow)
self.logger.warning(message)


if __name__ == "__main__":
Expand Down
12 changes: 11 additions & 1 deletion devicegui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import queue
import threading
import time
import logging
from abc import ABC, abstractmethod

from logger import ChannelState
from logger import ChannelState, configure_basic_logger
from utilsgui import validate_numeric_entry_input

class DeviceGUI(ABC):
Expand Down Expand Up @@ -80,6 +81,15 @@ def __init__(self, device, channels_name: list, parent_frame=None, **kwargs):
self.command_queue = queue.Queue()
self.device_lock = threading.Lock()

#Initialize logger
logger_name = f"app.{self.device.name}"
self.logger = logging.getLogger(logger_name)
if self.logger.parent.name == "root": # if it is not embedded in another GUI with its own logger
self.logger = configure_basic_logger(logger_name)
else:
pass # use the logger from the parent GUI (because it propagates)

# Create GUI
self.create_gui()
self.start_background_threads()

Expand Down
182 changes: 178 additions & 4 deletions logger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,181 @@
import datetime as dt
import os
import logging
import queue
import threading
import requests
import json

LOG_DIR = "logs"
SLACK_WEBHOOK_URL = "" # add here the webkook url

def create_directory_recursive(path):
try:
directory = os.path.dirname(path)
os.makedirs(directory, exist_ok=True)
except Exception as e:
print(f"Error occurred while creating directory '{path}': {e}")

def get_path_from_date(dt_obj):
return LOG_DIR + "/" + dt_obj.strftime("%Y/%m/%d")
def get_full_filename_from_date(dt_obj, suffix="", extension="dat"):
path = get_path_from_date(dt_obj)
return f"{path}/{dt_obj.strftime('%Y%m%d')}_{suffix}.{extension}"

class ThreadedHandler(logging.Handler):
def __init__(self):
self.log_queue = queue.Queue()
super().__init__()
self.worker = threading.Thread(target=self._process_queue)
self.worker.daemon = True # Ensures the thread exits with the main program
self.worker.start()

def emit(self, record):
# Add the log record to the queue
self.log_queue.put(self.format(record))

def logging_logic(self, log_message):
raise NotImplementedError

def _process_queue(self):
while True:
log_message = self.log_queue.get()
if log_message is None: # Sentinel to shut down the thread
break
self.logging_logic(log_message)

def close(self):
self.log_queue.put(None) # Send sentinel
self.worker.join() # Wait for the thread to finish
super().close()

class SlackHandler(ThreadedHandler):
def __init__(self, webhook_url:str):
super().__init__()
self.webhook_url = webhook_url

def logging_logic(self, message):
try:
# Enviar mensaje a Slack
slack_data = {'text': message}
requests.post(self.webhook_url, data=json.dumps(slack_data), headers={'Content-Type': 'application/json'})
except Exception as e:
print(e)
"""
def __repr__(self):
return f"{self.__class__.__name__}({self.webhook_url})"
"""

# Custom handler for logging to a Text widget
class TextWidgetHandler(logging.Handler):
def __init__(self, text_widget):
super().__init__()
self.text_widget = text_widget

def emit(self, record):
log_entry = self.format(record)
# use after() to avoid segmentation faults
if self.text_widget:
self.text_widget.after(0, self._write_log, log_entry)

def _write_log(self, log_entry):
self.text_widget.configure(state="normal")
self.text_widget.insert("end", log_entry + "\n")
self.text_widget.configure(state="disabled")
self.text_widget.see("end")
"""
def __repr__(self):
return f"{self.__class__.__name__}({self.text_widget})"
"""

def configure_basic_logger(logger_name:str, log_level=logging.DEBUG):
logger = logging.getLogger(logger_name)
logger.setLevel(log_level)

logger = configure_slack_logger(
logger_name,
log_filename=f"{LOG_DIR}/slack_{logger_name}.log",
slack_webhook_url=SLACK_WEBHOOK_URL,
log_level=logging.WARNING
)
logger = configure_streamer_logger(
logger_name,
log_filename=f"{LOG_DIR}/stream_{logger_name}.log",
log_level=logging.DEBUG
)

return logger

def configure_slack_logger(logger_name:str, log_filename:str, slack_webhook_url:str, log_level=logging.ERROR):
logger = logging.getLogger(logger_name)
# logger.setLevel(logging.DEBUG)
if slack_webhook_url:
slack_handler = SlackHandler(slack_webhook_url)
slack_handler.setLevel(log_level)
file_slack_handler = logging.FileHandler(log_filename)
file_slack_handler.setFormatter(logging.Formatter('%(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
file_slack_handler.setLevel(slack_handler.level)
logger.addHandler(slack_handler)
logger.addHandler(file_slack_handler)
return logger

def configure_streamer_logger(logger_name:str, text_widget=None, log_filename:str=None, log_level=logging.DEBUG):
logger = logging.getLogger(logger_name)
# logger.setLevel(logging.DEBUG)
if text_widget:
text_handler = TextWidgetHandler(text_widget)
text_handler.setLevel(log_level)
logger.addHandler(text_handler)
else:
stream_handler = logging.StreamHandler()
stream_handler.setLevel(log_level)
logger.addHandler(stream_handler)

if log_filename:
file_handler = logging.FileHandler(log_filename)
file_handler.setFormatter(logging.Formatter('%(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(file_handler)
return logger

def get_children_loggers(parent_name, include_parent=False):
"""
Get all direct child loggers of a given logger name. Method made for compatibility with Python<3.12
which does not have the logging.Logger.getChildren() method.

:param parent_name: The name of the parent logger.
:param include_parent: Whether to include the parent logger in the list of children.
:return: A list of child logger names.
"""
parent_logger = logging.getLogger(parent_name)
children = []
if include_parent:
children.append(parent_logger)
try:
c = parent_logger.getChildren() # Python>=3.12
children.extend(c)
except AttributeError:
all_loggers = logging.Logger.manager.loggerDict
children_names = [
name for name in all_loggers
if name.startswith(f"{parent_name}.") and name != parent_name
]
c = {logging.getLogger(name) for name in children_names}
children.extend(c)

return children


def get_level_names():
"""
Retrieve all logging level names from the `logging` module. Method made for compatibility with Python<3.11

:return: A list of level names.
"""
try:
return list(logging.getLevelNamesMapping().keys()) # Python>=3.11
except AttributeError:
return list(logging._nameToLevel.keys())
return []

class State:
def __init__(self, vmon=0, imon=0, stat=None):
Expand All @@ -26,6 +200,7 @@ def print_state(self):
print("vmon: {:.2f}V, imon: {:.2f}uA, stat: {}".format(self.vmon, self.imon, self.stat))

def write_to_file(self, filename, delimiter=' ', precision_vmon=1, precision_imon=3):
create_directory_recursive(filename)
if not os.path.isfile(filename):
try:
# create the file if it does not exist
Expand Down Expand Up @@ -64,8 +239,6 @@ def __init__(self, name="", ch=None, diff_vmon=0.5, diff_imon=0.01, precision_vm
self.precision_vmon = precision_vmon
self.precision_imon = precision_imon

self.filename = LOG_DIR + "/Log_" + self.channel_name.replace(" ", "") + ".dat"

def __str__(self):
return self.channel_name + ": " + str(self.current)

Expand Down Expand Up @@ -96,10 +269,11 @@ def is_different(self):
return (abs(self.current.vmon - self.last_saved.vmon) >= self.diff_vmon) or (abs(self.current.imon - self.last_saved.imon) >= self.diff_imon)

def save_state(self, force=False, save_previous=True):
filename = get_full_filename_from_date(self.current.time, suffix=self.channel_name.replace(" ", ""))
if self.is_different() or force:
if self.last_saved != self.previous and save_previous:
self.previous.write_to_file(self.filename, precision_vmon=self.precision_vmon, precision_imon=self.precision_imon)
self.current.write_to_file(self.filename, precision_vmon=self.precision_vmon, precision_imon=self.precision_imon)
self.previous.write_to_file(filename, precision_vmon=self.precision_vmon, precision_imon=self.precision_imon)
self.current.write_to_file(filename, precision_vmon=self.precision_vmon, precision_imon=self.precision_imon)
self.last_saved.assign(self.current)


Loading