How to catch libreoffice macro errors and forward to a Python log file?

I’m using code to generate the log like this:

# coding: utf-8
from __future__ import unicode_literals
import uno
from pathlib import Path
import logging

#====== Logging
log_all_disabled = False
my_log_level = 10 #'CRITICAL':50, 'ERROR':40, 'WARNING':30, 'INFO':20, 'DEBUG':10, 'NOTSET':0

doc = XSCRIPTCONTEXT.getDocument()
this_doc_url = doc.URL
this_doc_sys_path = uno.fileUrlToSystemPath(this_doc_url)
this_doc_parent_path = Path(this_doc_sys_path).parent
base_name = Path(this_doc_sys_path).name
only_name = str(base_name).rsplit('.ods')[0]

log_file_path = Path(this_doc_parent_path, f'''{only_name}.log''')

logger = logging.getLogger(__name__)
logger.setLevel(my_log_level)
logger.disabled = log_all_disabled

fh = logging.FileHandler(str(log_file_path), mode='w')
fh.setLevel(my_log_level)
formatter = logging.Formatter(fmt='%(asctime)s:%(levelname)s:%(filename)s:%(lineno)d:%(message)s', datefmt='%d-%m-%Y:%H:%M:%S')
fh.setFormatter(formatter)
logger.addHandler(fh)
#======

def sum(a, b, c):
    print(d) #variable d is not defined
    return a + b + c

def hello(*args):
    logger.warning("Atention!")

def test(*args):
    hello()
    sum(1,2,3)

On line 32 I purposely asked to print the variable d which is not defined.

print(d) #variable d is not defined

I would like when this type of error occurred to be stored in my custom log.
How to do this?

example
logging python.ods (9,0,KB)

first clean up: The code above should be replaced by:

doc = XSCRIPTCONTEXT.getDocument()

sys_path = Path(uno.fileUrlToSystemPath(doc.URL))

log_path = sys_path.with_suffix('.log')

I’m working on the logging-part give me some minutes!

2 Likes

Thanks for bringing this up. Very useful indeed.

Linux? Start LO from terminal and redirect output to file.

I use it on Linux, but other people will use the same file on Windows, so the option to start libreoffice from the terminal is discarded.

The APSO extension (alternative Python script orgainizer) comes with a Python terminal which does the same trick also on Windows, I guess. Its main functionality is about browsing, editing and embedding Python macros more conveniently.

The APSO is only present on the PC of the person who develops the script.
But when a file is run by several users on different PC, a log file is important to find possible errors and inconsistencies. And users don’t have APSO installed.

Hallo
So far it works with below:

import uno
from pathlib import Path
import logging

#'CRITICAL':50, 'ERROR':40, 'WARNING':30, 'INFO':20, 'DEBUG':10, 'NOTSET':0

doc = XSCRIPTCONTEXT.getDocument()
sys_path = Path(uno.fileUrlToSystemPath(doc.URL))
log_path = sys_path.with_suffix('.log')


class Logger_decorator():
    
    def __init__(self, function):
        LOGLEVEL = 10
        self.function = function
        self.logger = logging.getLogger(self.function.__name__)
        self.fh = logging.FileHandler(log_path, mode='w')                                             
        self.fh.setLevel(LOGLEVEL)
        formatter = logging.Formatter(fmt='%(asctime)s:%(levelname)s:%(filename)s:%(lineno)d:%(message)s',
                              datefmt='%Y-%m-%d-%H:%M:%S')
        self.fh.setFormatter(formatter)
        self.logger.addHandler(self.fh)

    def __call__(self, *args, **kwargs):
        try:
            return self.function(*args,**kwargs)
        except Exception as ex:
            print(ex)
            self.logger.exception(ex)
            

#======
@Logger_decorator
def summe(a,b,c):
    print(d) #variable d is not defined
    return a + b + c

@Logger_decorator
def hello(*args):
    #ogger.warning("Atention!")
    pass

@Logger_decorator
def test(*args):
    hello()
    return summe(4,5,6)

see attached Document
python_logger.ods (9.5 KB)

Very good was what I needed. Thank you for your attention.

You could help with another problem, I have this code below, a conditional decorator that logs the function information if a condition (log_decorator_condition) is true. I tried to merge with your code but I couldn’t. Would you know how to do something like that too?

# coding: utf-8
from __future__ import unicode_literals
import uno
from pathlib import Path
import logging
from functools import wraps

#====== Logging Decorator
log_decorator_condition = True
log_all_disabled = False
my_log_level = 10 #'CRITICAL':50, 'ERROR':40, 'WARNING':30, 'INFO':20, 'DEBUG':10, 'NOTSET':0

doc = XSCRIPTCONTEXT.getDocument()
sys_path = Path(uno.fileUrlToSystemPath(doc.URL))
log_path = sys_path.with_suffix('.log')


