Search code examples
pythonclipsclipspy

How to route I/O to console AND logfile using clipspy


I am using clipspy v1.0.0 and Python 3.10 on Ubuntu 22.0.4 LTS.

I am trying to get commands from CLIPS rules (e.g. print t 'WARN: Listen up, maggots...') to print to the console by calling my registered function, which will then parse the message and recognise that it is a warning, so use the logging module to write a warning level message to the log file.

This is what I have so far:

CLIPS rule file (example.clp)

(defrule welcome
(name "Tyler Durden") 
=>
(printout t "INFO: Gentlemen, welcome to Fight Club. The first rule of Fight Club is: you do not talk about Fight Club!" crlf))

Python program (example.py)

import logging
import logging.handlers
import re

import clips

log_format = '%(asctime)s - %(levelname)s - %(message)s'
logging.basicConfig(level=logging.INFO, format=log_format)
logger = logging.getLogger('CLIPS') 
log_level  = logging.INFO     
log_filename = 'expert'

handler = logging.handlers.TimedRotatingFileHandler(f"{log_filename}.log", when="midnight", interval=1)
handler.setLevel(log_level)
formatter = logging.Formatter(log_format)
handler.setFormatter(formatter)

# add a suffix which you want
handler.suffix = "%Y%m%d"

#need to change the extMatch variable to match the suffix for it
handler.extMatch = re.compile(r"^\d{8}$") 

# finally add handler to logger    
logger.addHandler(handler)


def my_print(msg):
    # Placeholder code, not parsing message or using logger for now ...
    print(f"CLIPS: {msg}")


try:

    env = clips.Environment()
    router = clips.LoggingRouter()
    env.add_router(router)
    env.define_function(my_print)
    env.load("example1.clp")
    env.assert_string('(name "Tyler Durden")')
    #env.reset()

    env.run()

    # while True:
    #     env.run()
except clips.CLIPSError as err:
    print(f"CLIPS error: {err}")
except KeyboardInterrupt:
    print("Stopping...")
finally:
    env.clear()

Bash

me@yourbox$ python example.py 
2023-02-21 23:58:20,860 - INFO - INFO: Gentlemen, welcome to Fight Club. The first rule of Fight Club is: you do not talk about Fight Club!

A log file is created, but nothing is written to it. Also, it seems stdout is being simply routed to Pythons stdout, instead of calling my function.

How do I fix the code above, so that when a (print t) statement is encountered in a CLIPS program, it, it simultaneously prints to console, and writes to log using the correct (i.e. specified) log level.


Solution

  • The clips.LoggingRouter uses the root logger API such as logging.info(). So, do like the following example.

    import logging
    import logging.handlers
    import clips
    
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers = [
            logging.StreamHandler(),
            logging.handlers.TimedRotatingFileHandler('expert.log'),
    ])
    
    env = clips.Environment()
    env.add_router(clips.LoggingRouter())
    ...
    env.run()
    

    If you want to specify different log levels for handlers, do like the following example.

    ...
    handler1 = logging.StreamHandler()
    handler1.setLevel(logging.INFO)
    handler2 = logging.handlers.TimedRotatingFileHandler('expert.log')
    handler2.setLevel(logging.ERROR)
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers = [handler1, handler2])
    

    If you want to log a message with the log level determined by the prefix of the message, define a subclass of the clips.LoggingRouter instead of hacking the logging module, like the following example.

    ...
    class LoggingRouterWithLevelByPrefix(clips.LoggingRouter):
        def write(self, name, msg):
            if msg.startswith('WARN:'):
                logging.warn(msg[5:])
            elif msg.startswith('ERROR:'):
                logging.error(msg[6:])
            else:
                logging.info(msg)
    ...
    env.add_router(LoggingRouterWithLevelByPrefix())
    ...
    

    But keep in mind that Clips defines logical names such as stdout, stderr, stdwrn, which clips.LoggingRouter uses to determine the log level. So you can use them like the following example.

    (printout t "This will be the INFO level." crlf)
    (printout stdwrn "This will be the WARNING level." crlf)
    (printout stderr "This will be the ERROR level." crlf)