Skip to content
Snippets Groups Projects
Commit a2492532 authored by Karim Ahmed's avatar Karim Ahmed
Browse files

feat: store logs in JSON format and split in different files for job_id

parent c8a0cc7c
No related branches found
No related tags found
1 merge request!1059[webservice] Intoduce Global Logger for xfel-calibrate
...@@ -26,6 +26,7 @@ echo "hostname: $(hostname)" ...@@ -26,6 +26,7 @@ echo "hostname: $(hostname)"
export CAL_NOTEBOOK_NAME="$notebook" export CAL_NOTEBOOK_NAME="$notebook"
export CAL_WORKING_DIR="$(pwd)" export CAL_WORKING_DIR="$(pwd)"
export SLURM_JOB_ID="${SLURM_JOB_ID:-local}"
# This is created by calibrate.py in CAL_WORKING_DIR # This is created by calibrate.py in CAL_WORKING_DIR
setup_logger="setup-logging-nb.py" setup_logger="setup-logging-nb.py"
......
%% Cell type:code id:98e38fec tags: %% Cell type:code id:98e38fec tags:
``` python ``` python
in_folder = "./" # input folder in_folder = "./" # input folder
out_folder = "./" # output folder out_folder = "./" # output folder
``` ```
%% Cell type:code id:7fbce574 tags: %% Cell type:code id:7fbce574 tags:
``` python ``` python
from logging import debug, error, info, warning import logging
``` ```
%% Cell type:markdown id:ba0663ca tags: %% Cell type:markdown id:ba0663ca tags:
## INFO/DEBUG ## INFO/DEBUG
%% Cell type:code id:a3b5e455 tags: %% Cell type:code id:a3b5e455 tags:
``` python ``` python
debug("(DEBUG)ing for some logs") logging.info("Logging some (INFO)rmation")
info("Logging some (INFO)rmation")
``` ```
%% Cell type:markdown id:1bf72c57 tags: %% Cell type:markdown id:1bf72c57 tags:
## WARNINGS ## WARNINGS
%% Cell type:code id:c0d290a1 tags: %% Cell type:code id:c0d290a1 tags:
``` python ``` python
from warnings import warn import warnings
class TestUserWarning(UserWarning):
"""Base class for custom user warnings"""
pass
def warn_user(message, warning_class):
warnings.warn(message, warning_class)
if 1 < 2: if 1 < 2:
warn('This inequality is true!') warn_user('This inequality is true!', TestUserWarning)
``` ```
%% Cell type:code id:9b79ea47 tags: %% Cell type:code id:9b79ea47 tags:
``` python ``` python
warning("This is a warning message using logging standard library.") logging.warning("This is a warning message using logging standard library.")
``` ```
%% Cell type:markdown id:2b22e2e0 tags: %% Cell type:markdown id:2b22e2e0 tags:
## ERRORS ## ERRORS
%% Cell type:code id:0f3bfeb7 tags: %% Cell type:code id:0f3bfeb7 tags:
``` python ``` python
error("Logging some (ERROR) without failing the notebook") logging.error("Logging some (ERROR) without failing the notebook")
``` ```
%% Cell type:code id:c3b87719 tags: %% Cell type:code id:c3b87719 tags:
``` python ``` python
raise ValueError('FAIL') raise ValueError('FAIL')
``` ```
%% Output %% Output
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
ValueError Traceback (most recent call last) ValueError Traceback (most recent call last)
Cell In[2], line 1 Cell In[2], line 1
----> 1 raise ValueError('FAIL') ----> 1 raise ValueError('FAIL')
ValueError: FAIL ValueError: FAIL
......
...@@ -92,6 +92,7 @@ install_requires = [ ...@@ -92,6 +92,7 @@ install_requires = [
"pymunge==0.1.3", "pymunge==0.1.3",
"pypandoc==1.13", "pypandoc==1.13",
"python-dateutil==2.8.2", "python-dateutil==2.8.2",
"python-json-logger==2.0.7"
"pyyaml==6.0.1", "pyyaml==6.0.1",
"pyzmq==25.1.2", "pyzmq==25.1.2",
"requests==2.29.0", "requests==2.29.0",
......
...@@ -3,49 +3,96 @@ import os ...@@ -3,49 +3,96 @@ import os
import sys import sys
import traceback import traceback
import warnings import warnings
import IPython import IPython
import json
from pythonjsonlogger import jsonlogger
NOTEBOOK_NAME = os.getenv('CAL_NOTEBOOK_NAME', 'Unknown notebook')
CURRENT_WORKING_DIR = os.getenv(
'CAL_WORKING_DIR', 'Unknown directory')
JOB_ID = os.getenv('SLURM_JOB_ID', 'local')
class ContextFilter(logging.Filter): class ContextFilter(logging.Filter):
def filter(self, record): def filter(self, record):
record.notebook = NOTEBOOK_NAME record.notebook = NOTEBOOK_NAME
record.directory = CURRENT_WORKING_DIR record.directory = CURRENT_WORKING_DIR
record.job_id = JOB_ID
return True return True
# Define a custom log format class CustomJsonFormatter(jsonlogger.JsonFormatter):
formatter = logging.Formatter( def add_fields(self, log_record, record, message_dict):
'%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - ' super(CustomJsonFormatter, self).add_fields(
'[Notebook: %(notebook)s] - [Directory: %(directory)s] - %(message)s' log_record, record, message_dict)
) log_record['timestamp'] = self.formatTime(record, self.datefmt)
log_record['level'] = record.levelname
log_record['filename'] = record.filename
log_record['lineno'] = record.lineno
log_record['class'] = getattr(record, 'class', 'DefaultClass')
if record.exc_info:
log_record['exc_info'] = self.formatException(record.exc_info)
class JSONArrayHandler(logging.FileHandler):
def __init__(self, filename, mode='a', encoding=None, delay=False):
super().__init__(filename, mode, encoding, delay)
self.file_empty = True
def emit(self, record):
try:
msg = self.format(record)
json_msg = json.loads(msg)
with self.lock:
if self.stream is None:
self.stream = self._open()
if self.file_empty:
self.stream.write('[\n')
self.file_empty = False
else:
self.stream.write(',\n')
json.dump(json_msg, self.stream, indent=2)
self.stream.flush()
except Exception:
self.handleError(record)
def close(self):
if self.stream is not None:
with self.lock:
if not self.file_empty:
self.stream.write('\n]')
super().close()
# Retrieve notebook name and current working directory from
# environment variables
NOTEBOOK_NAME = os.getenv('CAL_NOTEBOOK_NAME', 'Unknown notebook')
CURRENT_WORKING_DIR = os.getenv('CAL_WORKING_DIR', 'Unknown directory')
# Create a logger # Create a logger
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # Capture all levels logger.setLevel(logging.INFO)
# Create file handlers # Define a custom JSON format
error_handler = logging.FileHandler('errors.log') formatter = CustomJsonFormatter(
error_handler.setLevel(logging.ERROR) '%(timestamp)s %(level)s %(filename)s %(lineno)d '
error_handler.setFormatter(formatter) '%(notebook)s %(directory)s %(job_id)s %(class)s %(message)s')
warning_handler = logging.FileHandler('warnings.log')
warning_handler.setLevel(logging.WARNING)
# Avoid errors being logged in warnings.log
warning_handler.addFilter(lambda record: record.levelno < logging.ERROR)
warning_handler.setFormatter(formatter)
# In case any info is logged and for logged data # Function to create a file handler with job-specific JSON log file
# by other packages like calibration_client. def create_job_specific_handler(log_level, file_suffix):
info_handler = logging.FileHandler('info.log') log_file = f'{file_suffix}_{JOB_ID}.json'
info_handler.setLevel(logging.DEBUG) handler = JSONArrayHandler(log_file, mode='a+', delay=True)
info_handler.setFormatter(formatter) handler.setLevel(log_level)
handler.setFormatter(formatter)
return handler
# Create job-specific file handlers
error_handler = create_job_specific_handler(logging.ERROR, 'errors')
warning_handler = create_job_specific_handler(logging.WARNING, 'warnings')
info_handler = create_job_specific_handler(logging.DEBUG, 'info')
# Avoid errors being logged in warnings.json
warning_handler.addFilter(lambda record: record.levelno < logging.ERROR)
# Add the custom filter to handlers # Add the custom filter to handlers
context_filter = ContextFilter() context_filter = ContextFilter()
...@@ -68,20 +115,20 @@ def safe_handle_error(exc_type, exc_value, exc_traceback): ...@@ -68,20 +115,20 @@ def safe_handle_error(exc_type, exc_value, exc_traceback):
traceback.print_exception( traceback.print_exception(
exc_type, exc_value, exc_traceback, file=sys.stderr) exc_type, exc_value, exc_traceback, file=sys.stderr)
return return
handling_error = True handling_error = True
try: try:
# Log the error with the notebook name and additional metadata # Log the error with the notebook name, job ID, and additional metadata
logger.error( logger.error(
"An error occurred. Exception type: %s, Message: %s", "An error occurred. Exception type: %s, Message: %s",
exc_type.__name__, str(exc_value), exc_type.__name__, str(exc_value),
extra={ extra={
'notebook': NOTEBOOK_NAME, 'notebook': NOTEBOOK_NAME,
'directory': CURRENT_WORKING_DIR 'directory': CURRENT_WORKING_DIR,
'job_id': JOB_ID,
'class': exc_type.__name__ if exc_type else 'DefaultErrorClass', # noqa
}, },
exc_info=(exc_type, exc_value, exc_traceback) exc_info=(exc_type, exc_value, exc_traceback)
) )
except Exception as log_error: except Exception as log_error:
sys.stderr.write(f"Logging failed: {log_error}\n") sys.stderr.write(f"Logging failed: {log_error}\n")
traceback.print_exception( traceback.print_exception(
...@@ -92,11 +139,15 @@ def safe_handle_error(exc_type, exc_value, exc_traceback): ...@@ -92,11 +139,15 @@ def safe_handle_error(exc_type, exc_value, exc_traceback):
def handle_warning(message, category, filename, lineno, file=None, line=None): def handle_warning(message, category, filename, lineno, file=None, line=None):
try: try:
# Log the warning with the notebook name and additional metadata
logger.warning( logger.warning(
"Warning occurred: %s, File: %s, Line: %d", "Warning occurred: %s, File: %s, Line: %d",
message, filename, lineno, message, filename, lineno,
extra={'notebook': NOTEBOOK_NAME, 'directory': CURRENT_WORKING_DIR} extra={
'notebook': NOTEBOOK_NAME,
'directory': CURRENT_WORKING_DIR,
'job_id': JOB_ID,
'class': category.__name__ if category else 'DefaultWarningClass', # noqa
}
) )
except Exception as log_error: except Exception as log_error:
sys.stderr.write(f"Logging failed: {log_error}\n") sys.stderr.write(f"Logging failed: {log_error}\n")
...@@ -111,4 +162,5 @@ warnings.showwarning = handle_warning ...@@ -111,4 +162,5 @@ warnings.showwarning = handle_warning
def custom_showtraceback(self, *args, **kwargs): def custom_showtraceback(self, *args, **kwargs):
return safe_handle_error(*sys.exc_info()) return safe_handle_error(*sys.exc_info())
IPython.core.interactiveshell.InteractiveShell.showtraceback = custom_showtraceback # noqa IPython.core.interactiveshell.InteractiveShell.showtraceback = custom_showtraceback # noqa
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment