Coverage report: /home/ellis/comp/core/lib/log/log.lisp
Kind | Covered | All | % |
expression | 245 | 576 | 42.5 |
branch | 15 | 50 | 30.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; log.lisp --- Log Objects
7
(defparameter *log-levels* (vector t :trace :debug :info :warn :error :fatal nil)))
10
(position name *log-levels*))
11
(define-setf-expander ilevel (new name)
12
(setf (svref *log-levels* (ilevel name)) new))
14
(deftype log-level-designator () `(or (member ,@(coerce *log-levels* 'list)) integer))
16
(declaim (log-level-designator *log-level*))
17
(defparameter *log-level* :debug
18
"Logging is performed dynamically based on this variable. When NIL,
19
logging is disabled, which is equivalent to a level of :FATAL. When T,
20
Logging is enabled for all levels, which is equivalent to :TRACE.")
22
(defvar *log-message-class* 'simple-log-message
23
"The class of messages sent to the logger. May be a subclass of LOG-MESSAGE or
24
T which indicates that message data will be sent without generating a new
27
(defvar *log-show-backtrace* t)
29
(defvar *log-timestamp* t
30
"If non-nil, print a timestamp with log output. The value may be a
31
function in which case it is used as the function value of
32
`log-timestamp-source', or a number which will be used as the input arg to GET-REAL-TIME-SINCE.")
34
(defvar *log-timestamp-format* '((:year 4) #\- (:month 2) #\- (:day 2) #\Space (:hour 2) #\: (:min 2) #\: (:sec 2)))
36
(declaim (fixnum *log-indent*))
37
(defvar *log-indent* 0
38
"Level of indentation to apply to multi-line log messages.")
40
(defun get-real-time-since (n)
41
"Return the numbers of seconds since a relative value offset N."
42
(- (get-internal-real-time) n))
44
(defun init-log-timestamp ()
45
(setq *log-timestamp* (get-internal-real-time)))
47
;; TODO 2023-09-20: (declaim (inline log-timestamp-source)) ;; this
48
;; probably shouldn't be inlined.. bench it
49
(defun log-timestamp-source ()
50
(typecase *log-timestamp*
51
(function (funcall *log-timestamp*))
52
(number (/ (get-real-time-since *log-timestamp*) #.internal-time-units-per-second))
53
(t (/ (get-internal-real-time) #.internal-time-units-per-second))))
55
(defun universal-timestamp () (get-universal-time))
57
;; the purpose of this struct is to route log messages to the appropriate
60
(fatal *error-output*)
61
(error *error-output*)
65
(trace *trace-output*))
67
(defmacro define-log-level (name &body pred)
68
"Define a log-level of NAME with PRED being the body of the predicate
70
(let ((%name (string-upcase name)))
72
(defun ,(intern (concatenate 'string %name "-P")) ()
73
,@(or pred `((eql *log-level* ,(sb-int:keywordicate name)))))
74
(defun ,(intern (concatenate 'string %name "!")) (&rest args)
76
((and *logger* (started-p *logger*))
77
(log-message ,(keywordicate name) nil args))
78
((,(symbolicate (concatenate 'string %name "-P")))
79
(fresh-line *trace-output*)
80
(format *trace-output* "#:~(~A~)~@[ ~f~]~&"
82
(when *log-timestamp* (log-timestamp-source)))
83
(if (and (stringp (car args)) (> (length args) 1))
84
(let ((fmt (pop args)))
85
(apply 'format *trace-output* fmt args))
86
(mapc (lambda (x) (format *trace-output* "; ~A~&" x)) args))))
91
(defun ,(intern (concatenate 'string %name "-DESCRIBE")) (&rest args)
92
(,(intern (concatenate 'string %name "!")) (apply #'describe args))))))
94
(define-log-level trace (or (eq *log-level* :trace) (eq *log-level* t)))
95
(define-log-level debug (or (trace-p) (eq *log-level* :debug)))
96
(define-log-level info (or (debug-p) (eq *log-level* :info)))
97
(define-log-level warn (or (info-p) (eq *log-level* :warn)))
98
(define-log-level error (or (warn-p) (eq *log-level* :error)))
99
(define-log-level fatal t) ;; probably needs to be a special case
101
;; TODO 2023-08-31: single format control string
102
;; (defun debug! (&rest args)
105
;; ;; RESEARCH 2023-08-31: what's better here.. loop, do, mapc+nil?
106
;; (map nil (lambda (x) (format t "~X~%" x)) args))
110
(defclass log-message (message)
111
((timestamp :initarg :timestamp :accessor timestamp)
112
(level :initarg :level :accessor level)
113
(content :initarg :content :accessor content))
119
(defclass simple-log-message (log-message)
120
((thread :initarg :thread :accessor message-thread)
121
(tags :initarg :tags :accessor tags))
123
:thread *current-thread*
126
(defmethod initialize-instance :before ((message log-message) &key level)
127
(unless (typep level 'log-level-designator)
128
(error "Level must be one of ~a" *log-levels*)))
130
(defmethod initialize-instance :before ((message simple-log-message) &key tags)
131
(unless (every #'keywordp tags)
132
(error "Tags must be keywords")))
134
(defvar *simple-log-message-formatter* (formatter "~a [~4,a] ~{<~a>~}: ~a"))
136
(defmethod format-message (stream (message simple-log-message))
137
(format stream *simple-log-message-formatter*
138
(format-timestring nil (timestamp message) :format *log-timestamp-format*)
141
(format-message nil (content message))))
143
(declaim (inline %log-object))
144
(defun %log-object (obj)
149
(defun log-message (level tags content &optional (class *log-message-class*) &rest initargs)
151
(setf tags (list tags)))
152
(%log-object (apply #'make-instance class :level level :tags tags :content content initargs)))
154
(defun log-message* (level content &rest args)
155
(%log-object (make-instance 'log-message
157
:content (apply 'format nil content args))))
159
(defgeneric log-object (level tags datum &rest args)
160
(:method (level tags (datum string) &rest args)
161
(log-message level tags (apply #'format nil datum args)))
162
(:method (level tags (datum symbol) &rest args)
163
(log-object level tags (apply (if (subtypep datum 'condition)
167
(:method (level tags (datum function) &rest args)
168
(log-message level tags (lambda () (apply datum args))))
169
(:method (level tags datum &rest args)
170
(declare (ignore args))
171
(log-message level tags datum))
172
(:method (level tags (datum condition) &rest args)
173
(declare (ignore args))
174
(log-message level tags (princ-to-string datum)
175
'condition-message :condition datum)))
177
(defclass rotating-file-sink (file-sink)
178
((interval :accessor interval)
179
(last-rotation :initform 0 :accessor last-rotation)
180
(path :initarg :path :initform nil :accessor path))
185
(defun rotate-file-sink (obj &optional new-file)
186
(let ((time (setf (last-rotation obj) (get-universal-time))))
188
(setf (file obj) new-file))
190
(multiple-value-bind (s m h dd mm yy) (decode-universal-time time)
192
(make-pathname :name (format NIL "~4,'0d-~2,'0d-~2,'0d:~2,'0d:~2,'0d:~2,'0d~@[.~a~]"
193
yy mm dd h m s (pathname-name (path obj)))
194
:defaults (path obj))))))
195
(setf (last-rotation obj) time)))
197
(defmethod initialize-instance :after ((obj rotating-file-sink) &key interval)
198
(setf (interval obj) interval)
199
(rotate-file-sink obj))
201
(defmethod (setf interval) (value (obj rotating-file-sink))
203
((:hourly :daily :monthly :weekly)
204
(setf (slot-value obj 'interval) value))))
206
(defmethod msg :before ((obj rotating-file-sink) msg)
207
"Check the last-rotation value and rotate the file sink if needed before processing MSG."
208
(let ((pre (last-rotation obj))
209
(now (get-universal-time)))
211
(multiple-value-bind (s m h dd mm yy dow) (decode-universal-time now)
212
(declare (ignore s m dow))
213
(multiple-value-bind (ps pm ph pdd pmm pyy pdow) (decode-universal-time pre)
214
(declare (ignore ps pm pdow))
215
(ecase (interval obj)
217
(or (/= ph h) (/= pdd dd) (/= pmm mm) (/= pyy yy)))
219
(or (/= pdd dd) (/= pmm mm) (/= pyy yy)))
221
(or (/= pmm mm) (/= pyy yy)))
223
(< (* 60 60 24 7) (- (get-universal-time) (last-rotation obj)))))))
224
(rotate-file-sink obj))))
226
(defclass level-filter (filter)
227
((level :initform *log-level* :accessor level))
231
(defmethod initialize-instance :after ((filter level-filter) &key level)
232
(setf (level filter) level))
234
(defmethod (setf level) :before (level (filter level-filter))
235
"Assert LEVEL is a member of *LOG-LEVELS*."
236
(assert (find level *log-levels*) nil 'invalid-argument
237
:reason "LEVEL is not a member of *LOG-LEVELS* or an integer"
240
(defmethod msg ((filter level-filter) (message message))
241
(let ((level (level filter)))
242
(when (<= (ilevel level)
243
(ilevel (level message)))
246
(defclass tag-filter (filter)
247
((tags :initarg :tags :initform t :accessor tags)))
249
(defmethod msg ((filter tag-filter) (message message))
250
(when (or (eql (tags filter) T)
251
(loop for tag in (tags filter)
252
thereis (find tag (tags message))))
255
(defclass tag-tree-filter (tag-filter) ())
257
(defvar *tag-separator* #\.)
259
(defun matching-tree-tag (filter tag)
260
(let ((tag-leaves (ssplit *tag-separator* (string-upcase tag)))
261
(filter-leaves (ssplit *tag-separator* (string-upcase filter))))
262
(loop for ta in tag-leaves
263
for fill in filter-leaves
265
((or (string= ta "*")
268
((not (string= ta fill))
270
finally (return (>= (length tag-leaves)
271
(length filter-leaves))))))
273
(defmethod msg ((filter tag-tree-filter) (message message))
274
(when (or (eql (tags filter) t)
275
(loop for tag in (tags filter)
276
thereis (find tag (tags message) :test #'matching-tree-tag)))
280
(defun log-sync (&optional (logger *logger*))
281
(when (and logger (log-thread logger)
282
(thread-alive-p (log-thread logger)))
283
(with-sync-message sync
287
;; same as VERBOSE:CONTROLLER
288
(defclass logger (pipe)
289
((thread :initform nil :accessor log-thread)
290
(thread-continue :initform nil :accessor log-thread-continue)
291
(queue :initform (make-array '(10) :adjustable T :fill-pointer 0) :accessor queue)
292
(queue-back :initform (make-array '(10) :adjustable T :fill-pointer 0) :accessor queue-back)
293
(queue-condition :initform (make-waitqueue :name "message-condition") :reader queue-condition)
294
(queue-lock :initform (make-mutex :name "message-lock") :reader queue-lock))
295
(:documentation "A class which implements logging functionality. An instance of this class may
296
be designated as the 'global' logger by setting the value of *LOGGER*, or may
297
be implemented for a specific application."))
299
(defaccessor sink ((self logger)) (aref #1=(pipe self) (1- (length #1#))))
300
(defaccessor source ((self logger)) (aref (pipe self) 0))
302
(defmethod print-object ((self logger) stream)
303
(print-unreadable-object (self stream :type t)
304
(format stream "~@[:threaded ~* ~]~@[:running ~* ~]:size ~d"
305
(log-thread self) (log-thread-continue self) (length (queue self)))))
307
(defmethod start ((self logger))
308
(setf (log-thread-continue self) t)
309
(when (log-thread self)
310
(cerror "Spawn a new thread anyway"
311
"There is already a thread set on the logger."))
312
(setf (log-thread self)
314
(let ((*logger* self)
315
(*standard-output* *standard-output*)
316
(*error-output* *error-output*)
317
(*trace-output* *trace-output*)
318
(*query-io* *query-io*)
319
(*debug-io* *debug-io*))
320
(lambda () (logger-loop self)))
321
:name "log-thread")))
323
(defmethod started-p ((self logger)) (log-thread-continue self))
325
(defmethod stopped-p ((self logger)) (not (log-thread-continue self)))
327
(defmethod stop ((self logger) &key)
328
(setf (log-thread-continue self) nil)
329
(loop for th = (log-thread self)
331
while (and th (thread-alive-p th))
332
do (condition-notify (queue-condition self))
335
(terminate-thread th)
339
(defmacro with-logger-lock ((&optional (logger '*logger*)) &body body)
340
`(with-mutex ((queue-lock ,logger))
343
(defmacro with-logger (logger &body body)
344
"Temporarily bind LOGGER to *LOGGER* for the duration of BODY."
345
`(let ((*logger* ,logger))
348
(defmethod logger-loop ((self logger))
349
(let* ((lock (queue-lock self))
350
(condition (queue-condition self))
354
(loop do (let ((queue (queue self)))
355
(rotatef (queue self) (queue-back self))
357
(with-simple-restart (skip "Skip processing this message batch.")
360
do (with-simple-restart (continue "Continue processing messages, skipping ~A" m)
362
(setf (aref queue i) 0)))
363
(setf (fill-pointer queue) 0))
365
(loop while (= 0 (length (queue self)))
366
do (condition-wait* condition lock :timeout 1))
367
while (log-thread-continue self))
368
(setf (log-thread self) nil)
369
(ignore-errors (release-mutex lock)))))
371
(defmethod msg ((self logger) message)
372
(let ((th (log-thread self)))
373
(cond ((and th (thread-alive-p th)
374
(not (eql *current-thread* th)))
375
(with-logger-lock (self)
376
(vector-push-extend message (queue self)))
377
(condition-notify (queue-condition self)))
378
(t (msg (pipe self) message))))
382
(defun log-pipe (&rest elements)
383
(let ((logger (if (typep (first elements) 'logger)
386
(with-logger-lock (logger)
387
(let ((pipe (make-pipe)))
388
(dolist (elt elements)
389
(insert-element* elt pipe))
390
(add-element logger pipe)))))
392
(defun default-logger (&rest args)
393
(let ((pipe (apply 'make-instance 'logger args)))
395
(level-filter :id 'repl-level)
396
(tag-tree-filter :id 'repl-tags)
397
(stream-sink :id 'repl-stream))))
399
(defun remove-logger ()
402
(setf *logger* nil)))
404
(defun restart-logger (&optional (logger (default-logger)))
406
(setf *logger* logger))
409
(defmacro with-conditions-logged (&body body)
414
(log-message* :error "Error signalled: ~A" cond)
418
(log-message* :warn "Warning signalled: ~A" c))))