A logger is a way to have the system generate a text message and have that messaged saved somewhere for future review. Logging can be used as a debugging mechinasm or for just reporting on the status of a system.
Logs are sent to a particular log category, each log category sends the messages it recieves to its handlers. A handler's job is to take a message and write it somewhere. Log categories are organized in a hierarchy and messages sent to a log cateogry will also be sent to that category's ancestors.
Each log category has a log level which is used to determine whether are particular message should be processed or not. Categories inheirt their log level fro their ancestors. If a category has multiple fathers its log level is the min of the levels of its fathers.
(eval-when (:compile-toplevel :load-toplevel :execute) (defconstant +dribble+ 0) (defconstant +debug+ 1) (defconstant +info+ 2) (defconstant +warn+ 3) (defconstant +error+ 4) (defconstant +fatal+ 5) (deflookup-table logger))
(defclass log-category () ((ancestors :initform '() :accessor ancestors :initarg :ancestors) (childer :initform '() :Accessor childer :initarg :childer) (appenders :initform '() :accessor appenders :initarg :appenders) (level :initform +debug+ :initarg :level :accessor level) (name :initarg :name :accessor name)))
(defmethod shared-initialize :after ((l log-category) slot-names &key ancestors &allow-other-keys) (declare (ignore slot-names)) (dolist (anc ancestors) (pushnew l (childer anc) :test (lambda (a b) (eql (name a) (name b))))))
(defmethod enabled-p ((cat log-category) level) (>= level (log.level cat)))
(defmethod log.level ((cat log-category)) (with-slots (level) cat (or level (if (ancestors cat) (loop for ancestor in (ancestors cat) minimize (log.level ancestor)) (error "Can't determine level for ~S" cat)))))
(defmethod (setf log.level) (new-level (cat log-category) &optional (recursive t)) "Change the log level of CAT to NEW-LEVEL. If RECUSIVE is T the setting is also applied to the sub categories of CAT." (setf (slot-value cat 'level) new-level) (when recursive (dolist (child (childer cat)) (setf (log.level child) new-level))))
(defgeneric handle (category message level))
(defmethod handle ((cat log-category) message level) (if (appenders cat) ;; if we have any appenders send them the message (dolist (appender (appenders cat)) (append-message cat appender message level)) ;; send the message to our ancestors (dolist (ancestor (ancestors cat)) (handle ancestor message level))))
(defgeneric append-message (category log-appender message level))
(defclass stream-log-appender () ((stream :initarg :stream :accessor log-stream)) (:documentation "Human readable to the console logger."))
(defmethod append-message ((category log-category) (s stream-log-appender) message level) (multiple-value-bind (second minute hour date month year day) (decode-universal-time (get-universal-time)) (declare (ignore date)) (restart-case (progn (format (log-stream s) "~4,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D.~2,'0D ~S ~S: " year month day hour minute second level (name category)) (format (log-stream s) "~A~%" message)) (use-*debug-io* () :report "Use the current value of *debug-io*" (setf (log-stream s) *debug-io*) (append-message category s message level)) (use-*standard-output* () :report "Use the current value of *standard-output*" (setf (log-stream s) *standard-output*) (append-message category s message level)) (silence-logger () :report "Ignore all future messages to this logger." (setf (log-stream s) (make-broadcast-stream))))))
(defun make-stream-log-appender (&optional (stream *debug-io*)) (make-instance 'stream-log-appender :stream stream))
(defclass file-log-appender (stream-log-appender) ((log-file :initarg :log-file :accessor log-file)) (:documentation "Logs to a file. the output of the file logger is not meant to be read directly by a human."))
(defmethod append-message ((category log-category) (appender file-log-appender) message level) (with-output-to-file (log-file (log-file appender) :if-exists :append :if-does-not-exist :create) (let ((*package* (find-package :it.bese.arnesi))) (format log-file "(~S ~D ~S ~S)~%" level (get-universal-time) (name category) message))))
(defun make-file-log-appender (file-name) (make-instance 'file-log-appender :log-file file-name))
(defmacro deflogger (name ancestors &key level appender appenders documentation) (declare (ignore documentation)) (when appender (setf appenders (append appenders (list appender)))) (let ((ancestors (mapcar (lambda (ancestor-name) `(or (get-logger ',ancestor-name) (error "Attempt to define a sub logger of the undefined logger ~S." ',ancestor-name))) ancestors))) (flet ((make-log-helper (suffix level) `(defmacro ,(intern (strcat name "." suffix)) (message-control &rest message-args) `(when (enabled-p (get-logger ',',name) ,',level) ,(if message-args `(handle (get-logger ',',name) (format nil ,message-control ,@message-args) ',',level) `(handle (get-logger ',',name) ,message-control ',',level)))))) `(progn (setf (get-logger ',name) (make-instance 'log-category :name ',name :level ,level :appenders (list ,@appenders) :ancestors (list ,@ancestors))) ,(make-log-helper '#:dribble '+dribble+) ,(make-log-helper '#:info '+info+) ,(make-log-helper '#:warn '+warn+) ,(make-log-helper '#:error '+error+) ,(make-log-helper '#:fatal '+fatal+)))))
-*- lisp -*-
(in-package :it.bese.arnesi)