logger = logging.getLogger(__name__)
logger.setLevel(my_log_level)
logger.disabled = log_all_disabled

fh = logging.FileHandler(log_path, mode='w')
fh.setLevel(my_log_level)
formatter = logging.Formatter(fmt='%(asctime)s:%(levelname)s:%(filename)s:%(lineno)d:%(message)s', datefmt='%d-%m-%Y:%H:%M:%S')
fh.setFormatter(formatter)
logger.addHandler(fh)

class log(object):
    def __init__(self, condition):
        self.logger = logging.getLogger(__name__)
        self.condition = condition

    def __call__(self, fn):
        if not self.condition:
            return fn
        else:
            @wraps(fn)
            def decorated(*args, **kwargs):
                result = fn(*args, **kwargs)
                log_msg = f'func:{fn.__name__}:args:{args}:kw:{kwargs}:result:{result}'
                self.logger.debug(log_msg)
                return result
            return decorated
#======

@log(log_decorator_condition)
def sum(a, b, c):
    #print(d) #variable d is not defined
    return a + b + c

def hello(*args):
    logger.warning('Atention!')

def test(*args):
    hello()
    sum(1,2,3)

example 2:
logging2.ods (9,2,KB)

output:

03-11-2021:15:24:00:WARNING:Module.py:52:Atention!
03-11-2021:15:24:00:DEBUG:Module.py:41:func:sum:args:(1, 2, 3):kw:{}:result:6


How to make a decorator that in addition to recording the exceptions in the log, could also record the function “information” if a certain parameter is true.

@karolus
Thank you for the tips. It worked according to my need.

# coding: utf-8
from __future__ import unicode_literals
import uno
from pathlib import Path
import logging
from functools import wraps

doc = XSCRIPTCONTEXT.getDocument()
main_sheet = doc.getSheets().getByIndex(0)
log_decorator_condition = False
if main_sheet['B1'].getString() == 'Yes':
    log_decorator_condition = True

log_all_disabled = False
my_log_level = 10

#sys_path = Path(__file__)
sys_path = Path(uno.fileUrlToSystemPath(doc.URL))
log_path = sys_path.with_suffix('.log')

logger = logging.getLogger(__name__)
logger.setLevel(my_log_level)
logger.disabled = log_all_disabled

fh = logging.FileHandler(log_path, mode='w')
fh.setLevel(my_log_level)
formatter = logging.Formatter(fmt='%(asctime)s:%(levelname)s:%(filename)s:%(lineno)d:%(message)s', datefmt='%d-%m-%Y:%H:%M:%S')
fh.setFormatter(formatter)
logger.addHandler(fh)

class log(object):
    def __init__(self, condition):
        self.logger = logging.getLogger(__name__)
        self.condition = condition

    def __call__(self, fn):
        @wraps(fn)
        def decorated(*args, **kwargs):
            try:
                result = fn(*args, **kwargs)
                if not self.condition:
                    return result
                else:
                    log_msg = f'func:{fn.__name__} - args:{args} - kw:{kwargs} - result:{result}'
                    self.logger.debug(log_msg)
                    return result
            except Exception as ex:
                #self.logger.debug(f'''Exception:{ex}''')
                self.logger.exception(ex)
                raise ex
        return decorated


@log(log_decorator_condition)
def sum(a, b, c):
    print(d) #variable d is not defined
    return a + b + c

@log(log_decorator_condition)
def hello(*args):
    logger.warning('Atention!')
    return "Hello world"

def test(*args):
    hello()
    sum(1,2,3)

log_decorator_condition.ods (11,7,KB)

I could check to see if there is anything that could be improved or fixed. Thanks

Just another suggestion here. This decorator of karolus works well with functions, but it falls down when used on a class method. Here is my idea (which assumes that a Logger logger has been configured with appropriate scope):

def logger_decorator():
    def pseudo_decorator(func):
        if not callable(func):
            raise Exception(f'func {func} is type {type(func)}')
        def inner_function(*args, **kwargs):
            # logger.exception() below won't in fact show the stack trace leading up to this point...
            caller_trace = ''.join(traceback.format_stack())
            try:
                return func(*args, **kwargs)
            except BaseException:
                logger.exception(f'func {func} args {args} kwargs {kwargs}\ncaller trace\n{caller_trace}')
                # in fact raising this exception will mean it is just "swallowed silently" by the LO exception-handling
                # to alert the user that something has gone wrong and check the logs, ideally a modeless error dialog should be displayed here
                raise
        return inner_function
    return pseudo_decorator

Used like this:

    @logger_decorator()
    def on_load(self, event):
        print(f'xxx: {xxx}') # "NameError: name 'xxx' is not defined"