diff --git a/.dir-locals.el b/.dir-locals.el index 292d4dcf5..14b1c7804 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -28,7 +28,9 @@ (cl-defun . 2) (with-parsed-tramp-file-name . 2) (thread-first . 0) - (thread-last . 0))))) + (thread-last . 0) + (transient-define-prefix . defmacro) + (transient-define-suffix . defmacro))))) ;; To use the bug-reference stuff, do: ;; (add-hook 'text-mode-hook #'bug-reference-mode) diff --git a/CHANGELOG.md b/CHANGELOG.md index 936d8c97b..fc6fe7118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### New features +- [#3352](https://github.com/clojure-emacs/cider/pull/3352) Add CIDER Log Mode, a major mode that allows you to capture, debug, inspect and view log events emitted by Java logging frameworks. - [#3354](https://github.com/clojure-emacs/cider/issues/3354): Add new customization variable `cider-reuse-dead-repls` to control how dead REPL buffers are reused on new connections. ### Bugs fixed diff --git a/cider-eval.el b/cider-eval.el index eccea291f..110505bd8 100644 --- a/cider-eval.el +++ b/cider-eval.el @@ -320,6 +320,7 @@ If CONNECTION is nil, use `cider-current-repl'." "cider.nrepl/wrap-format" "cider.nrepl/wrap-info" "cider.nrepl/wrap-inspect" + "cider.nrepl/wrap-log" "cider.nrepl/wrap-macroexpand" "cider.nrepl/wrap-ns" "cider.nrepl/wrap-out" diff --git a/cider-log.el b/cider-log.el new file mode 100644 index 000000000..80635d503 --- /dev/null +++ b/cider-log.el @@ -0,0 +1,1403 @@ +;;; cider-log.el --- Log inspection functionality for Clojure -*- lexical-binding: t -*- + +;; Copyright © 2023 Bozhidar Batsov and CIDER contributors + +;; Author: r0man + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;; This file is not part of GNU Emacs. + +;;; Commentary: + +;; Log inspection functionality for Clojure. + +;;; Code: + +(require 'cider-inspector) +(require 'cider-stacktrace) +(require 'cl-lib) +(require 'logview) +(require 'org) +(require 'seq) +(require 'transient) + +(defcustom cider-log-framework-name nil + "The name of the current log framework." + :group 'cider + :package-version '(cider . "1.8.0") + :safe #'stringp + :type 'string) + +(defcustom cider-log-appender-id "cider-log" + "The name of the default log appender." + :group 'cider + :package-version '(cider . "1.8.0") + :safe #'stringp + :type 'string) + +(defcustom cider-log-buffer "*cider-log*" + "The name of the log buffer." + :group 'cider + :package-version '(cider . "1.8.0") + :safe #'stringp + :type 'string) + +(defcustom cider-log-event-buffer "*cider-log-event*" + "The name of the log event buffer." + :group 'cider + :package-version '(cider . "1.8.0") + :safe #'stringp + :type 'string) + +(defcustom cider-log-max-message-length 500 + "The maximum length of the log message to display." + :group 'cider + :package-version '(cider . "1.8.0") + :safe #'integerp + :type 'integer) + +(defcustom cider-log-pagination-limit 250 + "The maximum number of log events to return when searching events." + :group 'cider + :package-version '(cider . "1.8.0") + :safe #'integerp + :type 'integer) + +(defcustom cider-log-pagination-offset 0 + "The offset from which to return results when searching events." + :group 'cider + :package-version '(cider . "1.8.0") + :safe #'integerp + :type 'integer) + +(defvar cider-log--initialized-once-p nil + "Set to t if log framework and appender have been initialized once.") + +(defvar cider-log-framework nil + "The current log framework to use.") + +(defvar cider-log-appender nil + "The current log appender.") + +(defvar cider-log-appender-size 100000 + "The size of the log appender.") + +(defvar cider-log-appender-threshold 10 + "The threshold in percent of the log appender.") + +(defvar-local cider-log-consumer nil + "The current log consumer.") + +;; Filters + +(defvar cider-log--end-time-filter nil) +(defvar cider-log--exceptions-filter nil) +(defvar cider-log--level-filter nil) +(defvar cider-log--loggers-filter nil) +(defvar cider-log--pattern-filter nil) +(defvar cider-log--start-time-filter nil) +(defvar cider-log--threads-filter nil) + +(defun cider-log--bold (s) + "Return S with a bold face." + (when s (propertize (format "%s" s) 'face 'bold))) + +(defun cider-log-buffer-clear-p (&optional buffer) + "Return non-nil if BUFFER is not empty, otherwise nil." + (when-let (buffer (get-buffer (or buffer cider-log-buffer))) + (> (buffer-size buffer) 0))) + +(defun cider-log--description-clear-events-buffer () + "Return the description for the set framework action." + (format "Clear %s buffer" + (if cider-log-buffer + (cider-log--format-value cider-log-buffer) + (propertize "n/a" 'face 'font-lock-comment-face)))) + +(defun cider-log--description-set-framework () + "Return the description for the set framework action." + (format "Select framework %s" + (if cider-log-framework-name + (cider-log--format-value cider-log-framework-name) + (propertize "n/a" 'face 'font-lock-comment-face)))) + +(defun cider-log--description-set-buffer () + "Return the description for the set buffer action." + (format "Select buffer %s" + (if cider-log-buffer + (cider-log--format-value cider-log-buffer) + (propertize "n/a" 'face 'font-lock-comment-face)))) + +(defun cider-log--buffers-in-major-mode (expected) + "Return all buffers which are in the EXPECTED major mode." + (seq-filter (lambda (buffer) + (with-current-buffer buffer + (equal expected major-mode))) + (buffer-list))) + +(defun cider-log--format-time (time) + "Format TIME in ISO8601 format." + (format-time-string "%FT%T%z" time)) + +(defun cider-log--format-value (value) + "Format the VALUE for display in a transient menu." + (cond ((null value) "") + ((or (listp value) (vectorp value)) + (string-join (seq-map #'cider-log--format-value value) + (propertize ", " 'face 'font-lock-comment-face))) + (t (propertize (format "%s" value) 'face 'transient-value)))) + +(defun cider-log--strip-whitespace (s) + "Replace multiple white space characters in S with a single one." + (replace-regexp-in-string "[\n ]+" " " s)) + +;; NREPL + +(defun cider-request:log-add-consumer (framework appender consumer &optional callback) + "Add CONSUMER to the APPENDER of FRAMEWORK and call CALLBACK on log events." + (cider-ensure-op-supported "cider/log-add-consumer") + (thread-first `("op" "cider/log-add-consumer" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "filters" ,(cider-log-consumer-filters consumer)) + (cider-nrepl-send-request callback))) + +(defun cider-request:log-analyze-stacktrace (framework appender event &optional callback) + "Analyze the EVENT stacktrace of the APPENDER of FRAMEWORK and call CALLBACK." + (cider-ensure-op-supported "cider/log-analyze-stacktrace") + (thread-first `("op" "cider/log-analyze-stacktrace" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "event" ,(cider-log-event-id event)) + (cider-nrepl-send-request callback))) + +(defun cider-sync-request:log-update-consumer (framework appender consumer) + "Add CONSUMER to the APPENDER of FRAMEWORK and call CALLBACK on log events." + (cider-ensure-op-supported "cider/log-update-consumer") + (thread-first `("op" "cider/log-update-consumer" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "consumer" ,(cider-log-consumer-id consumer) + "filters" ,(cider-log-consumer-filters consumer)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-update-consumer"))) + +(defun cider-sync-request:log-add-appender (framework appender) + "Add the APPENDER to the log FRAMEWORK." + (cider-ensure-op-supported "cider/log-add-appender") + (thread-first `("op" "cider/log-add-appender" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "filters" ,(cider-log-appender-filters appender) + "size" ,(cider-log-appender-size appender) + "threshold" ,(cider-log-appender-threshold appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-add-appender"))) + +(defun cider-sync-request:log-update-appender (framework appender) + "Update the APPENDER of the log FRAMEWORK." + (cider-ensure-op-supported "cider/log-update-appender") + (thread-first `("op" "cider/log-update-appender" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "filters" ,(cider-log-appender-filters appender) + "size" ,(cider-log-appender-size appender) + "threshold" ,(cider-log-appender-threshold appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-update-appender"))) + +(defun cider-sync-request:log-clear (framework appender) + "Clear the log events for FRAMEWORK and APPENDER." + (cider-ensure-op-supported "cider/log-clear-appender") + (thread-first `("op" "cider/log-clear-appender" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-clear-appender"))) + +(defun cider-sync-request:log-inspect-event (framework appender event) + "Inspect the log event with the ID in the APPENDER of the log FRAMEWORK." + (cider-ensure-op-supported "cider/log-inspect-event") + (thread-first `("op" "cider/log-inspect-event" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "event" ,(cider-log-event-id event)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "value"))) + +(defun cider-sync-request:log-format-event (framework appender event) + "Format the log EVENT from the APPENDER of the log FRAMEWORK." + (cider-ensure-op-supported "cider/log-format-event") + (thread-first + (seq-mapcat #'identity + (map-merge 'list + (cider--nrepl-print-request-map fill-column) + `(("op" "cider/log-format-event") + ("framework" ,(cider-log-framework-id framework)) + ("appender" ,(cider-log-appender-id appender)) + ("event" ,(cider-log-event-id event))))) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-format-event"))) + +(defun cider-sync-request:log-frameworks () + "Return the available log frameworks." + (cider-ensure-op-supported "cider/log-frameworks") + (thread-first `("op" "cider/log-frameworks") + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-frameworks"))) + +(cl-defun cider-sync-request:log-search (framework appender &key filters limit offset) + "Search log events of FRAMEWORK and APPENDER using FILTERS, LIMIT and OFFSET." + (cider-ensure-op-supported "cider/log-search") + (thread-first `("op" "cider/log-search" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "filters" ,filters + "limit" ,limit + "offset" ,offset) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-search"))) + +(defun cider-sync-request:log-exceptions (framework appender) + "Return the Cider log exceptions for FRAMEWORK and APPENDER." + (cider-ensure-op-supported "cider/log-exceptions") + (thread-first `("op" "cider/log-exceptions" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-exceptions"))) + +(defun cider-sync-request:log-levels (framework appender) + "Return the Cider log levels for FRAMEWORK and APPENDER." + (cider-ensure-op-supported "cider/log-levels") + (thread-first `("op" "cider/log-levels" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-levels"))) + +(defun cider-sync-request:log-loggers (framework appender) + "Return the Cider loggers for FRAMEWORK and APPENDER." + (cider-ensure-op-supported "cider/log-loggers") + (thread-first `("op" "cider/log-loggers" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-loggers"))) + +(defun cider-sync-request:log-remove-appender (framework appender) + "Remove the APPENDER from the log FRAMEWORK." + (cider-ensure-op-supported "cider/log-remove-appender") + (thread-first `("op" "cider/log-remove-appender" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-remove-appender"))) + +(defun cider-sync-request:log-remove-consumer (framework appender consumer) + "Remove the CONSUMER from the APPENDER of the log FRAMEWORK." + (cider-ensure-op-supported "cider/log-remove-consumer") + (thread-first `("op" "cider/log-remove-consumer" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender) + "consumer" ,(cider-log-consumer-id consumer)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-remove-consumer"))) + +(defun cider-sync-request:log-threads (framework appender) + "Return the threads for FRAMEWORK and APPENDER." + (cider-ensure-op-supported "cider/log-threads") + (thread-first `("op" "cider/log-threads" + "framework" ,(cider-log-framework-id framework) + "appender" ,(cider-log-appender-id appender)) + (cider-nrepl-send-sync-request) + (nrepl-dict-get "cider/log-threads"))) + +(defun cider-log--completion-extra-properties (keys &optional separator) + "Return the completion properties for NREPL dictionaries. + +The KEYS are used to lookup the values and are joined by SEPARATOR." + `(:annotation-function + ,(lambda (identifier) + (when-let (dict (cadr (assoc identifier minibuffer-completion-table))) + (let ((annotation (string-join (seq-map (lambda (key) (nrepl-dict-get dict key)) keys) + (or separator " ")))) + (unless (string-blank-p annotation) + (propertize (format " - %s" (cider-log--strip-whitespace annotation)) + 'face 'font-lock-comment-face))))))) + +(defun cider-log--read-appender-id (prompt initial-input history) + "Read a appender from the minibuffer using PROMPT, INITIAL-INPUT and HISTORY." + (let ((table (when cider-log-framework + (when-let (framework (cider-log-framework-reload cider-log-framework)) + (seq-map #'cider-log-appender-id (cider-log-framework-appenders framework)))))) + (completing-read (or prompt "Log appender: ") table nil nil + (or initial-input cider-log-appender-id) + history cider-log-appender-id))) + +(defun cider-log--read-buffer (&optional prompt initial-input history) + "Read the log buffer name using PROMPT, INITIAL-INPUT and HISTORY." + (let ((table (seq-map #'buffer-name (cider-log--buffers-in-major-mode 'cider-log-mode)))) + (completing-read (or prompt "Buffer: ") table nil nil + (or initial-input cider-log-buffer) + history cider-log-buffer))) + +(defun cider-log--read-exceptions (&optional prompt initial-input history) + "Read a list of exceptions using PROMPT, INITIAL-INPUT and HISTORY." + (let ((table (when (cider-log-appender-attached-p) + (nrepl-dict-keys (cider-sync-request:log-exceptions + cider-log-framework cider-log-appender))))) + (completing-read-multiple (or prompt "Exceptions: ") table nil nil initial-input history))) + +(defun cider-log--read-framework-name (&optional prompt initial-input history) + "Read a framework name using PROMPT, INITIAL-INPUT and HISTORY." + (let ((completion-extra-properties (cider-log--completion-extra-properties '("name"))) + (frameworks (cider-sync-request:log-frameworks))) + (completing-read (or prompt "Log framework: ") + (seq-map (lambda (framework) + (list (cider-log-framework-name framework) framework)) + frameworks) + nil nil initial-input history))) + +(defun cider-log--read-level (&optional prompt initial-input history) + "Read a log level using PROMPT, INITIAL-INPUT and HISTORY." + (let ((table (when cider-log-framework (cider-log-framework-level-names cider-log-framework)))) + (completing-read (or prompt "Level: ") table nil nil initial-input history))) + +(defun cider-log--read-loggers (&optional prompt initial-input history) + "Read a list of loggers using PROMPT, INITIAL-INPUT and HISTORY." + (let ((table (when (cider-log-appender-attached-p) + (nrepl-dict-keys (cider-sync-request:log-loggers + cider-log-framework cider-log-appender))))) + (completing-read-multiple (or "Loggers: " prompt) table nil nil initial-input history))) + +(defun cider-log--read-number-N0 (&optional prompt initial-input history) + "Read a natural number (including zero) using PROMPT, INITIAL-INPUT and HISTORY." + (when-let (value (transient-read-number-N0 (or prompt "Number: ") initial-input history)) + (string-to-number value))) + +(defun cider-log--read-number-N+ (&optional prompt initial-input history) + "Read a natural number (excluding zero) using PROMPT, INITIAL-INPUT and HISTORY." + (when-let (value (transient-read-number-N+ (or prompt "Number: ") initial-input history)) + (string-to-number value))) + +(defun cider-log--read-threads (&optional prompt initial-input history) + "Read a list of threads using PROMPT, INITIAL-INPUT and HISTORY." + (let ((table (when (cider-log-appender-attached-p) + (nrepl-dict-keys (cider-sync-request:log-threads + cider-log-framework cider-log-appender))))) + (completing-read-multiple (or prompt "Threads: ") table nil nil initial-input history))) + +(defun cider-log--read-time (&optional prompt initial-input _) + "Read a time from the minibuffer using PROMPT and INITIAL-INPUT." + (cider-log--format-time (org-read-date t 'to-time nil prompt nil initial-input))) + +;; Log Framework + +(defun cider-log-framework-appender (framework id) + "Return the appender of the log FRAMEWORK with the given ID." + (seq-find (lambda (appender) (equal id (cider-log-appender-id appender))) + (cider-log-framework-appenders framework))) + +(defun cider-log-framework-appenders (framework) + "Return the appenders of the log FRAMEWORK." + (nrepl-dict-get framework "appenders")) + +(defun cider-log-framework-id (framework) + "Return the id of the log FRAMEWORK." + (nrepl-dict-get framework "id")) + +(defun cider-log-framework-javadoc-url (framework) + "Return the Javadoc URL of the log FRAMEWORK." + (nrepl-dict-get framework "javadoc-url")) + +(defun cider-log-framework-name (framework) + "Return the name of the log FRAMEWORK." + (nrepl-dict-get framework "name")) + +(defun cider-log-framework-level-names (framework) + "Return the log level names of the log FRAMEWORK." + (seq-map (lambda (level) (nrepl-dict-get level "name")) + (nrepl-dict-get framework "levels"))) + +(defun cider-log-framework-website-url (framework) + "Return the website URL of the log FRAMEWORK." + (nrepl-dict-get framework "website-url")) + +(defun cider-log-framework-display-name (framework) + "Return the display name of the log FRAMEWORK." + (cider-log--bold (cider-log-framework-name framework))) + +(defun cider-log-framework-add-appender (framework appender) + "Add the APPENDER to the log FRAMEWORK." + (cider-sync-request:log-add-appender framework appender)) + +(defun cider-log-framework-by-id (frameworks id) + "Find the log framework in FRAMEWORKS by ID." + (seq-find (lambda (framework) (equal id (cider-log-framework-id framework))) frameworks)) + +(defun cider-log-framework-by-name (frameworks name) + "Find the log framework in FRAMEWORKS by NAME." + (seq-find (lambda (framework) (equal name (cider-log-framework-name framework))) frameworks)) + +(defun cider-log-framework-reload (framework) + "Reload the log FRAMEWORK." + (cider-log-framework-by-id + (cider-sync-request:log-frameworks) + (cider-log-framework-id framework))) + +;; Log Appender + +(defun cider-log-appender-attached-p (&optional framework appender) + "Return non-nil if the log APPENDER is attached to FRAMEWORK, otherwise nil." + (when-let ((framework (or framework + (cider-log-framework-by-name + (cider-sync-request:log-frameworks) + cider-log-framework-name))) + (appender-id (if appender + (cider-log-appender-id appender) + cider-log-appender-id))) + (cider-log-framework-appender framework appender-id))) + +(defun cider-log-appender-consumers (appender) + "Return the consumers of the log APPENDER." + (nrepl-dict-get appender "consumers")) + +(defun cider-log-appender-id (appender) + "Return the id of the log APPENDER." + (nrepl-dict-get appender "id")) + +(defun cider-log-appender-size (appender) + "Return the size of the log APPENDER." + (nrepl-dict-get appender "size")) + +(defun cider-log-appender-threshold (appender) + "Return the threshold of the log APPENDER." + (nrepl-dict-get appender "threshold")) + +(defun cider-log-appender-filters (appender) + "Return the filters of the log APPENDER." + (nrepl-dict-get appender "filters")) + +(defun cider-log-appender-display-name (appender) + "Return the display name of the log APPENDER." + (cider-log--bold (cider-log-appender-id appender))) + +(defun cider-log-appender-consumer (appender consumer) + "Find the consumer in the log APPENDER by the id slot of CONSUMER." + (let ((id (cider-log-consumer-id consumer))) + (seq-find (lambda (consumer) (equal id (cider-log-consumer-id consumer))) + (cider-log-appender-consumers appender)))) + +(defun cider-log-appender-reload (framework appender) + "Reload the APPENDER of the log FRAMEWORK." + (when-let (framework (cider-log-framework-reload framework)) + (cider-log-framework-appender framework (cider-log-appender-id appender)))) + +;; Log Consumer + +(defun cider-log-consumer-attached-p (&optional framework appender consumer) + "Return non-nil if the CONSUMER is attached to the APPENDER of FRAMEWORK." + (when-let ((framework (or framework cider-log-framework)) + (appender (or appender cider-log-appender)) + (consumer (or consumer cider-log-consumer))) + (cider-log-consumer-reload framework appender consumer))) + +(defun cider-log-consumer-id (consumer) + "Return the id of the log CONSUMER." + (nrepl-dict-get consumer "id")) + +(defun cider-log-consumer-filters (consumer) + "Return the filters of the log CONSUMER." + (nrepl-dict-get consumer "filters")) + +(defun cider-log-consumer-buffers (consumer) + "Find all buffers in which `cider-log-consumer' is bound to CONSUMER." + (seq-filter (lambda (buffer) + (with-current-buffer buffer + (and (nrepl-dict-p cider-log-consumer) + (equal (cider-log-consumer-id consumer) + (cider-log-consumer-id cider-log-consumer))))) + (buffer-list))) + +(defun cider-log-consumer-display-name (consumer) + "Return the display name of the log CONSUMER." + (cider-log--bold (cider-log-consumer-id consumer))) + +(defun cider-log-consumer-reload (framework appender consumer) + "Reload the CONSUMER attached to APPENDER of the log FRAMEWORK." + (when-let (appender (cider-log-appender-reload framework appender)) + (cider-log-appender-consumer appender consumer))) + +(defun cider-log--consumer-add (framework appender consumer buffer) + "Add the CONSUMER to the APPENDER of FRAMEWORK and write events to BUFFER." + (cider-request:log-add-consumer + framework appender consumer + (lambda (msg) + (nrepl-dbind-response msg (cider/log-add-consumer cider/log-consumer cider/log-event status) + (cond ((member "done" status) + (with-current-buffer (get-buffer-create buffer) + (setq-local cider-log-framework framework) + (setq-local cider-log-appender appender) + (setq cider-log-consumer cider/log-add-consumer) + (switch-to-buffer buffer))) + ((member "cider/log-event" status) + (let* ((consumer (nrepl-dict "id" cider/log-consumer)) + (buffers (cider-log-consumer-buffers consumer))) + (when (seq-empty-p buffers) + (message "WARNING: No buffers found for %s log consumer %s of appender %s." + (cider-log-framework-display-name framework) + (cider-log-consumer-display-name consumer) + (cider-log-appender-display-name appender)) + (cider-sync-request:log-remove-consumer framework appender consumer)) + (seq-doseq (buffer buffers) + (with-current-buffer buffer + (cider-log--insert-events buffer (list cider/log-event)) + (when (not (logview-initialized-p)) + (let ((framework cider-log-framework) + (appender cider-log-appender) + (consumer cider-log-consumer)) + (logview--guess-submode) + (cider-log-mode) + ;; Restore buffer local vars reset by calling major mode. + (setq-local cider-log-framework framework + cider-log-appender appender + cider-log-consumer consumer)))))))))))) + +(defun cider-log--remove-current-buffer-consumer () + "Cleanup the log consumer of the current buffer." + (when-let ((framework cider-log-framework) + (appender cider-log-appender) + (consumer cider-log-consumer)) + (setq-local cider-log-consumer nil) + (when-let (consumer (cider-log-consumer-reload framework appender consumer)) + (cider-sync-request:log-remove-consumer framework appender consumer) + consumer))) + +;; Event + +(defun cider-log-event-id (event) + "Return the id of the log EVENT." + (nrepl-dict-get event "id")) + +(defun cider-log-event-exception (event) + "Return the exception of the log EVENT." + (nrepl-dict-get event "exception")) + +(defun cider-log-event--format-logback (event) + "Format the log EVENT in logview's Logback format." + (nrepl-dbind-response event (_exception level logger message thread timestamp) + (propertize (format "%s [%s] %s %s - %s\n" + (if (numberp timestamp) + (format-time-string "%F %T.%3N" (/ timestamp 1000)) + (format "%s" timestamp)) + thread + (upcase level) + logger + (if (and (stringp message) + (numberp cider-log-max-message-length)) + (substring message 0 (min (length message) cider-log-max-message-length)) + "")) + :cider-log-event event))) + +(defun cider-log-event--pretty-print (framework appender event) + "Format the log EVENT of FRAMEWORK and APPENDER." + (when-let (event (cider-sync-request:log-format-event framework appender event)) + (cider-popup-buffer cider-log-event-buffer cider-auto-select-error-buffer + 'clojure-mode 'ancillary) + (with-current-buffer cider-log-event-buffer + (let ((inhibit-read-only t)) + (erase-buffer) + (insert event) + (goto-char (point-min)))))) + +(defun cider-log-event--inspect (framework appender event) + "Inspect the log EVENT of FRAMEWORK and APPENDER." + (thread-last (cider-sync-request:log-inspect-event framework appender event) + (cider-inspector--render-value))) + +(defun cider-log--insert-events (buffer events) + "Insert the log EVENTS into BUFFER." + (with-current-buffer (get-buffer-create buffer) + (let ((windows (seq-filter (lambda (window) (= (window-point window) (point-max))) + (get-buffer-window-list buffer)))) + (save-excursion + (let ((inhibit-read-only t)) + (goto-char (point-max)) + (seq-doseq (event events) + (insert (cider-log-event--format-logback event))))) + (seq-doseq (window windows) + (set-window-point window (point-max)))))) + +(defun cider-log-event--show-stacktrace (framework appender event) + "Show the stacktrace of the log EVENT of FRAMEWORK and APPENDER." + (when (and framework appender event (cider-log-event-exception event)) + (let ((auto-select-buffer cider-auto-select-error-buffer) + (causes nil)) + (cider-request:log-analyze-stacktrace + framework appender event + (lambda (response) + (nrepl-dbind-response response (class status) + (cond (class (setq causes (cons response causes))) + (status (when causes + (cider-stacktrace-render + (cider-popup-buffer cider-error-buffer + auto-select-buffer + #'cider-stacktrace-mode + 'ancillary) + (reverse causes))))))))))) + +(defun cider-log-event-next-line (&optional n) + "Move N lines forward." + (interactive "p") + (let ((n (or n 1))) + (forward-line n) + (beginning-of-line) + (when-let ((framework cider-log-framework) + (appender cider-log-appender) + (event (cider-log-event-at-point))) + (let ((cider-auto-select-error-buffer nil)) + (save-window-excursion + (when (get-buffer-window cider-error-buffer) + (cider-log-event--show-stacktrace framework appender event)) + (when (get-buffer-window cider-inspector-buffer) + (cider-log-event--inspect framework appender event)) + (when (get-buffer-window cider-log-event-buffer) + (cider-log-event--pretty-print framework appender event))))))) + +(defun cider-log-event-previous-line (&optional n) + "Move N lines backward." + (interactive "p") + (cider-log-event-next-line (- (or n 1)))) + +(defun cider-log--set-filters (filters) + "Set the filter variables from the NREPL dict FILTERS." + (when filters + (setq cider-log--end-time-filter (nrepl-dict-get filters "end-time") + cider-log--exceptions-filter (nrepl-dict-get filters "exceptions") + cider-log--level-filter (nrepl-dict-get filters "level") + cider-log--loggers-filter (nrepl-dict-get filters "loggers") + cider-log--pattern-filter (nrepl-dict-get filters "pattern") + cider-log--start-time-filter (nrepl-dict-get filters "start-time") + cider-log--threads-filter (nrepl-dict-get filters "threads")))) + +(defun cider-log--ensure-initialized (framework &optional appender consumer) + "Ensure that the given FRAMEWORK, APPENDER and CONSUMER are initialized." + (setq cider-log-framework framework + cider-log-framework-name (cider-log-framework-name framework)) + (when appender + (setq cider-log-appender appender + cider-log-appender-id (cider-log-appender-id appender) + cider-log-appender-size (cider-log-appender-size appender) + cider-log-appender-threshold (cider-log-appender-threshold appender)) + (cider-log--set-filters (cider-log-appender-filters appender))) + (when consumer + (setq cider-log-consumer consumer) + (cider-log--set-filters (cider-log-consumer-filters appender))) + (when (and appender (not cider-log--initialized-once-p)) + (unless (cider-log-appender-reload framework appender) + (setq cider-log-appender (cider-sync-request:log-add-appender framework appender)) + (setq cider-log--initialized-once-p t)))) + +(defun cider-log-kill-buffer-hook-handler () + "Called from `kill-buffer-hook' to remove the consumer." + (when (eq 'cider-log-mode major-mode) + (when-let ((framework cider-log-framework) + (appender cider-log-appender) + (consumer cider-log-consumer)) + (cider-log--remove-current-buffer-consumer) + (message "Removed %s event consumer %s from appender %s." + (cider-log-framework-display-name framework) + (cider-log-consumer-display-name consumer) + (cider-log-appender-display-name appender))))) + +(defun cider-log-select-framework () + "Select the log framework." + (let ((frameworks (cider-sync-request:log-frameworks))) + (cond ((= 1 (length frameworks)) + (car frameworks)) + (t (let ((name (cider-log--read-framework-name))) + (cider-log-framework-by-name frameworks name)))))) + +(defun cider-log--current-framework () + "Return the log framework by the name bound to `cider-log-framework-name'." + (when cider-log-framework-name + (let ((frameworks (cider-sync-request:log-frameworks))) + (cider-log-framework-by-name frameworks cider-log-framework-name)))) + +(defun cider-log--framework () + "Return the current log framework, or select it." + (or (cider-log--current-framework) (cider-log-select-framework))) + +(defun cider-log--appender () + "Return the current log appender." + (when cider-log-appender-id + (nrepl-dict "id" cider-log-appender-id + "filters" (cider-log--filters) + "size" cider-log-appender-size + "threshold" cider-log-appender-threshold))) + +(defun cider-log--consumer () + "Return the current log consumer." + (let ((consumer (nrepl-dict "filters" (cider-log--filters)))) + (when cider-log-consumer + (nrepl-dict-put consumer "id" (cider-log-consumer-id cider-log-consumer))) + consumer)) + +(defun cider-log--event-options () + "Return the current log consumer." + (nrepl-dict "filters" (cider-log--filters) + "limit" cider-log-pagination-limit + "offset" cider-log-pagination-offset)) + +(defun cider-log--filters () + "Return the log event filters." + (nrepl-dict + "end-time" cider-log--end-time-filter + "exceptions" cider-log--exceptions-filter + "level" cider-log--level-filter + "loggers" cider-log--loggers-filter + "pattern" cider-log--pattern-filter + "start-time" cider-log--start-time-filter + "threads" cider-log--threads-filter)) + +(defun cider-log-event-at-point () + "Return the log event at point." + (get-text-property (point) :cider-log-event)) + +(defun cider-log-info () + "Show the current log buffer, framework, appender and consumer." + (interactive) + (message "%s" + (string-join + (list (when cider-log-buffer + (format "Buffer: %s" (cider-log--bold cider-log-buffer))) + (when cider-log-framework-name + (format "Framework: %s" (cider-log--bold cider-log-framework-name))) + (when cider-log-appender-id + (format "Appender: %s" (cider-log--bold cider-log-appender-id))) + (when-let (id (and cider-log-consumer (cider-log-consumer-id cider-log-consumer))) + (format "Consumer: %s" (cider-log--bold id)))) + " "))) + +;; Major mode + +(defvar cider-log-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map logview-mode-map) + (define-key map (kbd "C-c l a") #'cider-log-appender) + (define-key map (kbd "C-c l c") #'cider-log-consumer) + (define-key map (kbd "C-c l e") #'cider-log-event) + (define-key map (kbd "C-c l f") #'cider-log-framework) + (define-key map (kbd "C-c l i") #'cider-log-info) + (define-key map (kbd "C-c l l") #'cider-log) + (define-key map (kbd "E") 'cider-log-show-stacktrace) + (define-key map (kbd "F") 'cider-log-print-event) + (define-key map (kbd "I") 'cider-log-inspect-event) + (define-key map (kbd "RET") 'cider-log-inspect-event) + (define-key map (kbd "n") 'cider-log-event-next-line) + (define-key map (kbd "p") 'cider-log-event-previous-line) + map) + "The Cider log stream mode key map.") + +(define-derived-mode cider-log-mode logview-mode "Cider Log" + "Major mode for inspecting Clojure log events. + +CIDER Log Mode allows you to capture, debug, inspect and view log events +emitted by Java logging frameworks. The captured log events can be +searched, streamed to the client, pretty printed and are integrated with +the CIDER Inspector and the CIDER stacktrace mode. + +\\{cider-log-mode-map}" + (use-local-map cider-log-mode-map) + (setq-local electric-indent-chars nil) + (setq-local logview-show-ellipses nil) + (setq-local sesman-system 'CIDER) + (setq-local truncate-lines t) + (when (fboundp 'evil-set-initial-state) + (evil-set-initial-state 'cider-log-mode 'emacs))) + +;; Transient Lisp Variable + +(defclass cider-log--lisp-variable (transient-lisp-variable) ()) + +(cl-defmethod transient-init-value ((obj cider-log--lisp-variable)) + "Set the initial value of the object OBJ." + (let* ((prefix-value (oref transient--prefix value)) + (value (cdr (assoc (oref obj variable) prefix-value))) + (new-value (if (assoc (oref obj variable) prefix-value) + value + (symbol-value (oref obj variable))))) + (funcall (oref obj set-value) + (oref obj variable) + (oset obj value new-value)))) + +(cl-defmethod transient-infix-set ((obj cider-log--lisp-variable) value) + "Set the value of infix object OBJ to VALUE." + (funcall (oref obj set-value) + (oref obj variable) + (oset obj value value))) + +(cl-defmethod transient-infix-value ((obj cider-log--lisp-variable)) + "Return the value of the suffix object OBJ." + (cons (oref obj variable) (oref obj value))) + +(cl-defmethod transient-format-value ((obj cider-log--lisp-variable)) + "Format OBJ's value for display and return the result." + (propertize (prin1-to-string (oref obj value)) + 'face 'transient-value)) + +(cl-defmethod transient-format-value ((obj cider-log--lisp-variable)) + "Format OBJ's value for display and return the result." + (cider-log--format-value (oref obj value))) + +;; Transient options + +(transient-define-infix cider-log--appender-size-option () + :always-read t + :argument "--size=" + :class 'cider-log--lisp-variable + :description "Appender size" + :key "=s" + :prompt "Size: " + :reader #'cider-log--read-number-N+ + :variable 'cider-log-appender-size) + +(transient-define-infix cider-log--appender-threshold-option () + :always-read t + :argument "--threshold=" + :class 'cider-log--lisp-variable + :description "Appender threshold" + :key "=t" + :prompt "Threshold: " + :reader #'cider-log--read-number-N+ + :variable 'cider-log-appender-threshold) + +(transient-define-infix cider-log--buffer-option () + :always-read t + :class 'cider-log--lisp-variable + :description "Buffer" + :key "=b" + :prompt "Log buffer: " + :reader #'cider-log--read-buffer + :variable 'cider-log-buffer) + +(transient-define-infix cider-log--end-time-option () + :argument "--end-time=" + :class 'cider-log--lisp-variable + :description "Filter by end time" + :key "-e" + :prompt "End time: " + :reader #'cider-log--read-time + :variable 'cider-log--end-time-filter) + +(transient-define-infix cider-log--exceptions-option () + :argument "--exceptions=" + :class 'cider-log--lisp-variable + :description "Filter by exceptions" + :key "-E" + :multi-value t + :prompt "Exceptions: " + :reader #'cider-log--read-exceptions + :variable 'cider-log--exceptions-filter) + +(transient-define-infix cider-log--level-option () + :argument "--level=" + :class 'cider-log--lisp-variable + :description "Filter by level" + :key "-l" + :prompt "Log Level: " + :reader #'cider-log--read-level + :variable 'cider-log--level-filter) + +(transient-define-infix cider-log--limit-option () + :always-read t + :argument "--limit=" + :class 'cider-log--lisp-variable + :description "Limit" + :key "=l" + :prompt "Limit: " + :reader #'cider-log--read-number-N+ + :variable 'cider-log-pagination-limit) + +(transient-define-infix cider-log--logger-option () + :argument "--loggers=" + :class 'cider-log--lisp-variable + :description "Filter by loggers" + :key "-L" + :multi-value t + :prompt "Loggers: " + :reader #'cider-log--read-loggers + :variable 'cider-log--loggers-filter) + +(transient-define-infix cider-log--offset-option () + :always-read t + :argument "--offset=" + :class 'cider-log--lisp-variable + :description "Offset" + :key "=o" + :prompt "Offset: " + :reader #'cider-log--read-number-N0 + :variable 'cider-log-pagination-offset) + +(transient-define-infix cider-log--pattern-option () + :argument "--pattern=" + :class 'cider-log--lisp-variable + :description "Filter by regex pattern" + :key "-r" + :prompt "Regex pattern: " + :reader #'read-string + :variable 'cider-log--pattern-filter) + +(transient-define-infix cider-log--start-time-option () + :argument "--start-time=" + :class 'cider-log--lisp-variable + :description "Filter by start time" + :key "-s" + :prompt "Start time: " + :reader #'cider-log--read-time + :variable 'cider-log--start-time-filter) + +(transient-define-infix cider-log--threads-option () + :argument "--threads=" + :class 'cider-log--lisp-variable + :description "Filter by threads" + :key "-t" + :multi-value t + :prompt "Threads: " + :reader #'cider-log--read-threads + :variable 'cider-log--threads-filter) + +;; Framework actions + +(transient-define-suffix cider-log-browse-javadocs (framework) + "Browse the Javadoc of the log FRAMEWORK." + :description "Browse Java documentation" + (interactive (list (cider-log--framework))) + (browse-url (or (cider-log-framework-javadoc-url framework) + (user-error (format "The %s framework does not have Javadocs." + (cider-log-framework-name framework)))))) + +(transient-define-suffix cider-log-browse-website (framework) + "Browse the website of the log FRAMEWORK." + :description "Browse website" + (interactive (list (cider-log--framework))) + (browse-url (or (cider-log-framework-website-url framework) + (user-error (format "The %s framework does not have a website." + (cider-log-framework-name framework)))))) + +(transient-define-suffix cider-log-set-framework (framework-name) + "Set the current log framework to FRAMEWORK-NAME." + :description #'cider-log--description-set-framework + (interactive (list (cider-log--read-framework-name))) + (setq cider-log-framework-name framework-name)) + +(transient-define-suffix cider-log-set-buffer (buffer) + "Set the current log buffer to BUFFER." + :description #'cider-log--description-set-buffer + (interactive (list (cider-log--read-buffer))) + (setq cider-log-buffer buffer)) + +;; Appender actions + +(transient-define-suffix cider-log-clear-appender (framework appender) + "Clear the log events of the APPENDER of FRAMEWORK." + :description "Clear log appender" + :inapt-if-not #'cider-log-appender-attached-p + (interactive (list (cider-log--framework) (cider-log--appender))) + (cider-sync-request:log-clear framework appender) + (message "Cleared the %s log appender of the %s framework." + (cider-log-appender-display-name appender) + (cider-log-framework-display-name framework))) + +(transient-define-suffix cider-log-kill-appender (framework appender) + "Remove the log APPENDER from FRAMEWORK." + :description "Kill log appender" + :inapt-if-not #'cider-log-appender-attached-p + (interactive (list (cider-log--framework) (cider-log--appender))) + (cider-sync-request:log-remove-appender framework appender) + (setq-local cider-log-consumer nil) + (message "Log appender %s removed from the %s framework." + (cider-log-framework-display-name framework) + (cider-log-appender-display-name appender))) + +(transient-define-suffix cider-log--do-add-appender (framework appender) + "Add the APPENDER to the log FRAMEWORK." + :description "Add log appender" + :inapt-if #'cider-log-appender-attached-p + (interactive (list (cider-log--framework) (cider-log--appender))) + (setq cider-log-appender (cider-sync-request:log-add-appender framework appender)) + (message "Log appender %s added to the %s framework." + (cider-log-appender-display-name appender) + (cider-log-framework-display-name framework))) + +(transient-define-suffix cider-log--do-update-appender (framework appender) + "Update the APPENDER of the log FRAMEWORK." + :description "Update log appender" + :inapt-if-not #'cider-log-appender-attached-p + (interactive (list (cider-log--framework) (cider-log--appender))) + (setq cider-log-appender (cider-sync-request:log-update-appender framework appender)) + (message "Updated log appender %s of the %s framework." + (cider-log-appender-display-name appender) + (cider-log-framework-display-name framework))) + +;; Consumer actions + +(transient-define-suffix cider-log--do-add-consumer (framework appender consumer buffer) + "Add the CONSUMER to the APPENDER of the log FRAMEWORK and write events to BUFFER." + :description "Add log consumer" + :inapt-if #'cider-log-consumer-attached-p + (interactive (list (cider-log--framework) + (cider-log--appender) + (cider-log--consumer) + (current-buffer))) + (cider-log--consumer-add framework appender consumer buffer)) + +(transient-define-suffix cider-log-kill-consumer (framework appender consumer) + "Remove the CONSUMER listening to the APPENDER of the log FRAMEWORK." + :description "Kill log consumer" + :inapt-if-not #'cider-log-consumer-attached-p + (interactive (list (cider-log--framework) (cider-log--appender) (cider-log--consumer))) + (cider-sync-request:log-remove-consumer framework appender consumer) + (setq-local cider-log-consumer nil) + (message "Removed %s log consumer %s for appender %s." + (cider-log-framework-display-name framework) + (cider-log-consumer-display-name consumer) + (cider-log-appender-display-name appender))) + +(transient-define-suffix cider-log--do-update-consumer (framework appender consumer) + "Update the CONSUMER listening to the APPENDER of the log FRAMEWORK." + :description "Update log consumer" + :inapt-if-not #'cider-log-consumer-attached-p + (interactive (list (cider-log--framework) + (cider-log--appender) + (cider-log--consumer))) + (setq cider-log-consumer (cider-sync-request:log-update-consumer framework appender consumer)) + (message "Updated %s log consumer %s for appender %s." + (cider-log-framework-display-name framework) + (cider-log-consumer-display-name consumer) + (cider-log-appender-display-name appender))) + +;; Event actions + +(transient-define-suffix cider-log-show-stacktrace (framework appender event) + "Show the stacktrace of the log EVENT of FRAMEWORK and APPENDER." + :description "Show log event stacktrace" + :if #'cider-log-event-at-point + :inapt-if-not (lambda () + (when-let (event (cider-log-event-at-point)) + (cider-log-event-exception event))) + (interactive (list (cider-log--framework) (cider-log--appender) (cider-log-event-at-point))) + (cider-log-event--show-stacktrace framework appender event)) + +(transient-define-suffix cider-log-print-event (framework appender event) + "Format the log EVENT of FRAMEWORK and APPENDER." + :description "Pretty print log event at point" + :if #'cider-log-event-at-point + (interactive (list (cider-log--framework) (cider-log--appender) (cider-log-event-at-point))) + (if event + (cider-log-event--pretty-print framework appender event) + (user-error "No log event found at point"))) + +(transient-define-suffix cider-log-inspect-event (framework appender event) + "Inspect the log EVENT of FRAMEWORK and APPENDER." + :description "Inspect log event at point" + :if #'cider-log-event-at-point + (interactive (list (cider-log--framework) (cider-log--appender) (cider-log-event-at-point))) + (cider-log-event--inspect framework appender event)) + +(transient-define-suffix cider-log-clear-event-buffer (buffer) + "Clear the Cider log events in BUFFER." + :description #'cider-log--description-clear-events-buffer + :inapt-if-not #'cider-log-buffer-clear-p + (interactive (list cider-log-buffer)) + (when (get-buffer buffer) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (erase-buffer))))) + +(transient-define-suffix cider-log-switch-to-buffer (buffer) + "Switch to the Cider log event BUFFER." + :description "Switch to the log event buffer" + (interactive (list cider-log-buffer)) + (when (get-buffer-create buffer) + (switch-to-buffer-other-window buffer))) + +(transient-define-suffix cider-log--do-search-events (framework appender filters) + "Search the log events of FRAMEWORK and APPENDER which match FILTERS." + :description "Search log events" + :inapt-if-not #'cider-log-appender-attached-p + (interactive (list (cider-log--framework) (cider-log--appender) (cider-log--filters))) + (with-current-buffer (get-buffer-create cider-log-buffer) + (let ((consumer (nrepl-dict "filters" (cider-log--filters))) + (inhibit-read-only t)) + (cider-log--remove-current-buffer-consumer) + (erase-buffer) + (let ((events (cider-sync-request:log-search + framework appender + :filters filters + :limit cider-log-pagination-limit + :offset cider-log-pagination-offset))) + (seq-doseq (event (nreverse events)) + (insert (cider-log-event--format-logback event))) + (cider-log-mode) + (setq-local cider-log-framework framework) + (setq-local cider-log-appender appender) + (when (seq-empty-p events) + (message "No log events found.")) + (cider-log--consumer-add framework appender consumer (current-buffer)))))) + +;; Log Framework Transient + +;;;###autoload (autoload 'cider-log-framework "cider-log" "Show the Cider log framework menu." t) +(transient-define-prefix cider-log-framework (framework) + "Show the Cider log framework menu." + [["Cider Log Framework\n\nActions:" + ("b" cider-log-set-buffer) + ("j" cider-log-browse-javadocs) + ("s" cider-log-set-framework) + ("w" cider-log-browse-website)]] + (interactive (list (cider-log--framework))) + (cider-log--ensure-initialized framework) + (transient-setup 'cider-log-framework)) + +;; Log Appender Transients + +(defun cider-log--appender-interactive-list () + "Return the interactive arguments for a appender transient." + (let ((framework (cider-log--current-framework))) + (list framework (cider-log-framework-appender framework cider-log-appender-id)))) + +(transient-define-prefix cider-log-add-appender (framework appender) + "Show the menu to add a Cider log appender." + :history-key 'cider-log-appender + ["Cider Log Appender\n\nSettings:" + (cider-log--appender-size-option) + (cider-log--appender-threshold-option)] + ["Filters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("a" cider-log--do-add-appender)] + (interactive (cider-log--appender-interactive-list)) + (cider-log--ensure-initialized framework appender) + (transient-setup 'cider-log-add-appender)) + +(transient-define-prefix cider-log-update-appender (framework appender) + "Show the menu to update a Cider log appender." + :history-key 'cider-log-appender + ["Cider Log Appender\n\nSettings:" + (cider-log--appender-size-option) + (cider-log--appender-threshold-option)] + ["Filters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("u" cider-log--do-update-appender)] + (interactive (cider-log--appender-interactive-list)) + (cider-log--ensure-initialized framework appender) + (transient-setup 'cider-log-update-appender)) + +;;;###autoload (autoload 'cider-log-appender "cider-log" "Show the Cider log appender menu." t) +(transient-define-prefix cider-log-appender (framework appender) + "Show the Cider log appender menu." + :history-key 'cider-log-appender + ["Cider Log Appender\n\nSettings:" + (cider-log--appender-size-option) + (cider-log--appender-threshold-option)] + ["Filters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("a" cider-log--do-add-appender) + ("c" cider-log-clear-appender) + ("k" cider-log-kill-appender) + ("u" cider-log--do-update-appender)] + (interactive (cider-log--appender-interactive-list)) + (cider-log--ensure-initialized framework appender) + (transient-setup 'cider-log-appender)) + +;; Log Consumer Transient Menu + +(defun cider-log--consumer-interactive-list () + "Return the interactive arguments for a consumer transient." + (let* ((framework (cider-log--current-framework)) + (appender (cider-log-framework-appender framework cider-log-appender-id))) + (list framework appender + (if (and appender cider-log-consumer) + (seq-find (lambda (consumer) + (equal (cider-log-consumer-id cider-log-consumer) + (cider-log-consumer-id consumer))) + (cider-log-appender-consumers appender)) + (nrepl-dict "filters" (cider-log--filters)))))) + +(transient-define-prefix cider-log-add-consumer (framework appender consumer) + "Show the menu to add a Cider log consumer." + :history-key 'cider-log-consumer + ["Cider Log Consumer\n\nFilters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("a" cider-log--do-add-consumer)] + (interactive (cider-log--consumer-interactive-list)) + (cider-log--ensure-initialized framework appender consumer) + (transient-setup 'cider-log-add-consumer)) + +(transient-define-prefix cider-log-update-consumer (framework appender consumer) + "Show the menu to update a Cider log consumer." + :history-key 'cider-log-consumer + ["Cider Log Consumer\n\nFilters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("u" cider-log--do-update-consumer)] + (interactive (cider-log--consumer-interactive-list)) + (cider-log--ensure-initialized framework appender consumer) + (transient-setup 'cider-log-update-consumer)) + +;;;###autoload (autoload 'cider-log-consumer "cider-log" "Show the Cider log consumer menu." t) +(transient-define-prefix cider-log-consumer (framework appender consumer) + "Show the Cider log consumer menu." + :history-key 'cider-log-consumer + ["Cider Log Consumer\n\nFilters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("a" cider-log--do-add-consumer) + ("k" cider-log-kill-consumer) + ("u" cider-log--do-update-consumer)] + (interactive (cider-log--consumer-interactive-list)) + (cider-log--ensure-initialized framework appender consumer) + (transient-setup 'cider-log-consumer)) + +;; Log Event Transient Menu + +(transient-define-prefix cider-log-event-search (framework appender) + "Search the search log events menu." + :history-key 'cider-log-event + ["Cider Log Event\n\nPagination:" + (cider-log--limit-option) + (cider-log--offset-option)] + ["Filters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("s" cider-log--do-search-events)] + (interactive (list (cider-log--framework) (cider-log--appender))) + (cider-log--ensure-initialized framework appender) + (transient-setup 'cider-log-event-search)) + +;;;###autoload (autoload 'cider-log-event "cider-log" "Show the Cider log event menu." t) +(transient-define-prefix cider-log-event (framework appender) + "Show the Cider log event menu." + :history-key 'cider-log-event + ["Cider Log Event\n\nPagination:" + (cider-log--limit-option) + (cider-log--offset-option)] + ["Filters:" + (cider-log--end-time-option) + (cider-log--exceptions-option) + (cider-log--level-option) + (cider-log--logger-option) + (cider-log--pattern-option) + (cider-log--start-time-option) + (cider-log--threads-option)] + ["Actions" + ("c" cider-log-clear-event-buffer) + ("e" cider-log-show-stacktrace) + ("i" cider-log-inspect-event) + ("p" cider-log-print-event) + ("s" cider-log--do-search-events)] + (interactive (list (cider-log--framework) (cider-log--appender))) + (cider-log--ensure-initialized framework appender) + (transient-setup 'cider-log-event)) + +;; Main Transient Menu + +;;;###autoload (autoload 'cider-log "cider-log" "Show the Cider log menu." t) +(transient-define-prefix cider-log (framework appender) + "Show the Cider log menu." + [["Framework Actions" + ("fs" cider-log-set-framework) + ("fb" cider-log-set-buffer) + ("fj" cider-log-browse-javadocs) + ("fw" cider-log-browse-website)] + ["Appender Actions" + ("aa" "Add log appender" cider-log-add-appender + :inapt-if cider-log-appender-attached-p) + ("ac" cider-log-clear-appender) + ("ak" cider-log-kill-appender) + ("am" "Manage appender" cider-log-appender) + ("au" "Update log appender" cider-log-update-appender + :inapt-if-not cider-log-appender-attached-p)] + ["Consumer Actions" + ("ca" "Add log consumer" cider-log-add-consumer + :inapt-if cider-log-consumer-attached-p) + ("ck" cider-log-kill-consumer) + ("cm" "Manage consumer" cider-log-consumer) + ("cu" "Update log consumer" cider-log-update-consumer + :inapt-if-not cider-log-consumer-attached-p)] + ["Event Actions" + ("eb" cider-log-switch-to-buffer) + ("ec" cider-log-clear-event-buffer) + ("ee" cider-log-show-stacktrace) + ("ei" cider-log-inspect-event) + ("ep" cider-log-print-event) + ("es" "Search log events" cider-log-event-search + :inapt-if-not cider-log-appender-attached-p)]] + (interactive (list (cider-log--framework) (cider-log--appender))) + (cider-log--ensure-initialized framework appender) + (transient-setup 'cider-log)) + +(add-hook 'kill-buffer-hook #'cider-log-kill-buffer-hook-handler) + +(provide 'cider-log) + +;;; cider-log.el ends here diff --git a/cider-mode.el b/cider-mode.el index 94a854a8e..2d7234ec5 100644 --- a/cider-mode.el +++ b/cider-mode.el @@ -33,6 +33,7 @@ (require 'clojure-mode) (require 'cider-eval) +(require 'cider-log) (require 'cider-test) ; required only for the menu (require 'cider-eldoc) (require 'cider-resolve) @@ -529,6 +530,12 @@ higher precedence." (define-key map (kbd "C-c C-? C-d") #'cider-xref-fn-deps-select) (define-key map (kbd "C-c C-q") #'cider-quit) (define-key map (kbd "C-c M-r") #'cider-restart) + (define-key map (kbd "C-c l a") #'cider-log-appender) + (define-key map (kbd "C-c l c") #'cider-log-consumer) + (define-key map (kbd "C-c l e") #'cider-log-event) + (define-key map (kbd "C-c l f") #'cider-log-framework) + (define-key map (kbd "C-c l i") #'cider-log-info) + (define-key map (kbd "C-c l l") #'cider-log) (dolist (variable '(cider-mode-interactions-menu cider-mode-eval-menu cider-mode-menu)) diff --git a/cider.el b/cider.el index dbcad2f57..476c6cd77 100644 --- a/cider.el +++ b/cider.el @@ -12,7 +12,7 @@ ;; Maintainer: Bozhidar Batsov ;; URL: http://www.github.com/clojure-emacs/cider ;; Version: 1.8.0-snapshot -;; Package-Requires: ((emacs "26") (clojure-mode "5.16.0") (parseedn "1.0.6") (queue "0.2") (spinner "1.7") (seq "2.22") (sesman "0.3.2")) +;; Package-Requires: ((emacs "26") (clojure-mode "5.16.0") (parseedn "1.0.6") (queue "0.2") (spinner "1.7") (seq "2.22") (sesman "0.3.2") (logview "0.16.1") (transient "0.4.1")) ;; Keywords: languages, clojure, cider ;; This program is free software: you can redistribute it and/or modify diff --git a/doc/modules/ROOT/assets/images/cider-log.png b/doc/modules/ROOT/assets/images/cider-log.png new file mode 100644 index 000000000..6584599c5 Binary files /dev/null and b/doc/modules/ROOT/assets/images/cider-log.png differ diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index d22f943b6..121efcc78 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -38,6 +38,7 @@ ** xref:debugging/debugger.adoc[Debugger] ** xref:debugging/enlighten.adoc[Enlighten] ** xref:debugging/inspector.adoc[Inspector] +** xref:debugging/logging.adoc[Logging] ** xref:debugging/macroexpansion.adoc[Macroexpansion] ** xref:debugging/profiling.adoc[Profiling] ** xref:debugging/tracing.adoc[Tracing] diff --git a/doc/modules/ROOT/pages/debugging/logging.adoc b/doc/modules/ROOT/pages/debugging/logging.adoc new file mode 100644 index 000000000..3d94b6af0 --- /dev/null +++ b/doc/modules/ROOT/pages/debugging/logging.adoc @@ -0,0 +1,274 @@ += Logging +:experimental: + +CIDER Log Mode allows you to capture, debug, inspect and view log +events emitted by Java logging frameworks. The captured log events can +be searched, streamed to the client, pretty printed and are integrated +with the CIDER link:inspector.html[Inspector] and +link:../usage/dealing_with_errors.html[Stacktrace Mode]. Here is a +screenshot of CIDER Log Mode in action. + +image::cider-log.png[CIDER Log] + +NOTE: The screenshot displays the list of log events in the +`+*cider-log*+` buffer on the left. To the right, a log event is +visible in the `+*cider-inspect*+` buffer, where the exception of the +event is also displayed in the CIDER Stacktrace Mode. From the +Stacktrace Mode buffer you can jump to the source of each frame. At +the bottom the CIDER log menu is shown from which you can perform +logging related actions. + +== Features + +- Browse Javadocs and website of log framework. +- Search log events and show them in buffers. +- link:../usage/pretty_printing.html[Pretty Print] log events. +- Show log events in the CIDER link:inspector.html[Inspector] +- Show log event exceptions in the CIDER link:../usage/dealing_with_errors.html[Stacktrace Mode] + +== Usage + +To use CIDER Log Mode, type kbd:[C-c l l] or kbd:[M-x cider-log] in +any buffer that has a CIDER https://github.com/vspinu/sesman[Sesman] +session attached to it. The first time you run the command, it will +prompt you to select a log framework to use, and then attach a log +appender to the root logger of the selected framework. After the log +appender has been attached, the `cider-log` command will show a +https://www.gnu.org/software/emacs/manual/html_mono/transient.html[Transient] +menu, from which you can take further actions, like managing the log +framework, appenders, consumers and events. + +To view log events and stream them to your client, type kbd:[es] +(Search log events) followed by kbd:[s]. This will open the +`+*cider-log*+` buffer showing any log events captured thus far. It will +also add a log consumer to this buffer, which receives newly-arriving +log events. + +NOTE: The `+*cider-log*+` buffer might initially be empty, and you may +see a `No log events found` message. This is because nothing has been +logged between adding the appender and searching for events. So, now +would be a good time to run some code that triggers a log event for +the selected framework. + +=== Keybindings + +|=== +| Command | Keyboard shortcut | Description + +| `cider-log` +| kbd:[C-c l l] +| Show the CIDER log menu. + +| `cider-log-framework` +| kbd:[C-c l f] +| Show the menu to manage a logging framework. + +| `cider-log-appender` +| kbd:[C-c l a] +| Show the menu to manage appenders of a logging framework. + +| `cider-log-consumer` +| kbd:[C-c l c] +| Show the menu to manage consumers listening to log events. + +| `cider-log-event` +| kbd:[C-c l e] +| Show the menu to manage log events. +|=== + +== Log framework + +CIDER Log Mode supports log frameworks that allow reconfiguration at +run time. More specifically the framework should support attaching log +appenders to loggers, in order to capture events. + +At the moment the following log frameworks are supported: + +- https://docs.oracle.com/en/java/javase/19/core/java-logging-overview.html[Java Util Logging] +- https://logback.qos.ch[Logback] + +There is some https://github.com/clojure-emacs/logjam/issues/2[work in +progress] to support https://logging.apache.org/log4j/2.x/[Log4j] as +well, but there are some +https://stackoverflow.com/a/17842174/12711900[difficulties] with +configuration changes made at runtime, which are wiped out by the +Log4j2 reconfiguration mechanism. + +=== Keybindings + +|=== +| Command | Keyboard shortcut | Description + +| `cider-log-set-framework` +| kbd:[C-c l f s] +| Select the log framework to use. + +| `cider-log-set-buffer` +| kbd:[C-c l f b] +| Select the log buffer to user. Default: `+*cider-log*+` + +| `cider-log-browse-javadocs` +| kbd:[C-c l f j] +| Browse the Javadocs of the log framework. + +| `cider-log-browse-website` +| kbd:[C-c l f w] +| Browse the website of the log framework. +|=== + +== Log Appender + +In order to capture log events, a log appender needs to be attached to +a logger of a framework. Once an appender is attached to a logger it +captures the log events emitted by the framework in an in-memory +atom. A log appender can be configured to have a certain size +(default: 100000) and a threshold in percentage (default: 10). Log +events are cleared from the appender when threshold (appender size +plus threshold) is reached. Additionally an appender can be configured +to only capture events that match a set of filters. + +=== Keybindings + +The following keybindings can be used to interact with log appenders. + +|=== +| Command | Keyboard shortcut | Description + +| `cider-log-appender` +| kbd:[C-c l a] +| Show the transient menu to manage log appenders. + +| `cider-log-add-appender` +| kbd:[C-c l a a] +| Add a log appender to a logger. + +| `cider-log-clear-appender` +| kbd:[C-c l a c] +| Clear all captured events of a log appender. + +| `cider-log-kill-appender` +| kbd:[C-c l a k] +| Kill a log appender by removing it from the logger. + +| `cider-log-update-appender` +| kbd:[C-c l a u] +| Update the filters, size or threshold of a log appender. +|=== + +== Log Consumer + +Log events can be streamed to a client by attaching a log consumer to +an appender. Once a log consumer has been attached to an appender, it +will receive events from the appender. Similar to log appenders, +consumers can also be configured with a set of filters to only receive +certain events. + +=== Keybindings + +The following keybindings can be used to interact with log consumers. + +|=== +| Command | Main / Consumer Menu | Keyboard shortcut | Description + +| `cider-log-consumer` +| +| kbd:[C-c l c] +| Show the transient menu to manage log consumers. + +| `cider-log-add-consumer` +| kbd:[ca] / kbd:[a] +| kbd:[C-c l c a] +| Add a log consumer to a log appender streaming event to the client. + +| `cider-log-kill-consumer` +| kbd:[ck] / kbd:[k] +| kbd:[C-c l c k] +| Kill a log consumer and stop streaming events to the client. + +| `cider-log-update-consumer` +| kbd:[cu] / kbd:[u] +| kbd:[C-c l c u] +| Update the filters of a log consumer to change which events are streamed to the client. +|=== + +== Log Event + +Log events can be searched, streamed to a client or viewed in CIDER's +Inspector and Stacktrace Mode. When searching log events the user can +specify a set of filters. Events that match the filters are shown in +the `+*cider-log*+` buffer. Additionally a log consumer will be +attached to the appender to receive log events matching the search +criteria after the search command has been issued. The log appender +will be removed automatically once a new search has been submitted or +when the `+*cider-log*+` buffer gets killed. + +=== Keybindings + +The following keybindings can be used to interact with log events. + +|=== +| Command | Keyboard shortcut | Description + +| `cider-log-event` +| kbd:[C-c l e] +| Show the transient menu to manage log events. + +| `cider-log-clear-event-buffer` +| kbd:[C-c l e c] +| Clear all events from the log event buffer. + +| `cider-log-show-stacktrace` +| kbd:[C-c l e e] +| Show the stacktrace of the log event at point in the CIDER Stacktrace Mode. + +| `cider-log-inspect-event` +| kbd:[C-c l e i] +| Show the log event in the CIDER Inspector. + +| `cider-log-print-event` +| kbd:[C-c l e p] +| Pretty print the log event in the `+*cider-log-event*+` buffer. + +| `cider-log-event-search` +| kbd:[C-c l e s] +| Search log events and show them in the `+*cider-log*+` buffer. +|=== + +== Log Filters + +Filters for log events can be attached to log appenders and +consumers. They also take effect when searching events or streaming +them to clients. If multiple filters are chosen they are combined +using logical AND condition. The following filters are available: + +|=== +| Filter | Keyboard shortcut | Description + +| `end-time` +| kbd:[-e] +| Only include log events that were emitted before `end-time`. + +| `exceptions` +| kbd:[-E] +| Only include log events caused by an exception in the list of `exceptions`. + +| `level` +| kbd:[-l] +| Only include log events with a log level above `level`. + +| `loggers` +| kbd:[-L] +| Only include log events that were emitted by a logger in the list of `loggers`. + +| `pattern` +| kbd:[-r] +| Only include log events whose message matcches the regular expression `pattern`. + +| `start-time` +| kbd:[-s] +| Only include log events that were emitted at, or after `start-time`. + +| `threads` +| kbd:[-t] +| Only include log events that were emitted by a thread in the list of `threads`. +|=== diff --git a/test/cider-log-test.el b/test/cider-log-test.el new file mode 100644 index 000000000..662f9fc06 --- /dev/null +++ b/test/cider-log-test.el @@ -0,0 +1,52 @@ +;;; cider-log-tests.el -*- lexical-binding: t; -*- + +;; Copyright © 2023 Bozhidar Batsov and CIDER contributors + +;; Author: r0man + +;; This file is NOT part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or +;; modify it under the terms of the GNU General Public License as +;; published by the Free Software Foundation, either version 3 of the +;; License, or (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see `http://www.gnu.org/licenses/'. + +;;; Commentary: + +;; This file is part of CIDER + +;;; Code: + +(require 'buttercup) +(require 'cider-log) + +(describe "cider-log" + (let ((framework (nrepl-dict "id" "jul" "name" "Java Util Logging")) + (appender (nrepl-dict "id" "cider-log"))) + + (it "raises user-error when cider is not connected." + (spy-on 'cider-connected-p :and-return-value nil) + (expect (cider-log framework appender) :to-throw 'user-error)) + + (it "doesn't add an appender when initialized." + (let ((cider-log--initialized-once-p t)) + (spy-on 'cider-sync-request:log-frameworks :and-return-value (list framework)) + (spy-on 'transient-setup) + (cider-log framework appender) + (expect 'transient-setup :to-have-been-called-with 'cider-log))) + + (it "does add an appender when not initialized." + (let ((cider-log--initialized-once-p nil)) + (spy-on 'cider-sync-request:log-frameworks :and-return-value (list framework)) + (spy-on 'cider-sync-request:log-add-appender :and-return-value appender) + (spy-on 'transient-setup) + (cider-log framework appender) + (expect 'transient-setup :to-have-been-called-with 'cider-log)))))