From c3204572b0e696beec3da6d3eff7538b4f7e02b1 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Mon, 12 Jun 2023 11:28:01 +0200 Subject: [PATCH 01/20] Add CIDER log middleware --- .clj-kondo/config.edn | 4 + README.md | 2 +- eastwood.clj | 8 + project.clj | 13 +- src/cider/log/appender.clj | 133 ++++++ src/cider/log/event.clj | 71 +++ src/cider/log/framework.clj | 144 +++++++ src/cider/log/framework/jul.clj | 99 +++++ src/cider/log/framework/log4j2.clj | 119 ++++++ src/cider/log/framework/logback.clj | 110 +++++ src/cider/log/repl.clj | 316 ++++++++++++++ src/cider/log/specs.clj | 286 +++++++++++++ src/cider/nrepl.clj | 130 ++++++ src/cider/nrepl/middleware.clj | 1 + src/cider/nrepl/middleware/log.clj | 293 +++++++++++++ test/clj/cider/log/appender_test.clj | 60 +++ test/clj/cider/log/event_test.clj | 86 ++++ test/clj/cider/log/framework_test.clj | 63 +++ test/clj/cider/log/repl_test.clj | 161 +++++++ test/clj/cider/log/test.clj | 17 + test/clj/cider/nrepl/middleware/log_test.clj | 427 +++++++++++++++++++ test/resources/logback-test.xml | 16 + test/resources/logging.properties | 59 +++ 23 files changed, 2615 insertions(+), 3 deletions(-) create mode 100644 src/cider/log/appender.clj create mode 100644 src/cider/log/event.clj create mode 100644 src/cider/log/framework.clj create mode 100644 src/cider/log/framework/jul.clj create mode 100644 src/cider/log/framework/log4j2.clj create mode 100644 src/cider/log/framework/logback.clj create mode 100644 src/cider/log/repl.clj create mode 100644 src/cider/log/specs.clj create mode 100644 src/cider/nrepl/middleware/log.clj create mode 100644 test/clj/cider/log/appender_test.clj create mode 100644 test/clj/cider/log/event_test.clj create mode 100644 test/clj/cider/log/framework_test.clj create mode 100644 test/clj/cider/log/repl_test.clj create mode 100644 test/clj/cider/log/test.clj create mode 100644 test/clj/cider/nrepl/middleware/log_test.clj create mode 100644 test/resources/logback-test.xml create mode 100644 test/resources/logging.properties diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 3122f411e..87524e40a 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -1,5 +1,9 @@ {:hooks {:analyze-call {cider.nrepl.middleware.out/with-out-binding hooks.core/with-out-binding}} + :lint-as {cider.log.repl-test/with-each-framework clojure.core/let + cider.nrepl.middleware.log-test/with-each-framework clojure.core/let + clojure.test.check.clojure-test/defspec clojure.test/deftest + clojure.test.check.properties/for-all clojure.core/let} :linters {:unresolved-symbol {:exclude [(cider.nrepl/def-wrapper) (cider.nrepl.middleware.util.instrument/definstrumenter) (cider.nrepl.middleware.util.instrument/with-break) diff --git a/README.md b/README.md index 70569f9d0..d8c8790c5 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ to be modified in the `target/srcdeps` directory. When you want to release locally: ``` -lein with-profile plugin.mranderson/config install +lein with-profile +plugin.mranderson/config install ``` #### Using the Makefile diff --git a/eastwood.clj b/eastwood.clj index 5b2a77604..04468d479 100644 --- a/eastwood.clj +++ b/eastwood.clj @@ -12,3 +12,11 @@ (disable-warning {:linter :deprecations :symbol-matches #{#"^public final void java\.lang\.Thread\.stop\(\)$"}}) + +(disable-warning + {:linter :deprecations + :symbol-matches #{#"^public long java\.lang\.Thread\.getId\(\)$"}}) + +(disable-warning + {:linter :deprecations + :symbol-matches #{#"^public int java\.util\.logging\.LogRecord\.getThreadID\(\)$"}}) diff --git a/project.clj b/project.clj index 5a4bc75d4..3a93fbff7 100644 --- a/project.clj +++ b/project.clj @@ -34,6 +34,8 @@ :filespecs [{:type :bytes :path "cider/cider-nrepl/project.clj" :bytes ~(slurp "project.clj")}] + :jvm-opts ["-Djava.util.logging.config.file=test/resources/logging.properties"] + :source-paths ["src"] :resource-paths ["resources"] :test-paths ["test/clj" "test/cljs" "test/common"] @@ -69,17 +71,24 @@ :profiles {:provided {:dependencies [[org.clojure/clojure "1.10.3"] [org.clojure/clojurescript "1.11.4" :scope "provided"] + [org.clojure/test.check "1.1.1"] + ;; 1.3.7 and 1.4.7 are working, but we need 1.3.7 for JDK8 + [ch.qos.logback/logback-classic "1.3.7"] [com.cognitect/transit-clj "1.0.324"] [com.fasterxml.jackson.core/jackson-core "2.13.1"] [commons-codec "1.15"] [com.cognitect/transit-java "1.0.343"] [com.google.errorprone/error_prone_annotations "2.11.0"] - [com.google.code.findbugs/jsr305 "3.0.2"]] + [com.google.code.findbugs/jsr305 "3.0.2"] + [org.apache.logging.log4j/log4j-api "2.20.0"] + [org.apache.logging.log4j/log4j-core "2.20.0"]] :test-paths ["test/spec"]} :1.8 {:dependencies [[org.clojure/clojure "1.8.0"] [org.clojure/clojurescript "1.10.520" :scope "provided"] - [javax.xml.bind/jaxb-api "2.3.1" :scope "provided"]]} + [javax.xml.bind/jaxb-api "2.3.1" :scope "provided"] + ;; Leiningen 2.10.0 throws NoClassDefFoundError (only in Clojure 1.8) + [commons-logging/commons-logging "1.2" :scope "provided"]]} :1.9 {:dependencies [[org.clojure/clojure "1.9.0"] [org.clojure/clojurescript "1.10.520" :scope "provided"] [javax.xml.bind/jaxb-api "2.3.1" :scope "provided"]] diff --git a/src/cider/log/appender.clj b/src/cider/log/appender.clj new file mode 100644 index 000000000..010e62b10 --- /dev/null +++ b/src/cider/log/appender.clj @@ -0,0 +1,133 @@ +(ns cider.log.appender + (:require [cider.log.event :as event])) + +(def ^:private default-size + "The default size of the appender." + 100000) + +(def ^:private default-threshold + "The default threshold of the appender in percentage." + 10) + +(defn- garbage-collect? + "Whether to garbage collect events, or not." + [{:keys [event-index size threshold]}] + (> (count event-index) (+ size (* size (/ threshold 100.0))))) + +(defn- garbage-collect-events + "Garbage collect some events of the `appender`." + [{:keys [events event-index size] :as appender}] + (if (garbage-collect? appender) + (assoc appender + :events (take size events) + :event-index (apply dissoc event-index (map :id (drop size events)))) + appender)) + +(defn- add-event? + "Whether the `event` should be added to the appender." + [{:keys [filter-fn]} event] + (or (nil? filter-fn) (filter-fn event))) + +(defn- notify-consumers + [{:keys [consumers] :as appender} event] + (doseq [{:keys [callback filter-fn] :as consumer} (vals consumers) + :when (filter-fn event)] + (callback consumer event)) + appender) + +(defn- enqueue-event + "Enqueue the `event` to the event list of `appender`." + [appender event] + (update appender :events #(cons event %))) + +(defn- index-event + "Add the `event` to the index of `appender`." + [appender event] + (assoc-in appender [:event-index (:id event)] event)) + +(defn add-consumer + "Add the `consumer` to the `appender`." + [appender {:keys [id filters] :as consumer}] + (assert (not (get-in appender [:consumers id])) + (format "Consumer %s already registered" id)) + (assoc-in appender [:consumers id] + (-> (select-keys consumer [:callback :filters :id]) + (assoc :filter-fn (event/search-filter (:levels appender) filters))))) + +(defn add-event + "Add the `event` to the `appender`." + [appender event] + (if (add-event? appender event) + (-> (enqueue-event appender event) + (index-event event) + (notify-consumers event) + (garbage-collect-events)) + appender)) + +(defn clear + "Clear the events from the `appender`." + [appender] + (assoc appender :events [] :event-index {})) + +(defn consumers + "Return the consumers of the `appender`." + [appender] + (vals (:consumers appender))) + +(defn consumer-by-id + "Find the consumer of `appender` by `id`." + [appender id] + (some #(and (= id (:id %)) %) (consumers appender))) + +(defn event + "Lookup the event by `id` from the log `appender`." + [appender id] + (get (:event-index appender) id)) + +(defn events + "Return the events from the `appender`." + [appender] + (take (:size appender) (:events appender))) + +(defn make-appender + "Make a hash map appender." + [{:keys [id filters levels logger size threshold]}] + (cond-> {:consumers {} + :event-index {} + :events nil + :filters (or filters {}) + :id id + :levels levels + :size (or size default-size) + :threshold (or threshold default-threshold)} + (map? filters) + (assoc :filter-fn (event/search-filter levels filters)) + logger + (assoc :logger logger))) + +(defn remove-consumer + "Remove the `consumer` from the `appender`." + [appender consumer] + (update appender :consumers dissoc (:id consumer))) + +(defn update-appender + "Update the log `appender`." + [appender {:keys [filters size threshold]}] + (cond-> appender + (map? filters) + (assoc :filters filters :filter-fn (event/search-filter (:levels appender) filters)) + (pos-int? size) + (assoc :size size) + (nat-int? threshold) + (assoc :threshold threshold))) + +(defn update-consumer + "Update the `consumer` of the `appender`." + [appender {:keys [id filters] :as consumer}] + (update-in appender [:consumers id] + (fn [existing-consumer] + (assert (:id existing-consumer) + (format "Consumer %s not registered" id)) + (-> existing-consumer + (merge (select-keys consumer [:filters])) + (assoc :filter-fn (event/search-filter (:levels appender) filters)))))) diff --git a/src/cider/log/event.clj b/src/cider/log/event.clj new file mode 100644 index 000000000..1d3db0aed --- /dev/null +++ b/src/cider/log/event.clj @@ -0,0 +1,71 @@ +(ns cider.log.event + (:require [clojure.string :as str]) + (:import [java.util.regex Pattern])) + +(defn- exception-name + "Return the `exception` class name." + [^Class exception] + (some-> exception .getClass .getName)) + +(defn exception-frequencies + "Return the exception name frequencies of `events`." + [events] + (frequencies (remove nil? (map #(some-> % :exception exception-name) events)))) + +(defn logger-frequencies + "Return the logger name frequencies of `events`." + [events] + (frequencies (map :logger events))) + +(defn level-frequencies + "Return the log level frequencies of `events`." + [events] + (frequencies (map :level events))) + +(defn search-filter + "Return a predicate function that computes if a given event matches the search criteria." + [levels {:keys [end-time exceptions level loggers pattern start-time threads]}] + (let [exceptions (set exceptions) + level->weight (into {} (map (juxt :name :weight) levels)) + level-weight (when (or (string? level) (keyword? level)) + (some-> level name str/upper-case keyword level->weight)) + loggers (set loggers) + threads (set threads) + pattern (cond + (string? pattern) + (try (re-pattern pattern) (catch Exception _)) + (instance? Pattern pattern) + pattern)] + (if (or (seq exceptions) (seq loggers) (seq threads) level-weight pattern start-time end-time) + (fn [event] + (and (or (empty? exceptions) + (contains? exceptions (some-> event :exception exception-name))) + (or (nil? level-weight) + (>= (level->weight (:level event)) level-weight)) + (or (empty? loggers) + (contains? loggers (:logger event))) + (or (empty? threads) + (contains? threads (:thread event))) + (or (not pattern) + (some->> event :message (re-matches pattern))) + (or (not (nat-int? start-time)) + (>= (:timestamp event) start-time)) + (or (not (nat-int? end-time)) + (< (:timestamp event) end-time)))) + (constantly true)))) + +(defn search + "Search the log events by `criteria`." + [levels {:keys [filters limit offset] :as _criteria} events] + (cond->> events + (map? filters) + (filter (search-filter levels filters)) + (nat-int? offset) + (drop offset) + true + (take (if (nat-int? limit) limit 500)))) + +(defn thread-frequencies + "Return the thread frequencies of `events`." + [events] + (frequencies (map (comp name :thread) events))) diff --git a/src/cider/log/framework.clj b/src/cider/log/framework.clj new file mode 100644 index 000000000..b3b62bc3e --- /dev/null +++ b/src/cider/log/framework.clj @@ -0,0 +1,144 @@ +(ns cider.log.framework + (:require [cider.log.appender :as appender] + [cider.log.event :as event])) + +(def ^:dynamic *frameworks* + ['cider.log.framework.logback/framework + 'cider.log.framework.jul/framework + ;; 'cider.log.framework.log4j2/framework + ]) + +(defn- ex-appender-not-found + "Return the appender not found exception info." + [framework appender] + (ex-info (format "Log appender %s not found in framework %s" + (:id appender) (:id framework)) + {:error :log-appender-not-found + :framework (:id framework) + :appender (:id appender)})) + +(defn find-appender! + "Find the `appender` by its :id key in `framework`, or throw an exception." + [framework appender] + (or (get-in framework [:appenders (:id appender)]) + (throw (ex-appender-not-found framework appender)))) + +(defn appenders + "Return the appenders of the log `framework`." + [framework] + (vals (:appenders framework))) + +(defn appender [framework appender] + (some #(and (= (:id appender) (:id (deref %))) %) + (appenders framework))) + +(defn add-appender + "Add the log `appender` to the `framework`." + [framework appender] + (let [atom-appender (atom (-> {:levels (:levels framework) + :logger (:root-logger framework)} + (merge appender) + (appender/make-appender)))] + (-> (assoc-in framework [:appenders (:id @atom-appender)] atom-appender) + ((:add-appender-fn framework) atom-appender)))) + +(defn add-consumer + "Add `consumer` to the `appender` of the log `framework`." + [framework appender consumer] + (let [atom-appender (find-appender! framework appender)] + (swap! atom-appender appender/add-consumer consumer) + framework)) + +(defn clear-appender + "Clear the log `appender` of the `framework`." + [framework appender] + (let [atom-appender (find-appender! framework appender)] + (swap! atom-appender appender/clear) + framework)) + +(defn consumer + "Find the `consumer` listening to the `appender` of the log `framework`." + [framework appender consumer] + (let [atom-appender (find-appender! framework appender)] + (appender/consumer-by-id @atom-appender (:id consumer)))) + +(defn event + "Lookup the event by `id` in the `appender` of the `framework`." + [framework appender id] + (let [atom-appender (find-appender! framework appender)] + (appender/event @atom-appender id))) + +(defn events + "Return the log events captured by the `appender` of the `framework`." + [framework appender] + (let [atom-appender (find-appender! framework appender)] + (appender/events @atom-appender))) + +(defn log + "Log the `event` with the `framework`." + [framework event] + ((:log-fn framework) framework event) + nil) + +(defn remove-appender + "Remove the log `appender` from the `framework`." + [framework appender] + (let [atom-appender (find-appender! framework appender)] + (-> ((:remove-appender-fn framework) framework atom-appender) + (update :appenders dissoc (:id @atom-appender))))) + +(defn remove-consumer + "Remove the `consumer` listening to the `appender` of the log `framework`." + [framework appender consumer] + (let [atom-appender (find-appender! framework appender)] + (swap! atom-appender appender/remove-consumer consumer) + framework)) + +(defn update-appender + "Update the `appender` of the log `framework`." + [framework appender] + (let [atom-appender (find-appender! framework appender)] + (swap! atom-appender appender/update-appender appender) + framework)) + +(defn update-consumer + "Update the `consumer` listening to the `appender` of the log `framework`." + [framework appender consumer] + (let [atom-appender (find-appender! framework appender)] + (swap! atom-appender appender/update-consumer consumer) + framework)) + +(defn resolve-framework + "Resolve the framework bound to `framework-sym`." + [framework-sym] + (try (require (-> framework-sym namespace symbol)) + (some-> framework-sym resolve deref) + (catch Exception _))) + +(defn resolve-frameworks + "Resolve the framework bound to `framework-syms`." + ([] + (resolve-frameworks *frameworks*)) + ([framework-syms] + (reduce (fn [frameworks framework-sym] + (if-let [framework (resolve-framework framework-sym)] + (assoc frameworks (:id framework) framework) + frameworks)) + {} framework-syms))) + +(defn search-events + "Search the log events captured by the `appender` of the log + `framework` and filter them by the search `criteria`." + [framework appender criteria] + (->> (events framework appender) + (event/search (:levels framework) criteria))) + +(defn shutdown + "Shutdown all consumers and appenders of the log `framework`." + [framework] + (reduce (fn [framework appender] + (-> (reduce (fn [framework consumer] + (remove-consumer framework @appender consumer)) + framework (appender/consumers @appender)) + (remove-appender @appender))) + framework (appenders framework))) diff --git a/src/cider/log/framework/jul.clj b/src/cider/log/framework/jul.clj new file mode 100644 index 000000000..0652a3542 --- /dev/null +++ b/src/cider/log/framework/jul.clj @@ -0,0 +1,99 @@ +(ns cider.log.framework.jul + (:require [cider.log.appender :as appender] + [clojure.set :as set]) + (:import (java.util.logging Level Logger LogRecord StreamHandler))) + +(def ^:private log-levels + "The Java Util Logging level descriptors." + (->> [{:name :FINEST + :category :trace + :object Level/FINEST} + {:name :FINER + :category :trace + :object Level/FINER} + {:name :FINE + :category :debug + :object Level/FINE} + {:name :CONFIG + :category :info + :object Level/CONFIG} + {:name :INFO + :category :info + :object Level/INFO} + {:name :WARNING + :category :warning + :object Level/WARNING} + {:name :SEVERE + :category :error + :object Level/SEVERE}] + (map-indexed #(assoc %2 :weight %1)))) + +(def ^:private level-to-keyword + (into {} (map (juxt :object :name) log-levels))) + +(def ^:private keyword-to-level + (set/map-invert level-to-keyword)) + +(defn- event->record + "Convert a Cider log event into a Java LogRecord." + ^LogRecord [{:keys [arguments exception level logger message]}] + (doto (LogRecord. (keyword-to-level level Level/INFO) message) + (.setLoggerName (or logger "")) + (.setParameters (into-array Object arguments)) + (.setThrown exception))) + +(defn- thread-by-id + "Find the thread by `id`." + ^Thread [id] + (some #(and (= id (.getId ^Thread %)) %) + (keys (Thread/getAllStackTraces)))) + +(defn- record->event + "Convert a Java LogRecord into a Cider log event." + [^LogRecord record] + (let [exception (.getThrown record)] + (cond-> {:arguments (vec (.getParameters record)) + :id (java.util.UUID/randomUUID) + :level (level-to-keyword (.getLevel record)) + :logger (.getLoggerName record) + :mdc {} + :message (.getMessage record) + :thread (some-> (.getThreadID record) thread-by-id .getName) + :timestamp (.getMillis record)} + exception (assoc :exception exception)))) + +(defn- add-appender + "Attach the Logback appender." + [framework appender] + (let [instance (proxy [StreamHandler] [] + (publish [^LogRecord record] + (swap! appender appender/add-event (record->event record)))) + ^String logger-name (or (:logger appender) (:root-logger framework))] + (swap! appender assoc :instance instance) + (doto ^Logger (Logger/getLogger logger-name) + (.addHandler instance)) + framework)) + +(defn- remove-appender + "Remove `appender` from the Logback `framework`." + [framework appender] + (let [^String logger-name (or (:logger appender) (:root-logger framework)) + logger (Logger/getLogger logger-name)] + (.removeHandler logger (:instance @appender)) + framework)) + +(defn- log [framework event] + (let [^String logger-name (or (:logger event) (:root-logger framework))] + (.log (Logger/getLogger logger-name) (event->record event)))) + +(def framework + "The Java Util Logging framework." + {:add-appender-fn #'add-appender + :id "jul" + :javadoc-url "https://docs.oracle.com/en/java/javase/19/docs/api/java.logging/java/util/logging/package-summary.html" + :levels log-levels + :log-fn #'log + :name "Java Util Logging" + :remove-appender-fn #'remove-appender + :root-logger "" + :website-url "https://docs.oracle.com/en/java/javase/19/core/java-logging-overview.html"}) diff --git a/src/cider/log/framework/log4j2.clj b/src/cider/log/framework/log4j2.clj new file mode 100644 index 000000000..55711274b --- /dev/null +++ b/src/cider/log/framework/log4j2.clj @@ -0,0 +1,119 @@ +(ns cider.log.framework.log4j2 + (:require [cider.log.appender :as appender] + [clojure.set :as set]) + (:import (org.apache.logging.log4j Level MarkerManager ThreadContext) + (org.apache.logging.log4j.core LogEvent LoggerContext) + (org.apache.logging.log4j.core.appender AbstractAppender) + (org.apache.logging.log4j.core.config AppenderRef LoggerConfig$Builder Property) + (org.apache.logging.log4j.core.impl ThrowableProxy) + (org.apache.logging.log4j.message MessageFormatMessage))) + +(def ^:private log-levels + "The Log4j2 level descriptors." + (->> [{:name :TRACE + :category :trace + :object Level/TRACE} + {:name :DEBUG + :category :debug + :object Level/DEBUG} + {:name :INFO + :category :info + :object Level/INFO} + {:name :WARN + :category :warning + :object Level/WARN} + {:name :ERROR + :category :error + :object Level/ERROR} + {:name :FATAL + :category :error + :object Level/FATAL}] + (map-indexed #(assoc %2 :weight %1)))) + +(def ^:private level-to-keyword + (into {} (map (juxt :object :name) log-levels))) + +(def ^:private keyword-to-level + (set/map-invert level-to-keyword)) + +(defn- event-exception [^LogEvent event] + (let [proxy (.getThrownProxy event)] + (when (instance? ThrowableProxy proxy) + (.getThrowable ^ThrowableProxy proxy)))) + +(defn- event-data [^LogEvent event] + (let [exception (event-exception event)] + (cond-> {:arguments (-> event .getMessage .getParameters vec) + :id (java.util.UUID/randomUUID) + :level (level-to-keyword (.getLevel event)) + :logger (.getLoggerName event) + :mdc (some->> (.getContextData event) .toMap (into {})) + :message (some-> event .getMessage .getFormattedMessage) + :thread (.getThreadName event) + :timestamp (.getTimeMillis event)} + exception (assoc :exception exception)))) + +(defn- add-appender [framework appender] + (let [context (LoggerContext/getContext false) + config (.getConfiguration context)] + (locking config + (let [^String logger-name (or (:logger @appender) (:root-logger framework)) + logger-config (.getLoggerConfig config logger-name) + logger-config' (-> (LoggerConfig$Builder.) + (.withAdditivity (.isAdditive logger-config)) + (.withConfig config) + (.withIncludeLocation (str (.isIncludeLocation logger-config))) + (.withLevel ^Level (keyword-to-level (:level @appender) Level/INFO)) + (.withLoggerName logger-name) + (.withProperties (.getPropertyArray logger-config)) + (.withRefs (into-array + (conj (seq (.getAppenderRefs logger-config)) + (AppenderRef/createAppenderRef (:id @appender) nil nil)))) + (.withtFilter (.getFilter logger-config)) + (.build)) + instance (doto (proxy [AbstractAppender] [(:id @appender) nil nil true (into-array Property [])] + (append [^LogEvent event] + (swap! appender appender/add-event (event-data event)))) + (.start))] + (swap! appender assoc :instance instance) + (.addAppender logger-config' instance nil nil) + (.addLogger config logger-name logger-config') + (.updateLoggers context) + framework)))) + +(defn- remove-appender + "Remove `appender` from the Log4j `framework`." + [framework appender] + (let [context (LoggerContext/getContext false) + config (.getConfiguration context) + ^String logger-name (or (:logger appender) (:root-logger framework)) + logger-config (.getLoggerConfig config logger-name)] + (.removeAppender logger-config (:id @appender)) + (.setConfiguration context config) + (.updateLoggers context) + framework)) + +(defn- log [framework {:keys [arguments exception level logger marker mdc message]}] + (let [context (LoggerContext/getContext false) + ^String logger-name (or logger (:root-logger framework))] + (doseq [[key value] (seq mdc)] + (ThreadContext/put key value)) + (.log (.getLogger context logger-name) + ^Level (keyword-to-level level Level/INFO) + (some-> marker MarkerManager/getMarker) + (MessageFormatMessage. ^String message (into-array Object arguments)) + ^Throwable exception) + (when (seq mdc) + (ThreadContext/clearAll)))) + +(def framework + "The Log4j2 logging framework." + {:add-appender-fn #'add-appender + :id "log4j2" + :javadoc-url "https://logging.apache.org/log4j/2.x/javadoc/log4j-api" + :levels log-levels + :log-fn #'log + :name "Log4j2" + :remove-appender-fn #'remove-appender + :root-logger (-> (LoggerContext/getContext false) .getRootLogger .getName) + :website-url "https://logging.apache.org"}) diff --git a/src/cider/log/framework/logback.clj b/src/cider/log/framework/logback.clj new file mode 100644 index 000000000..dd23af092 --- /dev/null +++ b/src/cider/log/framework/logback.clj @@ -0,0 +1,110 @@ +(ns cider.log.framework.logback + (:require [cider.log.appender :as appender] + [clojure.set :as set]) + (:import (ch.qos.logback.classic Level Logger LoggerContext) + (ch.qos.logback.classic.spi ILoggingEvent LoggingEvent ThrowableProxy) + (ch.qos.logback.core Appender AppenderBase) + (org.slf4j LoggerFactory MarkerFactory MDC))) + +(def ^:private log-levels + "The Logback level descriptors." + (->> [{:name :TRACE + :category :trace + :object Level/TRACE} + {:name :DEBUG + :category :debug + :object Level/DEBUG} + {:name :INFO + :category :info + :object Level/INFO} + {:name :WARN + :category :warning + :object Level/WARN} + {:name :ERROR + :category :error + :object Level/ERROR}] + (map-indexed #(assoc %2 :weight %1)))) + +(def ^:private level-to-keyword + (into {} (map (juxt :object :name) log-levels))) + +(def ^:private keyword-to-level + (set/map-invert level-to-keyword)) + +(defn- logger-context + "Return the Logback logger context." + ^LoggerContext [] + (LoggerFactory/getILoggerFactory)) + +(defn- get-logger + "Return the logger by `name` from the logger `context`." + ^Logger [^String name] + (.getLogger (logger-context) name)) + +(defn- event-exception [^LoggingEvent event] + (let [proxy (.getThrowableProxy event)] + (when (instance? ThrowableProxy proxy) + (.getThrowable ^ThrowableProxy proxy)))) + +(defn- event-data [^LoggingEvent event] + (let [exception (event-exception event)] + (cond-> {:arguments (vec (.getArgumentArray event)) + :id (java.util.UUID/randomUUID) + :level (level-to-keyword (.getLevel event)) + :logger (.getLoggerName event) + :mdc (into {} (.getMDCPropertyMap event)) + :message (.getFormattedMessage event) + :thread (.getThreadName event) + :timestamp (.getTimeStamp event)} + exception (assoc :exception exception)))) + +(defn- add-appender + "Attach the Logback appender." + [framework appender] + (let [instance (doto (proxy [AppenderBase] [] + (append [^ILoggingEvent event] + (swap! appender appender/add-event (event-data event)))) + (.setContext (logger-context)) + (.setName (:id @appender)) + (.start))] + (swap! appender assoc :instance instance) + (doto ^Logger (get-logger (or (:logger appender) (:root-logger framework))) + (.addAppender instance)) + framework)) + +(defn- level-int [level] + (some-> level keyword-to-level Level/toLocationAwareLoggerInteger)) + +(defn- log [framework {:keys [arguments exception level logger marker mdc message]}] + (let [logger (get-logger (or logger (:root-logger framework)))] + (doseq [[key value] (seq mdc)] + (MDC/put key value)) + (.log logger + (some-> marker MarkerFactory/getMarker) + ^String (.getName logger) ;;TODO: What is "fqcn"? + (level-int (or level :INFO)) + message + (into-array Object arguments) + exception) + (when (seq mdc) + (MDC/clear)))) + +(defn- remove-appender + "Remove `appender` from the Logback `framework`." + [framework appender] + (.stop ^Appender (:instance @appender)) + (doto ^Logger (get-logger (or (:logger appender) (:root-logger framework))) + (.detachAppender ^String (:id @appender))) + framework) + +(def framework + "The Logback logging framework." + {:add-appender-fn #'add-appender + :id "logback" + :javadoc-url "https://logback.qos.ch/apidocs" + :levels log-levels + :log-fn #'log + :name "Logback" + :remove-appender-fn #'remove-appender + :root-logger Logger/ROOT_LOGGER_NAME + :website-url "https://logback.qos.ch"}) diff --git a/src/cider/log/repl.clj b/src/cider/log/repl.clj new file mode 100644 index 000000000..6bcec864f --- /dev/null +++ b/src/cider/log/repl.clj @@ -0,0 +1,316 @@ +(ns ^{:clojure.tools.namespace.repl/unload false + :clojure.tools.namespace.repl/load false} + cider.log.repl + "This namespace provide functionality to capture and search log events + emitted by Java logging frameworks. It is designed to be used by + non-NREPL users in a plain REPL. + + Example usage: + + ;; Use the :logback logging framework + (repl/set-framework! :logback) + + ;; Add an appender to the log framework that captures events. + (repl/add-appender) + + ;; Log something + (repl/log {:message \"hello\"}) + + ;; Return all captured log events + (repl/events) + + ;; Search log events by message + (repl/events {:pattern \"hell.*\"}) + + ;; Search log events by level + (repl/events {:level :INFO}) + + ;; Add a log consumer that prints log events + (repl/add-consumer + {:callback (fn [_consumer event] + (clojure.pprint/pprint event))}) + + ;; Log something else + (repl/log {:message \"world\"}) + + ;; Remove all consumers and appenders + (repl/shutdown)" + (:require [cider.log.framework :as framework] + [clojure.pprint :as pprint])) + +(defonce ^:dynamic *settings* + {:framework "jul" + :appender "cider-log" + :consumer "cider-log"}) + +(defonce ^:dynamic *frameworks* + (framework/resolve-frameworks)) + +(defn- merge-default [options] + (let [options (merge *settings* options)] + (cond-> options + (contains? options :appender) + (update :appender name) + (contains? options :consumer) + (update :consumer name) + (contains? options :framework) + (update :framework name)))) + +(defn- appender-options [options] + (let [{:keys [appender filters logger size threshold]} (merge-default options)] + (cond-> {:id appender} + filters (assoc :filters filters) + logger (assoc :logger logger) + size (assoc :size size) + threshold (assoc :threshold threshold)))) + +(defn- consumer-options [options] + (let [{:keys [callback consumer filters]} (merge-default options)] + (cond-> {:callback (or callback (fn [_ event] (pprint/pprint event)))} + consumer (assoc :id consumer) + filters (assoc :filters filters)))) + +(defn- criteria + [{:keys [end-time exceptions level loggers pattern start-time threads]}] + {:filters (cond-> {} + (seq exceptions) (assoc :exceptions exceptions) + (seq loggers) (assoc :loggers loggers) + end-time (assoc :end-time end-time) + level (assoc :level level) + pattern (assoc :pattern pattern) + start-time (assoc :start-time start-time) + threads (assoc :threads threads))}) + +(defn update-settings! + "Update the `*settings*` by applying `f` and `args` to it." + [f & args] + (alter-var-root #'*settings* #(apply f % args)) + *settings*) + +;; Log Frameworks + +(defn- ensure-framework + "Ensure that the :framework in `options` is valid. Returns the + framework or throws an exception." + [options] + (let [{:keys [framework]} (merge-default options)] + (or (get *frameworks* (some-> framework name)) + (throw (ex-info (str "Unsupported log framework: " framework) + {:framework framework + :supported-frameworks (keys *frameworks*)}))))) + +(defn framework + "Lookup the current log framework or the one in `options`. + + Options: + :framework - The identifier of the framework." + [& [options]] + (let [{:keys [framework]} (merge-default options)] + (get *frameworks* (some-> framework name)))) + +(defn- swap-framework! [options f & args] + (let [framework (ensure-framework options)] + (alter-var-root #'*frameworks* + (fn [frameworks] + (update frameworks (:id framework) #(apply f % args)))) + (get *frameworks* (:id framework)))) + +(defn set-framework! + "Change the current framework to `identifier`." + [identifier] + (update-settings! assoc :framework (name identifier))) + +(defn shutdown + "Remove all consumers and appenders of a framework. + + Options: + :framework - The identifier of the framework." + [& [options]] + (let [options (merge-default options)] + (swap-framework! options framework/shutdown))) + +;; Log Appenders + +(defn- ensure-appender + "Ensure that the :appender in `options` is registered. Returns the + registered appender or throws an exception." + [options] + (let [{:keys [appender]} (merge-default options) + framework (ensure-framework options)] + (or (framework/appender framework {:id appender}) + (throw (ex-info (str "Log appender not registered: " appender) + {:appender appender + :registered-appenders (->> (framework/appenders framework) + (map (comp :id deref)))}))))) + +(defn appenders + "Return all appenders of a log framework. + + Options: + :framework - The identifier of the framework." + [& [options]] + (framework/appenders (ensure-framework options))) + +(defn appender + "Return the appender of a log framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender." + [& [options]] + (let [{:keys [appender]} (merge-default options)] + (framework/appender (ensure-framework options) {:id appender}))) + +(defn add-appender + "Add a log appender to a framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender. + :filters - A map of criteria used to filter log events. + :logger - The name of the logger to which the append will be attached. + :size - The number of events to capture in the appender. + :threshold - A threshold in percentage used to garbage collect log events." + [& [options]] + (let [options (merge-default options)] + (swap-framework! options framework/add-appender (appender-options options)) + (appender options))) + +(defn clear-appender + "Clear the events captured by and appender of a framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender." + [& [options]] + (swap-framework! options framework/clear-appender @(ensure-appender options))) + +(defn remove-appender + "Remove an appender from a framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender." + [& [options]] + (swap-framework! options framework/remove-appender @(ensure-appender options))) + +(defn set-appender! + "Change the current appender to `identifier`." + [identifier] + (update-settings! assoc :appender (name identifier))) + +(defn update-appender + "Update the appender of a framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender. + :filters - A map of criteria used to filter log events. + :logger - The name of the logger to which the append will be attached. + :size - The number of events to capture in the appender. + :threshold - A threshold in percentage used to garbage collect log events." + [& [options]] + (ensure-appender options) + (swap-framework! options framework/update-appender (appender-options options))) + +;; Log Consumers + +(defn add-consumer + "Add a log consumer to a framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender. + :consumer - The identifier of the consumer. + :callback - A function that will be called for each log event. + :filters - A map of criteria used to filter log events." + [& [options]] + (swap-framework! options framework/add-consumer + @(ensure-appender options) + (consumer-options options))) + +(defn remove-consumer + "Remove a consumer from an appender. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender. + :consumer - The identifier of the consumer." + [& [options]] + (let [{:keys [consumer]} (merge-default options)] + (swap-framework! options framework/remove-consumer + @(ensure-appender options) + {:id consumer}))) + +(defn update-consumer + "Update the consumer of an appender. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender. + :consumer - The identifier of the consumer. + :filters - A map of criteria used to filter log events." + [& [options]] + (let [options (merge-default options)] + (swap-framework! options framework/update-consumer + @(ensure-appender options) + (consumer-options options)))) + +(defn set-consumer! + "Change the current consumer to `identifier`." + [identifier] + (update-settings! assoc :consumer (name identifier))) + +;; Log Events + +(defn event + "Find a log event captured by the an appender of a framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender. + :event - The identifier of the event." + [& [options]] + (when-let [event-id (:event options)] + (framework/event (ensure-framework options) + @(ensure-appender options) + event-id))) + +(defn events + "Search log events captured by an appender of a framework. + + Options: + :framework - The identifier of the framework. + :appender - The identifier of the appender. + :end-time - Only include events before this timestamp. + :exceptions - Only include events matching the exception classes. + :level - Only include events with the given level. + :loggers - Only include events emitted by the loggers. + :pattern - Only include events whose message matches the regex pattern. + :start-time - Only include events after this timestamp. + :threads - Only include events emitted by the given threads." + [& [options]] + (framework/search-events (ensure-framework options) + @(ensure-appender options) + (criteria options))) + +(defn log + "Emit a log event. + + Options: + :framework - The identifier of the framework. + :message - The message of the log event. + :mdc - The mapped diagnostic context of the log event. + :arguments - The arguments of the log event. + :level - The level of the log event. + :logger - The logger used to emit the log event." + [options] + (let [options (merge-default options)] + (framework/log (ensure-framework options) + (let [{:keys [arguments level logger message mdc]} options] + (cond-> {} + arguments (assoc :arguments arguments) + level (assoc :level level) + logger (assoc :logger logger) + mdc (assoc :mdc mdc) + message (assoc :message message)))))) diff --git a/src/cider/log/specs.clj b/src/cider/log/specs.clj new file mode 100644 index 000000000..9d13a91e3 --- /dev/null +++ b/src/cider/log/specs.clj @@ -0,0 +1,286 @@ +(ns cider.log.specs + (:require [cider.log.appender :as appender] + [cider.log.framework :as framework] + [cider.log.repl :as repl] + [clojure.spec.alpha :as s])) + +(s/def :cider.log.level/category simple-keyword?) +(s/def :cider.log.level/name simple-keyword?) +(s/def :cider.log.level/object any?) +(s/def :cider.log.level/weight nat-int?) + +(s/def :cider.log/level + (s/keys :req-un [:cider.log.level/category + :cider.log.level/name + :cider.log.level/object + :cider.log.level/weight])) + +(s/def :cider.log.filter/end-time pos-int?) + +(s/def :cider.log.filter/exceptions + (s/coll-of string? :kind set?)) + +(s/def :cider.log.filter/level simple-keyword?) + +(s/def :cider.log.filter/loggers + (s/coll-of string? :kind set?)) + +(s/def :cider.log.filter/pattern string?) +(s/def :cider.log.filter/start-time pos-int?) + +(s/def :cider.log.filter/threads + (s/coll-of string? :kind set?)) + +(s/def :cider.log/filters + (s/keys :opt-un [:cider.log.filter/end-time + :cider.log.filter/exceptions + :cider.log.filter/level + :cider.log.filter/loggers + :cider.log.filter/pattern + :cider.log.filter/start-time + :cider.log.filter/threads])) + +(s/def :cider.log.pagination/limit nat-int?) +(s/def :cider.log.pagination/offset nat-int?) + +(s/def :cider.log.event/search + (s/keys :opt-un [:cider.log.pagination/limit + :cider.log.pagination/offset + :cider.log/filters])) + +(s/def :cider.log.framework/add-appender-fn ifn?) +(s/def :cider.log.framework/id string?) +(s/def :cider.log.framework/javadoc-url string?) +(s/def :cider.log.framework/levels (s/coll-of :cider.log/level)) +(s/def :cider.log.framework/log-fn ifn?) +(s/def :cider.log.framework/name string?) +(s/def :cider.log.framework/remove-appender-fn ifn?) +(s/def :cider.log.framework/root-logger string?) +(s/def :cider.log.framework/website-url string?) + +(s/def :cider.log/framework + (s/keys :req-un [:cider.log.framework/add-appender-fn + :cider.log.framework/id + :cider.log.framework/javadoc-url + :cider.log.framework/levels + :cider.log.framework/log-fn + :cider.log.framework/name + :cider.log.framework/remove-appender-fn + :cider.log.framework/root-logger + :cider.log.framework/website-url])) + +(s/def :cider.log.appender/id string?) +(s/def :cider.log.appender/levels (s/coll-of :cider.log/level)) +(s/def :cider.log.appender/logger string?) +(s/def :cider.log.appender/size pos-int?) +(s/def :cider.log.appender/threshold (s/and nat-int? #(< % 100))) + +(s/def :cider.log/appender + (s/keys :req-un [:cider.log.appender/id] + :opt-un [:cider.log.appender/levels + :cider.log.appender/logger + :cider.log.appender/size + :cider.log.appender/threshold])) + +(s/def :cider.log.consumer/callback ifn?) +(s/def :cider.log.consumer/filter (s/map-of string? any?)) +(s/def :cider.log.consumer/id string?) + +(s/def :cider.log/consumer + (s/keys :req-un [:cider.log.consumer/id] + :opt-un [:cider.log.consumer/callback + :cider.log.consumer/filter])) + +(s/def :cider.log.event/argument any?) +(s/def :cider.log.event/arguments (s/coll-of :cider.log.event/argument :kind vector?)) +(s/def :cider.log.event/id uuid?) +(s/def :cider.log.event/level simple-keyword?) +(s/def :cider.log.event/logger string?) +(s/def :cider.log.event/mdc (s/map-of string? string?)) +(s/def :cider.log.event/message string?) +(s/def :cider.log.event/thread string?) +(s/def :cider.log.event/timestamp pos-int?) + +(s/def :cider.log/event + (s/keys :req-un [:cider.log.event/arguments + :cider.log.event/id + :cider.log.event/level + :cider.log.event/logger + :cider.log.event/mdc + :cider.log.event/message + :cider.log.event/thread + :cider.log.event/timestamp])) + +;; cider.log.framework + +(s/fdef framework/add-appender + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :ret :cider.log/framework) + +(s/fdef framework/add-consumer + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) + :ret :cider.log/framework) + +(s/fdef framework/clear-appender + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :ret :cider.log/framework) + +(s/fdef framework/consumer + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) + :ret (s/nilable :cider.log/consumer)) + +(s/fdef framework/event + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :id :cider.log.event/id) + :ret (s/nilable :cider.log/event)) + +(s/fdef framework/events + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :ret (s/coll-of :cider.log/event)) + +(s/fdef framework/log + :args (s/cat :framework :cider.log/framework :event map?) + :ret nil?) + +(s/fdef framework/remove-appender + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :ret :cider.log/framework) + +(s/fdef framework/remove-consumer + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) + :ret :cider.log/framework) + +(s/fdef framework/update-appender + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :ret :cider.log/framework) + +(s/fdef framework/resolve-framework + :args (s/cat :framework-sym qualified-symbol?) + :ret (s/nilable :cider.log/framework)) + +(s/fdef framework/resolve-frameworks + :args (s/or :arity-0 (s/cat) + :arity-1 (s/cat :framework-syms (s/coll-of qualified-symbol?))) + :ret (s/map-of :cider.log.framework/id :cider.log/framework)) + +(s/fdef framework/search-events + :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :criteria map?) + :ret (s/coll-of :cider.log/event)) + +;; cider.log.appender + +(s/fdef appender/add-consumer + :args (s/cat :appender :cider.log/appender :consumer :cider.log/consumer) + :ret :cider.log/appender) + +(s/fdef appender/add-event + :args (s/cat :appender :cider.log/appender :event :cider.log/event) + :ret :cider.log/appender) + +(s/fdef appender/clear + :args (s/cat :appender :cider.log/appender) + :ret :cider.log/appender) + +(s/fdef appender/consumers + :args (s/cat :appender :cider.log/appender) + :ret (s/coll-of :cider.log/consumer)) + +(s/fdef appender/consumer-by-id + :args (s/cat :appender :cider.log/appender :id :cider.log.consumer/id) + :ret (s/nilable :cider.log/consumer)) + +(s/fdef appender/event + :args (s/cat :appender :cider.log/appender :id :cider.log.event/id) + :ret (s/nilable :cider.log/event)) + +(s/fdef appender/events + :args (s/cat :appender :cider.log/appender) + :ret (s/coll-of :cider.log/event)) + +(s/fdef appender/make-appender + :args (s/cat :appender :cider.log/appender) + :ret :cider.log/appender) + +(s/fdef appender/remove-consumer + :args (s/cat :appender :cider.log/appender :consumer :cider.log/consumer) + :ret :cider.log/appender) + +(s/fdef appender/update-appender + :args (s/cat :appender :cider.log/appender :settings map?) + :ret :cider.log/appender) + +(s/fdef appender/update-consumer + :args (s/cat :appender :cider.log/appender :consumer :cider.log/consumer) + :ret :cider.log/appender) + +;; cider.log.repl + +(s/def :cider.log.repl.option/framework + (s/nilable (s/or :string string? :keyword keyword?))) + +(s/def :cider.log.repl.option/appender + (s/nilable (s/or :string string? :keyword keyword?))) + +(s/def :cider.log.repl/options + (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender])))) + +(s/fdef repl/add-appender + :args (s/cat :options :cider.log.repl/options) + :ret :cider.log/framework) + +;; (s/fdef repl/add-consumer +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) +;; :ret :cider.log/framework) + +;; (s/fdef repl/clear-appender +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) +;; :ret :cider.log/framework) + +;; (s/fdef repl/consumer +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) +;; :ret (s/nilable :cider.log/consumer)) + +;; (s/fdef repl/event +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :id :cider.log.event/id) +;; :ret (s/nilable :cider.log/event)) + +;; (s/fdef repl/events +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) +;; :ret (s/coll-of :cider.log/event)) + +;; (s/fdef repl/log +;; :args (s/cat :framework :cider.log/framework :event map?) +;; :ret nil?) + +;; (s/fdef repl/remove-appender +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) +;; :ret :cider.log/framework) + +;; (s/fdef repl/remove-consumer +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) +;; :ret :cider.log/framework) + +(s/fdef repl/set-appender! + :args (s/cat :framework (s/or :string string? :keyword keyword?)) + :ret nil?) + +(s/fdef repl/set-framework! + :args (s/cat :framework (s/or :string string? :keyword keyword?)) + :ret nil?) + +;; (s/fdef repl/update-appender +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) +;; :ret :cider.log/framework) + +;; (s/fdef repl/resolve-framework +;; :args (s/cat :framework-sym qualified-symbol?) +;; :ret (s/nilable :cider.log/framework)) + +;; (s/fdef repl/resolve-frameworks +;; :args (s/or :arity-0 (s/cat) +;; :arity-1 (s/cat :framework-syms (s/coll-of qualified-symbol?))) +;; :ret (s/map-of :cider.log.framework/id :cider.log/framework)) + +;; (s/fdef repl/search-events +;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :criteria map?) +;; :ret (s/coll-of :cider.log/event)) diff --git a/src/cider/nrepl.clj b/src/cider/nrepl.clj index 5d3305695..9d6434887 100644 --- a/src/cider/nrepl.clj +++ b/src/cider/nrepl.clj @@ -284,6 +284,136 @@ Depending on the type of the return value of the evaluation this middleware may "var-name" "The var name"} :returns {"status" "\"done\""}}}})) +(def-wrapper wrap-log cider.nrepl.middleware.log/handle-log + {:doc "Middleware that captures log events and makes them inspect-able." + :requires #{#'session #'wrap-print} + :handles + {"log-add-appender" + {:doc "Add an appender to a log framework." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender." + "filters" "A map from filter name to filter condition." + "size" "The number of events the appender keeps in memory." + "threshold" "The threshold in percent used to cleanup events."} + :optional {"logger" "The name of the logger to attach to."} + :returns {"status" "done" + "log-add-appender" "The appender that was added."}} + + "log-add-consumer" + {:doc "Add a consumer to an appender of a log framework." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender." + "filters" "A map from filter name to filter condition."} + :returns {"status" "done" + "log-add-consumer" "The consumer that was added."}} + + "log-analyze-stacktrace" + {:doc "Analyze the stacktrace of a log event." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender." + "event" "The id of the event to inspect."} + :returns {"status" "done"}} + + "log-clear-appender" + {:doc "Clear all events of a log appender." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender."} + :returns {"status" "done" + "log-clear-appender" "The appender that was cleared."}} + + "log-exceptions" + {:doc "Return the exceptions and their frequencies for the given framework and appender." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender."} + :returns {"status" "done" + "log-exceptions" "A map from exception name to event frequency."}} + + "log-frameworks" + {:doc "Return the available log frameworks." + :returns {"status" "done" + "log-frameworks" "A list of log frameworks."}} + + "log-format-event" + {:doc "Format a log event." + :requires {"framework" "The id of the log framework." + "appender" "The name of the log appender." + "event" "The id of the log event."} + :optional wrap-print-optional-arguments + :returns {"status" "done" + "log-format-event" "The formatted log event."}} + + "log-inspect-event" + {:doc "Inspect a log event." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender." + "event" "The id of the event to inspect."} + :returns {"status" "done" + "value" "The inspection result."}} + + "log-levels" + {:doc "Return the log levels and their frequencies for the given framework and appender." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender."} + :returns {"status" "done" + "log-levels" "A map from log level to event frequency."}} + + "log-loggers" + {:doc "Return the loggers and their frequencies for the given framework and appender." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender."} + :returns {"status" "done" + "log-loggers" "A map from logger name to event frequency."}} + + "log-remove-appender" + {:doc "Remove an appender from a log framework." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender."} + :returns {"status" "done" + "log-remove-appender" "The removed appender."}} + + "log-remove-consumer" + {:doc "Remove a consumer from the appender of a log framework." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender." + "consumer" "The name of the consumer."} + :returns {"status" "done" + "log-add-consumer" "The removed consumer."}} + + "log-update-appender" + {:doc "Update the appender of a log framework." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender." + "filters" "A map from filter name to filter condition." + "size" "The number of events the appender keeps in memory." + "threshold" "The threshold in percent used to cleanup events."} + :returns {"status" "done" + "log-update-appender" "The updated appender."}} + + "log-update-consumer" + {:doc "Update the consumer of a log appender." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender." + "consumer" "The name of the consumer." + "filters" "A map from filter name to filter condition."} + :returns {"status" "done" + "log-update-consumer" "The consumer that was updated."}} + + "log-search" + {:doc "Search the log events of an appender." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender."} + :optional {"filters" "A map from filter name to filter condition." + "limit" "Number of log events to return."} + :returns {"status" "done" + "log-search" "The list of log events matching the search."}} + + "log-threads" + {:doc "Return the threads and their frequencies for the given framework and appender." + :requires {"framework" "The id of the log framework." + "appender" "The name of the appender."} + :returns {"status" "done" + "log-threads" "A map from thread name to event frequency."}}}}) + (def-wrapper wrap-macroexpand cider.nrepl.middleware.macroexpand/handle-macroexpand (cljs/requires-piggieback {:doc "Macroexpansion middleware." diff --git a/src/cider/nrepl/middleware.clj b/src/cider/nrepl/middleware.clj index bdd53eb0d..293eb097b 100644 --- a/src/cider/nrepl/middleware.clj +++ b/src/cider/nrepl/middleware.clj @@ -16,6 +16,7 @@ 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/src/cider/nrepl/middleware/log.clj b/src/cider/nrepl/middleware/log.clj new file mode 100644 index 000000000..c1df399ef --- /dev/null +++ b/src/cider/nrepl/middleware/log.clj @@ -0,0 +1,293 @@ +(ns cider.nrepl.middleware.log + (:require [cider.log.event :as event] + [cider.log.framework :as framework] + [cider.nrepl.middleware.inspect :as middleware.inspect] + [cider.nrepl.middleware.util.error-handling :refer [with-safe-transport]] + [haystack.analyzer :as analyzer] + [haystack.parser.clojure.throwable :as throwable] + [nrepl.middleware.print :as print] + [nrepl.misc :refer [response-for]] + [nrepl.transport :as transport] + [orchard.inspect :as orchard.inspect]) + (:import (java.io StringWriter) + (java.util UUID))) + +(defn- select-consumer + "Return the log `consumer` in a Bencode compatible format." + [consumer] + (-> (select-keys consumer [:id :filters]) + (update :id str))) + +(defn- select-appender + "Return the log `appender` in a Bencode compatible format." + [appender] + (-> (select-keys appender [:filters :logger :id :size :threshold]) + (assoc :consumers (map select-consumer (vals (:consumers appender)))))) + +(defn- select-framework + "Return the log `frameowrk` in a Bencode compatible format." + [framework] + (-> (select-keys framework [:id :javadoc-url :name :root-logger :website-url]) + (assoc :appenders (map (comp select-appender deref) + (framework/appenders framework))) + (assoc :levels (map #(select-keys % [:name :category :weight]) (:levels framework))))) + +(defn- select-exception + "Return the `exception` in a Bencode compatible format." + [exception] + (let [exception-map (throwable/Throwable->map exception) + strip-cause #(dissoc % :data :trace)] + (cond-> (strip-cause exception-map) + (seq (:via exception-map)) + (update :via #(map strip-cause %))))) + +(defn- select-event + "Return the log `event` in a Bencode compatible format." + [{:keys [exception id] :as event}] + (cond-> (select-keys event [:exception :level :logger :message :id :thread :timestamp]) + (uuid? id) + (update :id str) + (instance? Throwable exception) + (update :exception select-exception))) + +;; TODO: Double check this! Sometimes inspecting a log event works only after +;; inspecting something else with the Cider inspector. +(defn- inspect-value + "Show `value` in the Cider inspector" + [{:keys [page-size max-atom-length max-coll-size] :as msg} value] + (let [inspector (middleware.inspect/swap-inspector! + msg #(-> (assoc % :page-size (or page-size 32) + :indentation 0 + :max-atom-length max-atom-length + :max-coll-size max-coll-size) + (orchard.inspect/start value)))] + (#'middleware.inspect/inspector-response msg inspector))) + +(defn- framework + "Lookup the framework from the :framework key of the NREPL message." + [{:keys [session framework]}] + (or (get-in (meta session) [::frameworks framework]) + (throw (ex-info "Log framework not found" + {:error :log-framework-not-found + :framework framework})))) + +(defn- filters + "Extract the filters from an NREPL dictinary." + [{:keys [end-time exceptions level loggers pattern start-time threads]}] + (cond-> {} + (nat-int? end-time) + (assoc :end-time end-time) + (and (seq exceptions) (every? string? exceptions)) + (assoc :exceptions exceptions) + (or (string? level) (keyword? level)) + (assoc :level (keyword level)) + (and (seq loggers) (every? string? loggers)) + (assoc :loggers loggers) + (string? pattern) + (assoc :pattern pattern) + (nat-int? start-time) + (assoc :start-time start-time) + (and (seq threads) (every? string? threads)) + (assoc :threads threads))) + +(defn- appender + "Make an appender map from the :appender, :filters, :size + and :threshold keys of the NREPL message." + [{:keys [appender logger size threshold] :as msg}] + (when (string? appender) + (cond-> {:id appender} + (map? (:filters msg)) + (assoc :filters (filters (:filters msg))) + (string? logger) + (assoc :logger logger) + (pos-int? size) + (assoc :size size) + (nat-int? threshold) + (assoc :threshold threshold)))) + +(defn- consumer + "Make a consumer map from the :consumer and :filters keys of the NREPL message." + [{:keys [consumer] :as msg}] + (when (string? consumer) + (cond-> {:id consumer} + (map? (:filters msg)) + (assoc :filters (filters (:filters msg)))))) + +(defn- event + "Lookup the log event from the :framework, :appender and :event + keys of the NREPL `msg`." + [{:keys [event] :as msg}] + (or (framework/event (framework msg) (appender msg) (UUID/fromString event)) + (throw (ex-info "Log event not found" + {:error :log-event-not-found + :framework (:framework msg) + :appender (:appender msg) + :event event})))) + +(defn swap-framework! + "Swap the framework bound in the session by applying `f` with `args`." + [msg f & args] + (if-let [framework (framework msg)] + (-> (:session msg) + (alter-meta! update-in [::frameworks (:id framework)] #(apply f % args)) + (get-in [::frameworks (:id framework)])) + (throw (ex-info "Log framework not found" + {:type :log-framework-not-found + :framework (:framework msg)})))) + +(defn add-appender-reply + "Add an appender to a log framework." + [msg] + (let [appender (appender msg)] + {:log-add-appender + (-> (swap-framework! msg framework/add-appender appender) + (framework/appender appender) + (deref) + (select-appender))})) + +(defn add-consumer-reply + "Add a consumer to an appender of a log framework." + [{:keys [consumer filters transport] :as msg}] + (let [appender (appender msg) + consumer {:id (or consumer (str (UUID/randomUUID))) + :filters (or filters {}) + :callback (fn [consumer event] + (->> (response-for msg + :log-consumer (str (:id consumer)) + :log-event (select-event event) + :status :log-event) + (transport/send transport)))}] + {:log-add-consumer + (-> (swap-framework! msg framework/add-consumer appender consumer) + (framework/consumer appender consumer) + (select-consumer))})) + +(defn clear-appender-reply + "Clear all events of a log appender." + [msg] + (let [appender (appender msg)] + {:log-clear-appender + (-> (swap-framework! msg framework/clear-appender appender) + (framework/appender appender) + (deref) + (select-appender))})) + +(defn analyze-stacktrace-reply + "Show the stacktrace of a log event in the debugger." + [{:keys [transport ::print/print-fn] :as msg}] + (let [event (event msg)] + (if-let [exception (:exception event)] + (do (doseq [cause (analyzer/analyze exception print-fn)] + (transport/send transport (response-for msg cause))) + (transport/send transport (response-for msg :status :done))) + (transport/send transport (response-for msg :status :no-error))))) + +(defn exceptions-reply + "Return the exceptions and their frequencies for the given framework and appender." + [msg] + {:log-exceptions (->> (framework/events (framework msg) (appender msg)) + (event/exception-frequencies))}) + +(defn frameworks-reply + "Return the available log frameworks." + [{:keys [session]}] + {:log-frameworks (->> (meta session) + ::frameworks vals + (map select-framework))}) + +(defn format-event-reply + "Format a log event." + [{:keys [::print/print-fn] :as msg}] + (let [event (event msg) + writer (StringWriter.)] + (print-fn event writer) + {:log-format-event (str writer)})) + +(defn inspect-event-reply + "Inspect a log event." + [msg] + (inspect-value msg (event msg))) + +(defn levels-reply + "Return the log levels and their frequencies for the given framework and appender." + [msg] + {:log-levels (->> (framework/events (framework msg) (appender msg)) + (event/level-frequencies))}) + +(defn loggers-reply + "Return the loggers and their frequencies for the given framework and appender." + [msg] + {:log-loggers (->> (framework/events (framework msg) (appender msg)) + (event/logger-frequencies))}) + +(defn remove-appender-reply + "Remove an appender from a log framework." + [msg] + (let [appender (appender msg)] + (swap-framework! msg framework/remove-appender appender) + {:log-remove-appender {:id (str (:id appender))}})) + +(defn remove-consumer-reply + "Remove a consumer from the appender of a log framework." + [msg] + (let [appender (appender msg) + consumer (consumer msg)] + (swap-framework! msg framework/remove-consumer appender consumer) + {:log-remove-consumer (select-consumer consumer)})) + +(defn update-appender-reply + "Update the appender of a log framework." + [msg] + (let [appender (appender msg)] + {:log-update-appender + (-> (swap-framework! msg framework/update-appender appender) + (framework/appender appender) + (deref) + (select-appender))})) + +(defn update-consumer-reply + "Update the consumer of a log appender." + [msg] + (let [appender (appender msg) + consumer (consumer msg)] + {:log-update-consumer + (-> (swap-framework! msg framework/update-consumer appender consumer) + (framework/consumer appender consumer) + (select-consumer))})) + +(defn search-reply + "Search the log events of an appender." + [msg] + {:log-search + (->> (select-keys msg [:filters :limit :offset]) + (framework/search-events (framework msg) (appender msg)) + (map select-event))}) + +(defn threads-reply + "Return the threads and their frequencies for the given framework and appender." + [msg] + {:log-threads (->> (framework/events (framework msg) (appender msg)) + (event/thread-frequencies))}) + +(defn handle-log + "Handle NREPL log operations." + [handler {:keys [session] :as msg}] + (when-not (contains? (meta session) ::frameworks) + (alter-meta! session assoc ::frameworks (framework/resolve-frameworks))) + (with-safe-transport handler msg + "log-add-appender" add-appender-reply + "log-add-consumer" add-consumer-reply + "log-analyze-stacktrace" analyze-stacktrace-reply + "log-clear-appender" clear-appender-reply + "log-exceptions" exceptions-reply + "log-format-event" format-event-reply + "log-frameworks" frameworks-reply + "log-inspect-event" inspect-event-reply + "log-levels" levels-reply + "log-loggers" loggers-reply + "log-remove-appender" remove-appender-reply + "log-remove-consumer" remove-consumer-reply + "log-search" search-reply + "log-update-appender" update-appender-reply + "log-update-consumer" update-consumer-reply + "log-threads" threads-reply)) diff --git a/test/clj/cider/log/appender_test.clj b/test/clj/cider/log/appender_test.clj new file mode 100644 index 000000000..0322bcb0b --- /dev/null +++ b/test/clj/cider/log/appender_test.clj @@ -0,0 +1,60 @@ +(ns cider.log.appender-test + (:require [cider.log.appender :as appender] + [cider.log.specs] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as stest] + [clojure.test.check.clojure-test :refer [defspec]] + [clojure.test.check.properties :as prop])) + +(stest/instrument) + +(def appender + (appender/make-appender {:id "my-appender" :levels []})) + +(defspec test-add-consumer + (prop/for-all + [events (s/gen (s/coll-of :cider.log/event))] + (let [captured-events (atom []) + consumer {:id "my-consumer" + :filters {} + :callback #(swap! captured-events conj %2)} + appender (appender/add-consumer appender consumer)] + (let [appender (reduce appender/add-event appender events)] + (appender/remove-consumer appender consumer) + (= events @captured-events))))) + +(defspec test-clear + (prop/for-all + [events (s/gen (s/coll-of :cider.log/event))] + (-> (reduce appender/add-event appender events) + (appender/clear) + (appender/events) + (empty?)))) + +(defspec test-event + (prop/for-all + [events (s/gen (s/coll-of :cider.log/event))] + (let [appender (reduce appender/add-event appender events)] + (= (appender/events appender) + (map #(appender/event appender (:id %)) + (appender/events appender)))))) + +(defspec test-events + (prop/for-all + [events (s/gen (s/coll-of :cider.log/event))] + (let [appender (reduce appender/add-event appender events)] + (= (take (:size appender) (reverse events)) + (appender/events appender))))) + +(defspec test-remove-consumer + (prop/for-all + [events (s/gen (s/coll-of :cider.log/event))] + (let [captured-events (atom []) + consumer {:id "my-consumer" + :filters {} + :callback #(swap! captured-events conj %2)} + appender (appender/add-consumer appender consumer) + appender (reduce appender/add-event appender events)] + (let [appender (appender/remove-consumer appender consumer)] + (doseq [event events] (appender/add-event appender event)) + (= events @captured-events))))) diff --git a/test/clj/cider/log/event_test.clj b/test/clj/cider/log/event_test.clj new file mode 100644 index 000000000..9f7582913 --- /dev/null +++ b/test/clj/cider/log/event_test.clj @@ -0,0 +1,86 @@ +(ns cider.log.event-test + (:require [cider.log.event :as event] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as stest] + [clojure.test.check.clojure-test :refer [defspec]] + [clojure.test.check.properties :as prop] + [clojure.set :as set] + [cider.log.framework :as framework] + [cider.log.test :as test] + [clojure.test.check.generators :as gen])) + +(stest/instrument) + +(def frameworks + (vals (framework/resolve-frameworks))) + +(defspec test-search + (prop/for-all + [{:keys [levels]} (gen/elements frameworks) + criteria (s/gen :cider.log.event/search) + events (s/gen (s/coll-of :cider.log/event))] + (every? #(s/valid? :cider.log/event %) + (event/search levels criteria events)))) + +(defspec test-search-end-time + (prop/for-all + [{:keys [levels]} (gen/elements frameworks) + end-time (s/gen :cider.log.filter/end-time) + events (s/gen (s/coll-of :cider.log/event))] + (every? #(< (:timestamp %) end-time) + (event/search levels {:filters {:end-time end-time}} events)))) + +(defspec test-search-exceptions + (prop/for-all + [{:keys [levels]} (gen/elements frameworks) + exceptions (s/gen :cider.log.filter/exceptions) + events (s/gen (s/coll-of :cider.log/event))] + (let [opts {:filters {:exceptions exceptions}} + events-found (event/search levels opts events)] + (set/subset? (set (map :exception events-found)) + (set (map :exception events)))))) + +(defspec test-search-level + (prop/for-all + [[framework events criteria] + (gen/let [framework (gen/elements frameworks) + level (gen/one-of [(gen/elements (map :name (:levels framework)))]) + events (gen/vector (test/event-gen framework) 3)] + [framework events {:filters {:level level}}])] + (let [level->weight (into {} (map (juxt :name :weight) (:levels framework))) + min-weight (level->weight (-> criteria :filters :level))] + (every? #(>= (level->weight (:level %)) min-weight) + (event/search (:levels framework) criteria events))))) + +(defspec test-search-loggers + (prop/for-all + [{:keys [levels]} (gen/elements frameworks) + loggers (s/gen :cider.log.filter/loggers) + events (s/gen (s/coll-of :cider.log/event))] + (let [opts {:filters {:loggers loggers}} + events-found (event/search levels opts events)] + (set/subset? (set (map :logger events-found)) + (set (map :logger events)))))) + +(defspec test-search-limit + (prop/for-all + [{:keys [levels]} (gen/elements frameworks) + limit (s/gen :cider.log.pagination/limit) + events (s/gen (s/coll-of :cider.log/event))] + (>= limit (count (event/search levels {:limit limit} events))))) + +(defspec test-search-offset + (prop/for-all + [{:keys [levels]} (gen/elements frameworks) + offset (s/gen :cider.log.pagination/limit) + events (s/gen (s/coll-of :cider.log/event))] + (= (drop offset events) + (event/search levels {:offset offset} events)))) + +(defspec test-search-start-time + (prop/for-all + [{:keys [levels]} (gen/elements frameworks) + start-time (s/gen :cider.log.filter/start-time) + events (s/gen (s/coll-of :cider.log/event))] + (every? #(>= (:timestamp %) start-time) + (event/search levels {:filters {:start-time start-time}} events)))) diff --git a/test/clj/cider/log/framework_test.clj b/test/clj/cider/log/framework_test.clj new file mode 100644 index 000000000..57d77ac3d --- /dev/null +++ b/test/clj/cider/log/framework_test.clj @@ -0,0 +1,63 @@ +(ns cider.log.framework-test + (:require [cider.log.framework :as framework] + [cider.log.framework.jul :as jul] + [cider.log.framework.log4j2 :as log4j2] + [cider.log.framework.logback :as logback] + [cider.log.specs] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as stest] + [clojure.test :refer [deftest is testing]] + [clojure.test.check.generators :as gen])) + +(stest/instrument) + +(def appender + {:id "my-appender"}) + +(def frameworks + [jul/framework log4j2/framework logback/framework]) + +(deftest test-add-appender + (doseq [framework frameworks] + (let [framework (framework/add-appender framework appender)] + (is (framework/appender framework appender)) + (framework/remove-appender framework appender)))) + +(deftest test-remove-appender + (doseq [framework frameworks] + (let [framework (-> (framework/add-appender framework appender) + (framework/remove-appender appender))] + (is (nil? (framework/appender framework appender)))))) + +(deftest test-log-levels + (doseq [framework frameworks] + (testing (:name framework) + (is (every? #(s/valid? :cider.log/level %) (:levels framework)))))) + +(deftest test-log-message + ;; TODO: Fix Log4j2 appender reload issue + (org.apache.logging.log4j.core.config.Configurator/reconfigure) + (doseq [framework frameworks] + (testing (:name framework) + (let [event (assoc (gen/generate (s/gen :cider.log/event)) + :level :INFO + :logger (:root-logger framework)) + framework (framework/add-appender framework appender)] + (is (nil? (framework/log framework event))) + (let [events (framework/events framework appender)] + (is (= 1 (count events))) + (let [captured-event (first events)] + (is (= (:arguments event) (:arguments captured-event))) + (is (uuid? (:id captured-event))) + (is (= (:level event) (:level captured-event))) + (is (= (:logger event) (:logger captured-event))) + (is (= (case (keyword (:id framework)) + :jul {} ;; not supported + :log4j2 (:mdc event) + :logback (:mdc event)) + (:mdc captured-event))) + (is (= (:message event) (:message captured-event))) + (is (= (.getName (Thread/currentThread)) + (:thread captured-event))) + (is (pos-int? (:timestamp captured-event))))) + (framework/remove-appender framework appender))))) diff --git a/test/clj/cider/log/repl_test.clj b/test/clj/cider/log/repl_test.clj new file mode 100644 index 000000000..3adfd3992 --- /dev/null +++ b/test/clj/cider/log/repl_test.clj @@ -0,0 +1,161 @@ +(ns cider.log.repl-test + (:require [cider.log.framework :as framework] + [cider.log.repl :as repl] + [cider.log.specs] + [clojure.spec.test.alpha :as stest] + [clojure.test :refer [deftest is testing]]) + (:import [java.util UUID])) + +(stest/instrument) + +(defn frameworks [] + (vals (framework/resolve-frameworks))) + +(defmacro with-each-framework + "Evaluate `body` for each `framework` bound to `framework-sym`." + [[framework-sym frameworks] & body] + `(let [settings# repl/*settings*] + (doseq [framework# ~frameworks :let [~framework-sym framework#]] + (testing (format "Log framework %s" (:name framework#)) + (repl/set-framework! (:id framework#)) + (try ~@body + (finally + (repl/shutdown {:framework (:id framework#)}) + (alter-var-root #'repl/*settings* (constantly settings#)))))))) + +(deftest test-appender + (with-each-framework [framework (frameworks)] + (testing "without any appenders" + (is (nil? (repl/appender)))) + (testing "with an appender" + (repl/add-appender) + (is (= (:appender repl/*settings*) + (:id @(repl/appender))))))) + +(deftest test-appenders + (with-each-framework [framework (frameworks)] + (testing "without any appenders" + (is (empty? (repl/appenders)))) + (testing "with an appender" + (repl/add-appender) + (is (= [(repl/appender)] (repl/appenders)))))) + +(deftest test-add-appender + (with-each-framework [framework (frameworks)] + (is (repl/add-appender)) + (is (= (:appender repl/*settings*) + (:id @(repl/appender)))))) + +(deftest test-add-consumer + (with-each-framework [framework (frameworks)] + (repl/add-appender) + (is (repl/add-consumer)))) + +(deftest test-clear-appender + (with-each-framework [framework (frameworks)] + (let [level (-> framework :levels last :name)] + (repl/add-appender) + (repl/log {:level level :message "1"}) + (repl/log {:level level :message "2"}) + (is (= 2 (count (repl/events)))) + (repl/clear-appender) + (is (= 0 (count (repl/events))))))) + +(deftest test-event + (with-each-framework [framework (frameworks)] + (let [events (atom []) + level (-> framework :levels last :name)] + (repl/add-appender) + (is (nil? (repl/event {:event (UUID/randomUUID)}))) + (repl/add-consumer {:callback (fn [_ event] (swap! events conj event))}) + (repl/log {:arguments [1 2 3] + :mdc {"a" "1"} + :level level + :logger (:root-logger framework) + :message "Hello World"}) + (is (= 1 (count @events))) + (let [event (first @events)] + (is (= event (repl/event {:event (:id event)}))))))) + +(deftest test-events + (with-each-framework [framework (frameworks)] + (let [level (-> framework :levels last :name)] + (repl/add-appender) + (repl/log {:level level :message "Hello World"}) + (repl/log {:level level :message "Hello Moon"}) + (let [events (repl/events {:pattern ".*World"})] + (is (= 1 (count events))) + (is (= "Hello World" (:message (first events)))))))) + +(deftest test-framework + (testing "default framework" + (is (map? (repl/framework)))) + (testing "unsupported framework" + (is (nil? (repl/framework {:framework "unknown"}))))) + +(deftest test-log + (with-each-framework [framework (frameworks)] + (let [events (atom []) + level (-> framework :levels last :name)] + (repl/add-appender) + (repl/add-consumer {:callback (fn [_ event] (swap! events conj event))}) + (repl/log {:arguments [1 2 3] + :mdc {"a" "1"} + :level level + :logger (:root-logger framework) + :message "Hello World"}) + (is (= 1 (count @events))) + (let [event (first @events)] + (is (= [1 2 3] (:arguments event))) + (is (= level (:level event))) + (when (contains? #{"logback"} (:id framework)) + (is (= {"a" "1"} (:mdc event)))) + (is (= "Hello World" (:message event))) + (is (pos-int? (:timestamp event))) + (is (string? (:thread event))))))) + +(deftest test-set-framework! + (with-each-framework [framework (frameworks)] + (doseq [framework (keys repl/*frameworks*)] + (repl/set-framework! framework) + (is (= framework (:id (repl/framework))))))) + +(deftest test-remove-appender + (with-each-framework [framework (frameworks)] + (testing "without any appenders" + (is (thrown-with-msg? clojure.lang.ExceptionInfo + #"Log appender not registered: cider-log" + (repl/remove-appender)))) + (testing "with an appender" + (repl/add-appender) + (repl/appender) + (repl/remove-appender) + (is (not (repl/appender)))))) + +(deftest test-remove-consumer + (with-each-framework [framework (frameworks)] + (repl/add-appender) + (repl/add-consumer) + (is (repl/remove-consumer)))) + +(deftest test-update-appender + (with-each-framework [framework (frameworks)] + (repl/add-appender) + (is (repl/update-appender)))) + +(deftest test-update-consumer + (with-each-framework [framework (frameworks)] + (repl/add-appender) + (repl/add-consumer) + (is (repl/update-consumer)))) + +(deftest test-shutdown + (with-each-framework [framework (frameworks)] + (testing "without any appenders" + (is (= (:id (repl/framework)) + (:id (repl/shutdown))))) + (testing "with an appender" + (repl/add-appender) + (is (= (:id (repl/framework)) + (:id (repl/shutdown)))) + (is (empty? (repl/appenders)))))) diff --git a/test/clj/cider/log/test.clj b/test/clj/cider/log/test.clj new file mode 100644 index 000000000..0886feb7d --- /dev/null +++ b/test/clj/cider/log/test.clj @@ -0,0 +1,17 @@ +(ns cider.log.test + (:require [clojure.spec.alpha :as s] + [clojure.test.check.generators :as gen])) + +(defn- exception-gen [] + (->> (gen/tuple gen/string-alphanumeric + (gen/map gen/keyword gen/any-printable-equatable)) + (gen/fmap (fn [[msg data]] (ex-info msg data))))) + +(defn event-gen [framework] + (->> (gen/tuple (s/gen :cider.log/event) + (gen/elements (:levels framework)) + (exception-gen)) + (gen/fmap (fn [[event level exception]] + (cond-> (assoc event :level (:name level)) + (= :error (:category level)) + (assoc :exception exception)))))) diff --git a/test/clj/cider/nrepl/middleware/log_test.clj b/test/clj/cider/nrepl/middleware/log_test.clj new file mode 100644 index 000000000..dc4860b57 --- /dev/null +++ b/test/clj/cider/nrepl/middleware/log_test.clj @@ -0,0 +1,427 @@ +(ns cider.nrepl.middleware.log-test + (:require [cider.log.framework :as framework] + [cider.log.specs] + [cider.log.test :as test] + [cider.nrepl.test-session :as session] + [clojure.set :as set] + [clojure.spec.test.alpha :as stest] + [clojure.test :refer [deftest is testing use-fixtures]] + [clojure.test.check.generators :as gen])) + +(stest/instrument) + +(use-fixtures :each session/session-fixture) + +(def appender + {:id "my-appender"}) + +(defn frameworks [] + (vals (framework/resolve-frameworks))) + +(defn- find-framework [frameworks framework] + (some #(and (= (name (:id framework)) (:id %)) %) frameworks)) + +(defn- uuid-str? [s] + (try (java.util.UUID/fromString s) + (catch Exception _))) + +(defn- add-appender [framework appender & [opts]] + (let [{:keys [status log-add-appender] :as response} + (session/message (merge {:op "log-add-appender" + :framework (:id framework) + :appender (:id appender)} + opts))] + (assert (= #{"done"} status) response) + log-add-appender)) + +(defn- add-consumer [framework appender & [opts]] + (let [{:keys [status log-add-consumer] :as response} + (session/message (merge {:op "log-add-consumer" + :framework (:id framework) + :appender (:id appender)} + opts))] + (assert (= #{"done"} status) response) + log-add-consumer)) + +(defn- remove-appender [framework appender] + (let [{:keys [status log-remove-appender] :as response} + (session/message {:op "log-remove-appender" + :framework (:id framework) + :appender (:id appender)})] + (assert (= #{"done"} status) response) + log-remove-appender)) + +(defn- search-events [framework appender & [opts]] + (let [{:keys [status log-search] :as response} + (session/message (merge {:op "log-search" + :framework (:id framework) + :appender (:id appender)} + opts))] + (assert (= #{"done"} status) response) + log-search)) + +(defmacro with-each-framework + "Evaluate `body` for each `framework` bound to `framework-sym`." + [[framework-sym frameworks] & body] + `(doseq [framework# ~frameworks :let [~framework-sym framework#]] + (testing (format "Log framework %s" (:name framework#)) + ~@body))) + +(defmacro with-appender + "Add an appender for `framework`, evaluate `body` and remove the appender." + [[framework appender options] & body] + `(let [framework# ~framework, appender# ~appender] + (add-appender framework# appender# ~options) + (try ~@body (finally (remove-appender framework# appender#))))) + +(deftest test-add-appender + (with-each-framework [framework (frameworks)] + (let [options {:filters {} :size 10 :threshold 10} + appender' (add-appender framework appender options)] + (is (= [] (:consumers appender'))) + (is (= (:filters options) (:filters appender'))) + (is (= (:id appender) (:id appender'))) + (is (= (:root-logger framework) (:logger appender'))) + (is (= (:size options) (:size appender'))) + (is (= (:threshold options) (:threshold appender')))) + (remove-appender framework appender))) + +(deftest test-add-consumer + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (let [options {:filters {:level :INFO}} + consumer (add-consumer framework appender options)] + (is (uuid-str? (:id consumer))) + (is (= {:level "INFO"} (:filters consumer)))) + (framework/log framework {:message "a-1"}) + ;; TODO: How to receive the async log event? + ))) + +(deftest test-analyze-stacktrace + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1" :exception (ex-info "BOOM" {:some (Object.)})}) + (let [events (search-events framework appender {})] + (is (= 1 (count events))) + (let [event (first events)] + (is (uuid-str? (:id event))) + (is (string? (:level event))) + (is (string? (:logger event))) + (is (= "a-1" (:message event))) + (is (int? (:timestamp event))) + (let [response (session/message {:op "log-analyze-stacktrace" + :framework (:id framework) + :appender (:id appender) + :event (:id event)})] + (is (= #{"done"} (:status response))) + (is (every? #(set/subset? #{:type :flags} (set (keys %))) + (:stacktrace response))))))))) + +(deftest test-clear + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1"}) + (let [response (session/message {:op "log-clear-appender" + :framework (:id framework) + :appender (:id appender)})] + (is (= #{"done"} (:status response))) + (is (= {:consumers [] + :filters {} + :id (:id appender) + :logger (:root-logger framework) + :size 100000 + :threshold 10} + (:log-clear-appender response))))))) + +(deftest test-exceptions + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1" :exception (IllegalArgumentException. "BOOM")}) + (framework/log framework {:message "b-1" :exception (IllegalStateException. "BOOM")}) + (framework/log framework {:message "b-2" :exception (IllegalStateException. "BOOM")}) + (let [response (session/message {:op "log-exceptions" + :framework (:id framework) + :appender (:id appender)})] + (is (= #{"done"} (:status response))) + (is (= {:java.lang.IllegalArgumentException 1 + :java.lang.IllegalStateException 2} + (:log-exceptions response))))))) + +(deftest test-frameworks + (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (is (= #{"done"} status)) + (is (= ["Logback" "Java Util Logging"] (map :name log-frameworks))) + (with-each-framework [framework (frameworks)] + (let [framework' (find-framework log-frameworks framework)] + (is (= [] (:appenders framework'))) + (is (= (:id framework) (name (:id framework')))) + (is (= (:javadoc-url framework) (:javadoc-url framework'))) + (is (= (:name framework) (:name framework'))) + (is (= (map (fn [level] + {:name (name (:name level)) + :category (name (:category level)) + :weight (:weight level)}) + (:levels framework)) + (:levels framework'))) + (is (= (:website-url framework) (:website-url framework'))) + (is (string? (:root-logger framework'))))))) + +(deftest test-frameworks-add-appender + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (is (= #{"done"} status)) + (let [framework' (find-framework log-frameworks framework)] + (is (= [{:consumers [] + :filters {} + :id (:id appender) + :logger (:root-logger framework) + :size 100000 + :threshold 10}] + (:appenders framework'))) + (is (= (:id framework) (name (:id framework')))) + (is (= (:javadoc-url framework) (:javadoc-url framework'))) + (is (= (:name framework) (:name framework'))) + (is (= (:website-url framework) (:website-url framework'))) + (is (string? (:root-logger framework')))))))) + +(deftest test-format-event + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (add-appender framework appender) + (framework/log framework {:message "a-1"}) + (framework/log framework {:message "a-2"}) + (doseq [event (:log-search + (session/message + {:op "log-search" + :framework (:id framework) + :appender (:id appender)}))] + (let [response (session/message {:op "log-format-event" + :framework (:id framework) + :appender (:id appender) + :event (:id event)})] + (is (= #{"done"} (:status response))) + (is (re-matches (re-pattern (str ".*" (:message event) ".*")) + (:log-format-event response)))))))) + +(deftest test-inspect + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (add-appender framework appender) + (framework/log framework {:message "a-1"}) + (framework/log framework {:message "a-2"}) + (doseq [event (:log-search + (session/message + {:op "log-search" + :framework (:id framework) + :appender (:id appender)}))] + (let [response (session/message {:op "log-inspect-event" + :framework (:id framework) + :appender (:id appender) + :event (:id event)})] + (is (= #{"done"} (:status response))) + (is (re-matches (re-pattern (format "(?s).*%s.*" (:id event))) + (first (:value response))))))))) + +(deftest test-levels + (with-each-framework [framework (frameworks)] + (let [levels (map :name (reverse (:levels framework)))] + (with-appender [framework appender] + (doseq [level levels] + (framework/log framework {:level level :message (name level)})) + (let [response (session/message {:op "log-levels" + :framework (:id framework) + :appender (:id appender)})] + (is (= #{"done"} (:status response))) + (is (= (into {} (map #(vector % 1) levels)) + (:log-levels response)))))))) + +(deftest test-loggers + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:logger "LOGGER-A" :message "a-1"}) + (framework/log framework {:logger "LOGGER-B" :message "b-1"}) + (framework/log framework {:logger "LOGGER-B" :message "b-2"}) + (let [response (session/message {:op "log-loggers" + :framework (:id framework) + :appender (:id appender)})] + (is (= #{"done"} (:status response))) + (is (= {:LOGGER-A 1 :LOGGER-B 2} (:log-loggers response))))))) + +(deftest test-search + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1"}) + (framework/log framework {:message "a-2"}) + (framework/log framework {:message "a-3"}) + (let [events (search-events framework appender {})] + (is (= 3 (count events))) + (is (= ["a-3" "a-2" "a-1"] (map :message events))))))) + +(deftest test-search-by-level + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (let [[level-1 level-2 level-3] (map :name (:levels framework))] + (framework/log framework {:level level-1 :message "a-1"}) + (framework/log framework {:level level-2 :message "a-2"}) + (framework/log framework {:level level-3 :message "a-3"}) + (is (= 3 (count (search-events framework appender {:filters {:level level-1}})))) + (is (= 2 (count (search-events framework appender {:filters {:level level-2}})))) + (is (= 1 (count (search-events framework appender {:filters {:level level-3}})))))))) + +(deftest test-search-by-exception + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1"}) + (framework/log framework {:message "a-2" :exception (IllegalArgumentException. "BOOM")}) + (framework/log framework {:message "a-3" :exception (IllegalStateException. "BOOM")}) + (let [options {:filters {:exceptions ["java.lang.IllegalStateException"]}} + events (search-events framework appender options)] + (is (= 1 (count events))) + (let [event (first events)] + (is (uuid-str? (:id event))) + (is (string? (:level event))) + (is (string? (:logger event))) + (is (= "a-3" (:message event))) + (is (int? (:timestamp event)))))))) + +(deftest test-search-by-pattern + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1"}) + (framework/log framework {:message "a-2"}) + (framework/log framework {:message "a-3"}) + (let [events (search-events framework appender {:filters {:pattern "a-3"}})] + (is (= 1 (count events))) + (let [event (first events)] + (is (uuid-str? (:id event))) + (is (= "INFO" (:level event))) + (is (string? (:logger event))) + (is (= "a-3" (:message event))) + (is (int? (:timestamp event)))))))) + +(deftest test-search-by-start-and-end-time + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1"}) + (Thread/sleep 100) + (framework/log framework {:message "a-2"}) + (Thread/sleep 100) + (framework/log framework {:message "a-3"}) + (let [[event-3 event-2 event-1] (search-events framework appender {})] + (let [options {:filters {:start-time (inc (:timestamp event-1)) + :end-time (dec (:timestamp event-3))}} + events (search-events framework appender options)] + (is (= 1 (count events))) + (let [event (first events)] + (is (= (:id event-2) (:id event))) + (is (= "INFO" (:level event))) + (is (string? (:logger event))) + (is (= "a-2" (:message event))) + (is (int? (:timestamp event))))))))) + +(deftest test-threads + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (framework/log framework {:message "a-1"}) + (let [response (session/message + {:op "log-threads" + :framework (:id framework) + :appender (:id appender)})] + (is (= #{"done"} (:status response))) + (let [threads (:log-threads response)] + (is (every? keyword? (keys threads))) + (is (every? pos-int? (vals threads)))))))) + +(deftest test-remove-appender + (with-each-framework [framework (frameworks)] + (testing "remove unregistered appender" + (let [response (session/message + {:op "log-remove-appender" + :framework (:id framework) + :appender "unknown"})] + (is (= #{"log-remove-appender-error" "done"} (:status response))))) + (testing "remove registered appender" + (let [appender (add-appender framework appender) + appender' (remove-appender framework appender)] + (is (= (:id appender) (:id appender'))) + (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (is (= #{"done"} status)) + (is (empty? (:appenders (find-framework log-frameworks framework))))))))) + +(deftest test-remove-consumer + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (let [consumer (add-consumer framework appender {:filters {:level :INFO}}) + response (session/message + {:op "log-remove-consumer" + :framework (:id framework) + :appender (:id appender) + :consumer (:id consumer)})] + (is (= #{"done"} (:status response))) + (is (= {:id (:id consumer)} + (:log-remove-consumer response))) + (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (is (= #{"done"} status)) + (is (= [{:consumers [] + :filters {} + :id (:id appender) + :logger (:root-logger framework) + :size 100000 + :threshold 10}] + (:appenders (find-framework log-frameworks framework))))))))) + +(deftest test-update-appender + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (let [response (session/message + {:op "log-update-appender" + :appender (:id appender) + :filters {:pattern "A.*"} + :framework (:id framework) + :size 2 + :threshold 0})] + (is (= #{"done"} (:status response))) + (let [appender (:log-update-appender response)] + (is (= {:pattern "A.*"} (:filters appender))) + (is (= 2 (:size appender))) + (is (= 0 (:threshold appender))) + (framework/log framework {:message "A1"}) + (framework/log framework {:message "A2"}) + (framework/log framework {:message "A3"}) + (framework/log framework {:message "A4"}) + (framework/log framework {:message "B1"}) + (let [events (:log-search + (session/message + {:op "log-search" + :framework (:id framework) + :appender (:id appender)}))] + (is (= ["A4" "A3"] (map :message events))))))))) + +(deftest test-update-consumer + (with-each-framework [framework (frameworks)] + (with-appender [framework appender] + (let [consumer (add-consumer framework appender {:filters {:level :INFO}}) + response (session/message + {:op "log-update-consumer" + :framework (:id framework) + :appender (:id appender) + :consumer (:id consumer) + :filters {:level :DEBUG}})] + (is (= #{"done"} (:status response))) + (is (= {:id (:id consumer) + :filters {:level "DEBUG"}} + (:log-update-consumer response))) + (framework/log framework {:message "B1" :level :DEBUG}))))) + +(defn log-something [framework & [^long n ^long sleep]] + (doseq [event (gen/sample (test/event-gen framework) (or n 1))] + (framework/log framework event) + (Thread/sleep (or sleep 10)))) + +(deftest test-log-something + (doseq [framework (frameworks)] + (is (nil? (log-something framework 10))))) + +(comment + (future (log-something (first (frameworks)) 1000))) diff --git a/test/resources/logback-test.xml b/test/resources/logback-test.xml new file mode 100644 index 000000000..94f549f0b --- /dev/null +++ b/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + %d{yyyy-MM-dd}T%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + OFF + + + + + + + + diff --git a/test/resources/logging.properties b/test/resources/logging.properties new file mode 100644 index 000000000..b51cf8e98 --- /dev/null +++ b/test/resources/logging.properties @@ -0,0 +1,59 @@ +############################################################ +# Default Logging Configuration File +# +# You can use a different file by specifying a filename +# with the java.util.logging.config.file system property. +# For example java -Djava.util.logging.config.file=myfile +############################################################ + +############################################################ +# Global properties +############################################################ + +# "handlers" specifies a comma separated list of log Handler +# classes. These handlers will be installed during VM startup. +# Note that these classes must be on the system classpath. +# By default we only configure a ConsoleHandler, which will only +# show messages at the INFO and above levels. +handlers=java.util.logging.FileHandler + +# To also add the FileHandler, use the following line instead. +#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler + +# Default global logging level. +# This specifies which kinds of events are logged across +# all loggers. For any given facility this global level +# can be overriden by a facility specific level +# Note that the ConsoleHandler also has a separate level +# setting to limit messages printed to the console. +.level= ALL + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +# default file output is in user's home directory. +java.util.logging.FileHandler.pattern = %h/java%u.log +java.util.logging.FileHandler.limit = 50000 +java.util.logging.FileHandler.count = 1 +java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter + +# Limit the message that are printed on the console to INFO and above. +java.util.logging.ConsoleHandler.level = INFO +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter + +# Example to customize the SimpleFormatter output format +# to print one-line log message like this: +# : [] +# +# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +# For example, set the com.xyz.foo logger to only log SEVERE +# messages: +com.xyz.foo.level = SEVERE From 8d7c9580679934389411e07788d403491045d765 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 10:12:00 +0200 Subject: [PATCH 02/20] Remove + from install profile --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8c8790c5..70569f9d0 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ to be modified in the `target/srcdeps` directory. When you want to release locally: ``` -lein with-profile +plugin.mranderson/config install +lein with-profile plugin.mranderson/config install ``` #### Using the Makefile From a16f5fda53510d2ee613a6a98ca19e4191a83605 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 10:32:40 +0200 Subject: [PATCH 03/20] Prefix NREPL ops with cider --- src/cider/nrepl.clj | 60 +++++++-------- src/cider/nrepl/middleware/log.clj | 78 +++++++++---------- test/clj/cider/nrepl/middleware/log_test.clj | 80 ++++++++++---------- 3 files changed, 109 insertions(+), 109 deletions(-) diff --git a/src/cider/nrepl.clj b/src/cider/nrepl.clj index 9d6434887..0063dc66b 100644 --- a/src/cider/nrepl.clj +++ b/src/cider/nrepl.clj @@ -288,7 +288,7 @@ Depending on the type of the return value of the evaluation this middleware may {:doc "Middleware that captures log events and makes them inspect-able." :requires #{#'session #'wrap-print} :handles - {"log-add-appender" + {"cider/log-add-appender" {:doc "Add an appender to a log framework." :requires {"framework" "The id of the log framework." "appender" "The name of the appender." @@ -297,52 +297,52 @@ Depending on the type of the return value of the evaluation this middleware may "threshold" "The threshold in percent used to cleanup events."} :optional {"logger" "The name of the logger to attach to."} :returns {"status" "done" - "log-add-appender" "The appender that was added."}} + "cider/log-add-appender" "The appender that was added."}} - "log-add-consumer" + "cider/log-add-consumer" {:doc "Add a consumer to an appender of a log framework." :requires {"framework" "The id of the log framework." "appender" "The name of the appender." "filters" "A map from filter name to filter condition."} :returns {"status" "done" - "log-add-consumer" "The consumer that was added."}} + "cider/log-add-consumer" "The consumer that was added."}} - "log-analyze-stacktrace" + "cider/log-analyze-stacktrace" {:doc "Analyze the stacktrace of a log event." :requires {"framework" "The id of the log framework." "appender" "The name of the appender." "event" "The id of the event to inspect."} :returns {"status" "done"}} - "log-clear-appender" + "cider/log-clear-appender" {:doc "Clear all events of a log appender." :requires {"framework" "The id of the log framework." "appender" "The name of the appender."} :returns {"status" "done" - "log-clear-appender" "The appender that was cleared."}} + "cider/log-clear-appender" "The appender that was cleared."}} - "log-exceptions" + "cider/log-exceptions" {:doc "Return the exceptions and their frequencies for the given framework and appender." :requires {"framework" "The id of the log framework." "appender" "The name of the appender."} :returns {"status" "done" - "log-exceptions" "A map from exception name to event frequency."}} + "cider/log-exceptions" "A map from exception name to event frequency."}} - "log-frameworks" + "cider/log-frameworks" {:doc "Return the available log frameworks." :returns {"status" "done" - "log-frameworks" "A list of log frameworks."}} + "cider/log-frameworks" "A list of log frameworks."}} - "log-format-event" + "cider/log-format-event" {:doc "Format a log event." :requires {"framework" "The id of the log framework." "appender" "The name of the log appender." "event" "The id of the log event."} :optional wrap-print-optional-arguments :returns {"status" "done" - "log-format-event" "The formatted log event."}} + "cider/log-format-event" "The formatted log event."}} - "log-inspect-event" + "cider/log-inspect-event" {:doc "Inspect a log event." :requires {"framework" "The id of the log framework." "appender" "The name of the appender." @@ -350,36 +350,36 @@ Depending on the type of the return value of the evaluation this middleware may :returns {"status" "done" "value" "The inspection result."}} - "log-levels" + "cider/log-levels" {:doc "Return the log levels and their frequencies for the given framework and appender." :requires {"framework" "The id of the log framework." "appender" "The name of the appender."} :returns {"status" "done" - "log-levels" "A map from log level to event frequency."}} + "cider/log-levels" "A map from log level to event frequency."}} - "log-loggers" + "cider/log-loggers" {:doc "Return the loggers and their frequencies for the given framework and appender." :requires {"framework" "The id of the log framework." "appender" "The name of the appender."} :returns {"status" "done" - "log-loggers" "A map from logger name to event frequency."}} + "cider/log-loggers" "A map from logger name to event frequency."}} - "log-remove-appender" + "cider/log-remove-appender" {:doc "Remove an appender from a log framework." :requires {"framework" "The id of the log framework." "appender" "The name of the appender."} :returns {"status" "done" - "log-remove-appender" "The removed appender."}} + "cider/log-remove-appender" "The removed appender."}} - "log-remove-consumer" + "cider/log-remove-consumer" {:doc "Remove a consumer from the appender of a log framework." :requires {"framework" "The id of the log framework." "appender" "The name of the appender." "consumer" "The name of the consumer."} :returns {"status" "done" - "log-add-consumer" "The removed consumer."}} + "cider/log-add-consumer" "The removed consumer."}} - "log-update-appender" + "cider/log-update-appender" {:doc "Update the appender of a log framework." :requires {"framework" "The id of the log framework." "appender" "The name of the appender." @@ -387,32 +387,32 @@ Depending on the type of the return value of the evaluation this middleware may "size" "The number of events the appender keeps in memory." "threshold" "The threshold in percent used to cleanup events."} :returns {"status" "done" - "log-update-appender" "The updated appender."}} + "cider/log-update-appender" "The updated appender."}} - "log-update-consumer" + "cider/log-update-consumer" {:doc "Update the consumer of a log appender." :requires {"framework" "The id of the log framework." "appender" "The name of the appender." "consumer" "The name of the consumer." "filters" "A map from filter name to filter condition."} :returns {"status" "done" - "log-update-consumer" "The consumer that was updated."}} + "cider/log-update-consumer" "The consumer that was updated."}} - "log-search" + "cider/log-search" {:doc "Search the log events of an appender." :requires {"framework" "The id of the log framework." "appender" "The name of the appender."} :optional {"filters" "A map from filter name to filter condition." "limit" "Number of log events to return."} :returns {"status" "done" - "log-search" "The list of log events matching the search."}} + "cider/log-search" "The list of log events matching the search."}} - "log-threads" + "cider/log-threads" {:doc "Return the threads and their frequencies for the given framework and appender." :requires {"framework" "The id of the log framework." "appender" "The name of the appender."} :returns {"status" "done" - "log-threads" "A map from thread name to event frequency."}}}}) + "cider/log-threads" "A map from thread name to event frequency."}}}}) (def-wrapper wrap-macroexpand cider.nrepl.middleware.macroexpand/handle-macroexpand (cljs/requires-piggieback diff --git a/src/cider/nrepl/middleware/log.clj b/src/cider/nrepl/middleware/log.clj index c1df399ef..3e950a184 100644 --- a/src/cider/nrepl/middleware/log.clj +++ b/src/cider/nrepl/middleware/log.clj @@ -139,7 +139,7 @@ "Add an appender to a log framework." [msg] (let [appender (appender msg)] - {:log-add-appender + {:cider/log-add-appender (-> (swap-framework! msg framework/add-appender appender) (framework/appender appender) (deref) @@ -153,11 +153,11 @@ :filters (or filters {}) :callback (fn [consumer event] (->> (response-for msg - :log-consumer (str (:id consumer)) - :log-event (select-event event) - :status :log-event) + :cider/log-consumer (str (:id consumer)) + :cider/log-event (select-event event) + :status :cider/log-event) (transport/send transport)))}] - {:log-add-consumer + {:cider/log-add-consumer (-> (swap-framework! msg framework/add-consumer appender consumer) (framework/consumer appender consumer) (select-consumer))})) @@ -166,7 +166,7 @@ "Clear all events of a log appender." [msg] (let [appender (appender msg)] - {:log-clear-appender + {:cider/log-clear-appender (-> (swap-framework! msg framework/clear-appender appender) (framework/appender appender) (deref) @@ -185,15 +185,15 @@ (defn exceptions-reply "Return the exceptions and their frequencies for the given framework and appender." [msg] - {:log-exceptions (->> (framework/events (framework msg) (appender msg)) - (event/exception-frequencies))}) + {:cider/log-exceptions (->> (framework/events (framework msg) (appender msg)) + (event/exception-frequencies))}) (defn frameworks-reply "Return the available log frameworks." [{:keys [session]}] - {:log-frameworks (->> (meta session) - ::frameworks vals - (map select-framework))}) + {:cider/log-frameworks (->> (meta session) + ::frameworks vals + (map select-framework))}) (defn format-event-reply "Format a log event." @@ -201,7 +201,7 @@ (let [event (event msg) writer (StringWriter.)] (print-fn event writer) - {:log-format-event (str writer)})) + {:cider/log-format-event (str writer)})) (defn inspect-event-reply "Inspect a log event." @@ -211,21 +211,21 @@ (defn levels-reply "Return the log levels and their frequencies for the given framework and appender." [msg] - {:log-levels (->> (framework/events (framework msg) (appender msg)) - (event/level-frequencies))}) + {:cider/log-levels (->> (framework/events (framework msg) (appender msg)) + (event/level-frequencies))}) (defn loggers-reply "Return the loggers and their frequencies for the given framework and appender." [msg] - {:log-loggers (->> (framework/events (framework msg) (appender msg)) - (event/logger-frequencies))}) + {:cider/log-loggers (->> (framework/events (framework msg) (appender msg)) + (event/logger-frequencies))}) (defn remove-appender-reply "Remove an appender from a log framework." [msg] (let [appender (appender msg)] (swap-framework! msg framework/remove-appender appender) - {:log-remove-appender {:id (str (:id appender))}})) + {:cider/log-remove-appender {:id (str (:id appender))}})) (defn remove-consumer-reply "Remove a consumer from the appender of a log framework." @@ -233,13 +233,13 @@ (let [appender (appender msg) consumer (consumer msg)] (swap-framework! msg framework/remove-consumer appender consumer) - {:log-remove-consumer (select-consumer consumer)})) + {:cider/log-remove-consumer (select-consumer consumer)})) (defn update-appender-reply "Update the appender of a log framework." [msg] (let [appender (appender msg)] - {:log-update-appender + {:cider/log-update-appender (-> (swap-framework! msg framework/update-appender appender) (framework/appender appender) (deref) @@ -250,7 +250,7 @@ [msg] (let [appender (appender msg) consumer (consumer msg)] - {:log-update-consumer + {:cider/log-update-consumer (-> (swap-framework! msg framework/update-consumer appender consumer) (framework/consumer appender consumer) (select-consumer))})) @@ -258,7 +258,7 @@ (defn search-reply "Search the log events of an appender." [msg] - {:log-search + {:cider/log-search (->> (select-keys msg [:filters :limit :offset]) (framework/search-events (framework msg) (appender msg)) (map select-event))}) @@ -266,8 +266,8 @@ (defn threads-reply "Return the threads and their frequencies for the given framework and appender." [msg] - {:log-threads (->> (framework/events (framework msg) (appender msg)) - (event/thread-frequencies))}) + {:cider/log-threads (->> (framework/events (framework msg) (appender msg)) + (event/thread-frequencies))}) (defn handle-log "Handle NREPL log operations." @@ -275,19 +275,19 @@ (when-not (contains? (meta session) ::frameworks) (alter-meta! session assoc ::frameworks (framework/resolve-frameworks))) (with-safe-transport handler msg - "log-add-appender" add-appender-reply - "log-add-consumer" add-consumer-reply - "log-analyze-stacktrace" analyze-stacktrace-reply - "log-clear-appender" clear-appender-reply - "log-exceptions" exceptions-reply - "log-format-event" format-event-reply - "log-frameworks" frameworks-reply - "log-inspect-event" inspect-event-reply - "log-levels" levels-reply - "log-loggers" loggers-reply - "log-remove-appender" remove-appender-reply - "log-remove-consumer" remove-consumer-reply - "log-search" search-reply - "log-update-appender" update-appender-reply - "log-update-consumer" update-consumer-reply - "log-threads" threads-reply)) + "cider/log-add-appender" add-appender-reply + "cider/log-add-consumer" add-consumer-reply + "cider/log-analyze-stacktrace" analyze-stacktrace-reply + "cider/log-clear-appender" clear-appender-reply + "cider/log-exceptions" exceptions-reply + "cider/log-format-event" format-event-reply + "cider/log-frameworks" frameworks-reply + "cider/log-inspect-event" inspect-event-reply + "cider/log-levels" levels-reply + "cider/log-loggers" loggers-reply + "cider/log-remove-appender" remove-appender-reply + "cider/log-remove-consumer" remove-consumer-reply + "cider/log-search" search-reply + "cider/log-update-appender" update-appender-reply + "cider/log-update-consumer" update-consumer-reply + "cider/log-threads" threads-reply)) diff --git a/test/clj/cider/nrepl/middleware/log_test.clj b/test/clj/cider/nrepl/middleware/log_test.clj index dc4860b57..d07cddd36 100644 --- a/test/clj/cider/nrepl/middleware/log_test.clj +++ b/test/clj/cider/nrepl/middleware/log_test.clj @@ -26,8 +26,8 @@ (catch Exception _))) (defn- add-appender [framework appender & [opts]] - (let [{:keys [status log-add-appender] :as response} - (session/message (merge {:op "log-add-appender" + (let [{:keys [status cider/log-add-appender] :as response} + (session/message (merge {:op "cider/log-add-appender" :framework (:id framework) :appender (:id appender)} opts))] @@ -35,8 +35,8 @@ log-add-appender)) (defn- add-consumer [framework appender & [opts]] - (let [{:keys [status log-add-consumer] :as response} - (session/message (merge {:op "log-add-consumer" + (let [{:keys [status cider/log-add-consumer] :as response} + (session/message (merge {:op "cider/log-add-consumer" :framework (:id framework) :appender (:id appender)} opts))] @@ -44,16 +44,16 @@ log-add-consumer)) (defn- remove-appender [framework appender] - (let [{:keys [status log-remove-appender] :as response} - (session/message {:op "log-remove-appender" + (let [{:keys [status cider/log-remove-appender] :as response} + (session/message {:op "cider/log-remove-appender" :framework (:id framework) :appender (:id appender)})] (assert (= #{"done"} status) response) log-remove-appender)) (defn- search-events [framework appender & [opts]] - (let [{:keys [status log-search] :as response} - (session/message (merge {:op "log-search" + (let [{:keys [status cider/log-search] :as response} + (session/message (merge {:op "cider/log-search" :framework (:id framework) :appender (:id appender)} opts))] @@ -109,7 +109,7 @@ (is (string? (:logger event))) (is (= "a-1" (:message event))) (is (int? (:timestamp event))) - (let [response (session/message {:op "log-analyze-stacktrace" + (let [response (session/message {:op "cider/log-analyze-stacktrace" :framework (:id framework) :appender (:id appender) :event (:id event)})] @@ -121,7 +121,7 @@ (with-each-framework [framework (frameworks)] (with-appender [framework appender] (framework/log framework {:message "a-1"}) - (let [response (session/message {:op "log-clear-appender" + (let [response (session/message {:op "cider/log-clear-appender" :framework (:id framework) :appender (:id appender)})] (is (= #{"done"} (:status response))) @@ -131,7 +131,7 @@ :logger (:root-logger framework) :size 100000 :threshold 10} - (:log-clear-appender response))))))) + (:cider/log-clear-appender response))))))) (deftest test-exceptions (with-each-framework [framework (frameworks)] @@ -139,16 +139,16 @@ (framework/log framework {:message "a-1" :exception (IllegalArgumentException. "BOOM")}) (framework/log framework {:message "b-1" :exception (IllegalStateException. "BOOM")}) (framework/log framework {:message "b-2" :exception (IllegalStateException. "BOOM")}) - (let [response (session/message {:op "log-exceptions" + (let [response (session/message {:op "cider/log-exceptions" :framework (:id framework) :appender (:id appender)})] (is (= #{"done"} (:status response))) (is (= {:java.lang.IllegalArgumentException 1 :java.lang.IllegalStateException 2} - (:log-exceptions response))))))) + (:cider/log-exceptions response))))))) (deftest test-frameworks - (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (let [{:keys [cider/log-frameworks status]} (session/message {:op "cider/log-frameworks"})] (is (= #{"done"} status)) (is (= ["Logback" "Java Util Logging"] (map :name log-frameworks))) (with-each-framework [framework (frameworks)] @@ -169,7 +169,7 @@ (deftest test-frameworks-add-appender (with-each-framework [framework (frameworks)] (with-appender [framework appender] - (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (let [{:keys [cider/log-frameworks status]} (session/message {:op "cider/log-frameworks"})] (is (= #{"done"} status)) (let [framework' (find-framework log-frameworks framework)] (is (= [{:consumers [] @@ -191,18 +191,18 @@ (add-appender framework appender) (framework/log framework {:message "a-1"}) (framework/log framework {:message "a-2"}) - (doseq [event (:log-search + (doseq [event (:cider/log-search (session/message - {:op "log-search" + {:op "cider/log-search" :framework (:id framework) :appender (:id appender)}))] - (let [response (session/message {:op "log-format-event" + (let [response (session/message {:op "cider/log-format-event" :framework (:id framework) :appender (:id appender) :event (:id event)})] (is (= #{"done"} (:status response))) (is (re-matches (re-pattern (str ".*" (:message event) ".*")) - (:log-format-event response)))))))) + (:cider/log-format-event response)))))))) (deftest test-inspect (with-each-framework [framework (frameworks)] @@ -210,12 +210,12 @@ (add-appender framework appender) (framework/log framework {:message "a-1"}) (framework/log framework {:message "a-2"}) - (doseq [event (:log-search + (doseq [event (:cider/log-search (session/message - {:op "log-search" + {:op "cider/log-search" :framework (:id framework) :appender (:id appender)}))] - (let [response (session/message {:op "log-inspect-event" + (let [response (session/message {:op "cider/log-inspect-event" :framework (:id framework) :appender (:id appender) :event (:id event)})] @@ -229,12 +229,12 @@ (with-appender [framework appender] (doseq [level levels] (framework/log framework {:level level :message (name level)})) - (let [response (session/message {:op "log-levels" + (let [response (session/message {:op "cider/log-levels" :framework (:id framework) :appender (:id appender)})] (is (= #{"done"} (:status response))) (is (= (into {} (map #(vector % 1) levels)) - (:log-levels response)))))))) + (:cider/log-levels response)))))))) (deftest test-loggers (with-each-framework [framework (frameworks)] @@ -242,11 +242,11 @@ (framework/log framework {:logger "LOGGER-A" :message "a-1"}) (framework/log framework {:logger "LOGGER-B" :message "b-1"}) (framework/log framework {:logger "LOGGER-B" :message "b-2"}) - (let [response (session/message {:op "log-loggers" + (let [response (session/message {:op "cider/log-loggers" :framework (:id framework) :appender (:id appender)})] (is (= #{"done"} (:status response))) - (is (= {:LOGGER-A 1 :LOGGER-B 2} (:log-loggers response))))))) + (is (= {:LOGGER-A 1 :LOGGER-B 2} (:cider/log-loggers response))))))) (deftest test-search (with-each-framework [framework (frameworks)] @@ -325,11 +325,11 @@ (with-appender [framework appender] (framework/log framework {:message "a-1"}) (let [response (session/message - {:op "log-threads" + {:op "cider/log-threads" :framework (:id framework) :appender (:id appender)})] (is (= #{"done"} (:status response))) - (let [threads (:log-threads response)] + (let [threads (:cider/log-threads response)] (is (every? keyword? (keys threads))) (is (every? pos-int? (vals threads)))))))) @@ -337,15 +337,15 @@ (with-each-framework [framework (frameworks)] (testing "remove unregistered appender" (let [response (session/message - {:op "log-remove-appender" + {:op "cider/log-remove-appender" :framework (:id framework) :appender "unknown"})] - (is (= #{"log-remove-appender-error" "done"} (:status response))))) + (is (= #{"cider/log-remove-appender-error" "done"} (:status response))))) (testing "remove registered appender" (let [appender (add-appender framework appender) appender' (remove-appender framework appender)] (is (= (:id appender) (:id appender'))) - (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (let [{:keys [log-frameworks status]} (session/message {:op "cider/log-frameworks"})] (is (= #{"done"} status)) (is (empty? (:appenders (find-framework log-frameworks framework))))))))) @@ -354,14 +354,14 @@ (with-appender [framework appender] (let [consumer (add-consumer framework appender {:filters {:level :INFO}}) response (session/message - {:op "log-remove-consumer" + {:op "cider/log-remove-consumer" :framework (:id framework) :appender (:id appender) :consumer (:id consumer)})] (is (= #{"done"} (:status response))) (is (= {:id (:id consumer)} - (:log-remove-consumer response))) - (let [{:keys [log-frameworks status]} (session/message {:op "log-frameworks"})] + (:cider/log-remove-consumer response))) + (let [{:keys [cider/log-frameworks status]} (session/message {:op "cider/log-frameworks"})] (is (= #{"done"} status)) (is (= [{:consumers [] :filters {} @@ -375,14 +375,14 @@ (with-each-framework [framework (frameworks)] (with-appender [framework appender] (let [response (session/message - {:op "log-update-appender" + {:op "cider/log-update-appender" :appender (:id appender) :filters {:pattern "A.*"} :framework (:id framework) :size 2 :threshold 0})] (is (= #{"done"} (:status response))) - (let [appender (:log-update-appender response)] + (let [appender (:cider/log-update-appender response)] (is (= {:pattern "A.*"} (:filters appender))) (is (= 2 (:size appender))) (is (= 0 (:threshold appender))) @@ -391,9 +391,9 @@ (framework/log framework {:message "A3"}) (framework/log framework {:message "A4"}) (framework/log framework {:message "B1"}) - (let [events (:log-search + (let [events (:cider/log-search (session/message - {:op "log-search" + {:op "cider/log-search" :framework (:id framework) :appender (:id appender)}))] (is (= ["A4" "A3"] (map :message events))))))))) @@ -403,7 +403,7 @@ (with-appender [framework appender] (let [consumer (add-consumer framework appender {:filters {:level :INFO}}) response (session/message - {:op "log-update-consumer" + {:op "cider/log-update-consumer" :framework (:id framework) :appender (:id appender) :consumer (:id consumer) @@ -411,7 +411,7 @@ (is (= #{"done"} (:status response))) (is (= {:id (:id consumer) :filters {:level "DEBUG"}} - (:log-update-consumer response))) + (:cider/log-update-consumer response))) (framework/log framework {:message "B1" :level :DEBUG}))))) (defn log-something [framework & [^long n ^long sleep]] From 2687e512de2bfa061f7e92686392ec946e170457 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 10:34:38 +0200 Subject: [PATCH 04/20] Move java.util.logging config into test profile --- project.clj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/project.clj b/project.clj index 3a93fbff7..b8aaac299 100644 --- a/project.clj +++ b/project.clj @@ -34,8 +34,6 @@ :filespecs [{:type :bytes :path "cider/cider-nrepl/project.clj" :bytes ~(slurp "project.clj")}] - :jvm-opts ["-Djava.util.logging.config.file=test/resources/logging.properties"] - :source-paths ["src"] :resource-paths ["resources"] :test-paths ["test/clj" "test/cljs" "test/common"] @@ -111,6 +109,7 @@ :test {:global-vars {*assert* true} :source-paths ["test/src"] :java-source-paths ["test/java"] + :jvm-opts ["-Djava.util.logging.config.file=test/resources/logging.properties"] :resource-paths ["test/resources"] :dependencies [[boot/base "2.8.3"] [boot/core "2.8.3"] From d37a30553be70ca2849a63c69b14aa9ceb95b115 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 10:40:15 +0200 Subject: [PATCH 05/20] Clarify default-size and default-threshold doc strings --- src/cider/log/appender.clj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cider/log/appender.clj b/src/cider/log/appender.clj index 010e62b10..f630b9f2a 100644 --- a/src/cider/log/appender.clj +++ b/src/cider/log/appender.clj @@ -2,11 +2,15 @@ (:require [cider.log.event :as event])) (def ^:private default-size - "The default size of the appender." + "The default number of events captured by an appender." 100000) (def ^:private default-threshold - "The default threshold of the appender in percentage." + "The default threshold in percentage after which log events are cleaned up. + + Events of a log appender are cleanup up if the number of events reach the + `default-size` plus the `default-threshold` percentage of + `default-threshold`." 10) (defn- garbage-collect? From 9df26478041b5f00c2fe33af3b7c2619a668afcb Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 10:42:32 +0200 Subject: [PATCH 06/20] Fix type hint of exception-name --- src/cider/log/event.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cider/log/event.clj b/src/cider/log/event.clj index 1d3db0aed..2481e6b32 100644 --- a/src/cider/log/event.clj +++ b/src/cider/log/event.clj @@ -4,7 +4,7 @@ (defn- exception-name "Return the `exception` class name." - [^Class exception] + [^Throwable exception] (some-> exception .getClass .getName)) (defn exception-frequencies From 1e7dea2e8aeac3941e91aa304b1efca164571492 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 10:55:24 +0200 Subject: [PATCH 07/20] Move test.check to the test profile --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index b8aaac299..e5f93094c 100644 --- a/project.clj +++ b/project.clj @@ -69,7 +69,6 @@ :profiles {:provided {:dependencies [[org.clojure/clojure "1.10.3"] [org.clojure/clojurescript "1.11.4" :scope "provided"] - [org.clojure/test.check "1.1.1"] ;; 1.3.7 and 1.4.7 are working, but we need 1.3.7 for JDK8 [ch.qos.logback/logback-classic "1.3.7"] [com.cognitect/transit-clj "1.0.324"] @@ -113,6 +112,7 @@ :resource-paths ["test/resources"] :dependencies [[boot/base "2.8.3"] [boot/core "2.8.3"] + [org.clojure/test.check "1.1.1"] [org.apache.httpcomponents/httpclient "4.5.13" :exclusions [commons-logging]] [leiningen-core "2.9.10" :exclusions [org.clojure/clojure commons-codec From 12c820c61d31edad6ede872fc745f6ad1eb884c0 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 10:59:12 +0200 Subject: [PATCH 08/20] Use keep instead of remove nil? --- src/cider/log/event.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cider/log/event.clj b/src/cider/log/event.clj index 2481e6b32..c9f204af6 100644 --- a/src/cider/log/event.clj +++ b/src/cider/log/event.clj @@ -10,7 +10,7 @@ (defn exception-frequencies "Return the exception name frequencies of `events`." [events] - (frequencies (remove nil? (map #(some-> % :exception exception-name) events)))) + (frequencies (keep #(some-> % :exception exception-name) events))) (defn logger-frequencies "Return the logger name frequencies of `events`." From 919cd39ed2f7282e43044a496a04392518fe4f5e Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 11:05:14 +0200 Subject: [PATCH 09/20] Move log specs to test dir --- {src => test/clj}/cider/log/specs.clj | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src => test/clj}/cider/log/specs.clj (100%) diff --git a/src/cider/log/specs.clj b/test/clj/cider/log/specs.clj similarity index 100% rename from src/cider/log/specs.clj rename to test/clj/cider/log/specs.clj From 09fac6372f024dc12d17555d3f77f53776c81644 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 12:23:57 +0200 Subject: [PATCH 10/20] Instrument in test namespace --- test/clj/cider/log/appender_test.clj | 4 ---- test/clj/cider/log/event_test.clj | 3 --- test/clj/cider/log/framework_test.clj | 3 --- test/clj/cider/log/repl_test.clj | 3 --- test/clj/cider/log/test.clj | 6 +++++- test/clj/cider/nrepl/middleware/log_test.clj | 3 --- 6 files changed, 5 insertions(+), 17 deletions(-) diff --git a/test/clj/cider/log/appender_test.clj b/test/clj/cider/log/appender_test.clj index 0322bcb0b..ae87d04a5 100644 --- a/test/clj/cider/log/appender_test.clj +++ b/test/clj/cider/log/appender_test.clj @@ -1,13 +1,9 @@ (ns cider.log.appender-test (:require [cider.log.appender :as appender] - [cider.log.specs] [clojure.spec.alpha :as s] - [clojure.spec.test.alpha :as stest] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop])) -(stest/instrument) - (def appender (appender/make-appender {:id "my-appender" :levels []})) diff --git a/test/clj/cider/log/event_test.clj b/test/clj/cider/log/event_test.clj index 9f7582913..def9ba2d8 100644 --- a/test/clj/cider/log/event_test.clj +++ b/test/clj/cider/log/event_test.clj @@ -1,7 +1,6 @@ (ns cider.log.event-test (:require [cider.log.event :as event] [clojure.spec.alpha :as s] - [clojure.spec.test.alpha :as stest] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.set :as set] @@ -9,8 +8,6 @@ [cider.log.test :as test] [clojure.test.check.generators :as gen])) -(stest/instrument) - (def frameworks (vals (framework/resolve-frameworks))) diff --git a/test/clj/cider/log/framework_test.clj b/test/clj/cider/log/framework_test.clj index 57d77ac3d..c855c6f01 100644 --- a/test/clj/cider/log/framework_test.clj +++ b/test/clj/cider/log/framework_test.clj @@ -5,12 +5,9 @@ [cider.log.framework.logback :as logback] [cider.log.specs] [clojure.spec.alpha :as s] - [clojure.spec.test.alpha :as stest] [clojure.test :refer [deftest is testing]] [clojure.test.check.generators :as gen])) -(stest/instrument) - (def appender {:id "my-appender"}) diff --git a/test/clj/cider/log/repl_test.clj b/test/clj/cider/log/repl_test.clj index 3adfd3992..186ce37f7 100644 --- a/test/clj/cider/log/repl_test.clj +++ b/test/clj/cider/log/repl_test.clj @@ -2,12 +2,9 @@ (:require [cider.log.framework :as framework] [cider.log.repl :as repl] [cider.log.specs] - [clojure.spec.test.alpha :as stest] [clojure.test :refer [deftest is testing]]) (:import [java.util UUID])) -(stest/instrument) - (defn frameworks [] (vals (framework/resolve-frameworks))) diff --git a/test/clj/cider/log/test.clj b/test/clj/cider/log/test.clj index 0886feb7d..3c9806467 100644 --- a/test/clj/cider/log/test.clj +++ b/test/clj/cider/log/test.clj @@ -1,7 +1,11 @@ (ns cider.log.test - (:require [clojure.spec.alpha :as s] + (:require [cider.log.specs] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as stest] [clojure.test.check.generators :as gen])) +(stest/instrument) + (defn- exception-gen [] (->> (gen/tuple gen/string-alphanumeric (gen/map gen/keyword gen/any-printable-equatable)) diff --git a/test/clj/cider/nrepl/middleware/log_test.clj b/test/clj/cider/nrepl/middleware/log_test.clj index d07cddd36..82488a46b 100644 --- a/test/clj/cider/nrepl/middleware/log_test.clj +++ b/test/clj/cider/nrepl/middleware/log_test.clj @@ -4,12 +4,9 @@ [cider.log.test :as test] [cider.nrepl.test-session :as session] [clojure.set :as set] - [clojure.spec.test.alpha :as stest] [clojure.test :refer [deftest is testing use-fixtures]] [clojure.test.check.generators :as gen])) -(stest/instrument) - (use-fixtures :each session/session-fixture) (def appender From 17f9a3662323d7e558e1508532d366220cf1ddb0 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 12:54:12 +0200 Subject: [PATCH 11/20] Complete repl namespace specs --- test/clj/cider/log/specs.clj | 250 ++++++++++++++++++++++++----------- 1 file changed, 171 insertions(+), 79 deletions(-) diff --git a/test/clj/cider/log/specs.clj b/test/clj/cider/log/specs.clj index 9d13a91e3..bb449f8c5 100644 --- a/test/clj/cider/log/specs.clj +++ b/test/clj/cider/log/specs.clj @@ -2,7 +2,8 @@ (:require [cider.log.appender :as appender] [cider.log.framework :as framework] [cider.log.repl :as repl] - [clojure.spec.alpha :as s])) + [clojure.spec.alpha :as s]) + (:import [java.util.regex Pattern])) (s/def :cider.log.level/category simple-keyword?) (s/def :cider.log.level/name simple-keyword?) @@ -75,13 +76,16 @@ (s/def :cider.log.appender/size pos-int?) (s/def :cider.log.appender/threshold (s/and nat-int? #(< % 100))) -(s/def :cider.log/appender +(s/def :cider.log.appender/options (s/keys :req-un [:cider.log.appender/id] :opt-un [:cider.log.appender/levels :cider.log.appender/logger :cider.log.appender/size :cider.log.appender/threshold])) +(s/def :cider.log/appender + #(instance? clojure.lang.Atom %)) + (s/def :cider.log.consumer/callback ifn?) (s/def :cider.log.consumer/filter (s/map-of string? any?)) (s/def :cider.log.consumer/id string?) @@ -113,44 +117,67 @@ ;; cider.log.framework +(s/fdef framework/appender + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options) + :ret :cider.log/appender) + +(s/fdef framework/appenders + :args (s/cat :framework :cider.log/framework) + :ret (s/coll-of :cider.log/appender)) + (s/fdef framework/add-appender - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options) :ret :cider.log/framework) (s/fdef framework/add-consumer - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options + :consumer :cider.log/consumer) :ret :cider.log/framework) (s/fdef framework/clear-appender - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options) :ret :cider.log/framework) (s/fdef framework/consumer - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options + :consumer :cider.log/consumer) :ret (s/nilable :cider.log/consumer)) (s/fdef framework/event - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :id :cider.log.event/id) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options + :id :cider.log.event/id) :ret (s/nilable :cider.log/event)) (s/fdef framework/events - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options) :ret (s/coll-of :cider.log/event)) (s/fdef framework/log - :args (s/cat :framework :cider.log/framework :event map?) + :args (s/cat :framework :cider.log/framework + :event map?) :ret nil?) (s/fdef framework/remove-appender - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options) :ret :cider.log/framework) (s/fdef framework/remove-consumer - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options + :consumer :cider.log/consumer) :ret :cider.log/framework) (s/fdef framework/update-appender - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options) :ret :cider.log/framework) (s/fdef framework/resolve-framework @@ -163,124 +190,189 @@ :ret (s/map-of :cider.log.framework/id :cider.log/framework)) (s/fdef framework/search-events - :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :criteria map?) + :args (s/cat :framework :cider.log/framework + :appender :cider.log.appender/options + :criteria map?) :ret (s/coll-of :cider.log/event)) ;; cider.log.appender (s/fdef appender/add-consumer - :args (s/cat :appender :cider.log/appender :consumer :cider.log/consumer) - :ret :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options + :consumer :cider.log/consumer) + :ret :cider.log.appender/options) (s/fdef appender/add-event - :args (s/cat :appender :cider.log/appender :event :cider.log/event) - :ret :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options + :event :cider.log/event) + :ret :cider.log.appender/options) (s/fdef appender/clear - :args (s/cat :appender :cider.log/appender) - :ret :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options) + :ret :cider.log.appender/options) (s/fdef appender/consumers - :args (s/cat :appender :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options) :ret (s/coll-of :cider.log/consumer)) (s/fdef appender/consumer-by-id - :args (s/cat :appender :cider.log/appender :id :cider.log.consumer/id) + :args (s/cat :appender :cider.log.appender/options + :id :cider.log.consumer/id) :ret (s/nilable :cider.log/consumer)) (s/fdef appender/event - :args (s/cat :appender :cider.log/appender :id :cider.log.event/id) + :args (s/cat :appender :cider.log.appender/options + :id :cider.log.event/id) :ret (s/nilable :cider.log/event)) (s/fdef appender/events - :args (s/cat :appender :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options) :ret (s/coll-of :cider.log/event)) (s/fdef appender/make-appender - :args (s/cat :appender :cider.log/appender) - :ret :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options) + :ret :cider.log.appender/options) (s/fdef appender/remove-consumer - :args (s/cat :appender :cider.log/appender :consumer :cider.log/consumer) - :ret :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options + :consumer :cider.log/consumer) + :ret :cider.log.appender/options) (s/fdef appender/update-appender - :args (s/cat :appender :cider.log/appender :settings map?) - :ret :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options + :settings map?) + :ret :cider.log.appender/options) (s/fdef appender/update-consumer - :args (s/cat :appender :cider.log/appender :consumer :cider.log/consumer) - :ret :cider.log/appender) + :args (s/cat :appender :cider.log.appender/options + :consumer :cider.log/consumer) + :ret :cider.log.appender/options) ;; cider.log.repl -(s/def :cider.log.repl.option/framework +(s/def :cider.log.repl.option/appender (s/nilable (s/or :string string? :keyword keyword?))) -(s/def :cider.log.repl.option/appender +(s/def :cider.log.repl.option/callback ifn?) + +(s/def :cider.log.repl.option/consumer (s/nilable (s/or :string string? :keyword keyword?))) -(s/def :cider.log.repl/options - (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework - :cider.log.repl.option/appender])))) +(s/def :cider.log.repl.option/exceptions + (s/nilable (s/coll-of string?))) + +(s/def :cider.log.repl.option/event uuid?) + +(s/def :cider.log.repl.option/filters + (s/nilable (s/map-of keyword? any?))) + +(s/def :cider.log.repl.option/framework + (s/nilable (s/or :string string? :keyword keyword?))) + +(s/def :cider.log.repl.option/logger + (s/nilable string?)) + +(s/def :cider.log.repl.option/loggers + (s/nilable (s/coll-of string?))) + +(s/def :cider.log.repl.option/pattern + (s/nilable (s/or :string string? :regex #(instance? Pattern %)))) + +(s/def :cider.log.repl.option/size + (s/nilable pos-int?)) + +(s/def :cider.log.repl.option/threads + (s/nilable (s/coll-of string?))) + +(s/def :cider.log.repl.option/threshold + (s/nilable (s/and nat-int? #(<= 0 % 100)))) (s/fdef repl/add-appender - :args (s/cat :options :cider.log.repl/options) + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender + :cider.log.repl.option/filters + :cider.log.repl.option/logger + :cider.log.repl.option/size + :cider.log.repl.option/threshold])))) + :ret :cider.log/appender) + +(s/fdef repl/add-consumer + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/callback + :cider.log.repl.option/consumer + :cider.log.repl.option/filters + :cider.log.repl.option/framework])))) :ret :cider.log/framework) -;; (s/fdef repl/add-consumer -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) -;; :ret :cider.log/framework) +(s/fdef repl/appender + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender])))) + :ret (s/nilable :cider.log/appender)) -;; (s/fdef repl/clear-appender -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) -;; :ret :cider.log/framework) +(s/fdef repl/appenders + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework])))) + :ret (s/coll-of :cider.log/appender)) -;; (s/fdef repl/consumer -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) -;; :ret (s/nilable :cider.log/consumer)) +(s/fdef repl/clear-appender + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender])))) + :ret :cider.log/framework) -;; (s/fdef repl/event -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :id :cider.log.event/id) -;; :ret (s/nilable :cider.log/event)) +(s/fdef repl/event + :args (s/cat :options (s/? (s/nilable (s/keys :req-un [:cider.log.repl.option/event] + :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/framework])))) + :ret :cider.log/framework) -;; (s/fdef repl/events -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) -;; :ret (s/coll-of :cider.log/event)) +(s/fdef repl/events + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/exceptions + :cider.log.repl.option/framework + :cider.log.repl.option/loggers + :cider.log.repl.option/pattern + :cider.log.repl.option/threads])))) + :ret :cider.log/framework) -;; (s/fdef repl/log -;; :args (s/cat :framework :cider.log/framework :event map?) -;; :ret nil?) +(s/fdef repl/framework + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework])))) + :ret :cider.log/framework) -;; (s/fdef repl/remove-appender -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) -;; :ret :cider.log/framework) +(s/fdef repl/remove-appender + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender])))) + :ret :cider.log/framework) -;; (s/fdef repl/remove-consumer -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :consumer :cider.log/consumer) -;; :ret :cider.log/framework) +(s/fdef repl/remove-consumer + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/consumer + :cider.log.repl.option/framework])))) + :ret :cider.log/framework) (s/fdef repl/set-appender! :args (s/cat :framework (s/or :string string? :keyword keyword?)) - :ret nil?) - -(s/fdef repl/set-framework! - :args (s/cat :framework (s/or :string string? :keyword keyword?)) - :ret nil?) + :ret map?) -;; (s/fdef repl/update-appender -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender) -;; :ret :cider.log/framework) +(s/fdef repl/set-consumer! + :args (s/cat :consumer (s/or :string string? :keyword keyword?)) + :ret map?) -;; (s/fdef repl/resolve-framework -;; :args (s/cat :framework-sym qualified-symbol?) -;; :ret (s/nilable :cider.log/framework)) +(s/fdef repl/shutdown + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework])))) + :ret :cider.log/framework) -;; (s/fdef repl/resolve-frameworks -;; :args (s/or :arity-0 (s/cat) -;; :arity-1 (s/cat :framework-syms (s/coll-of qualified-symbol?))) -;; :ret (s/map-of :cider.log.framework/id :cider.log/framework)) +(s/fdef repl/update-appender + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender + :cider.log.repl.option/filters + :cider.log.repl.option/logger + :cider.log.repl.option/size + :cider.log.repl.option/threshold])))) + :ret :cider.log/framework) -;; (s/fdef repl/search-events -;; :args (s/cat :framework :cider.log/framework :appender :cider.log/appender :criteria map?) -;; :ret (s/coll-of :cider.log/event)) +(s/fdef repl/update-consumer + :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/callback + :cider.log.repl.option/consumer + :cider.log.repl.option/filters + :cider.log.repl.option/framework])))) + :ret :cider.log/framework) From e195d140425b037555f5e100cd9317af5face60a Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 13:01:54 +0200 Subject: [PATCH 12/20] Remove obsolete disable-warning for deprecation --- eastwood.clj | 8 -------- 1 file changed, 8 deletions(-) diff --git a/eastwood.clj b/eastwood.clj index 04468d479..5b2a77604 100644 --- a/eastwood.clj +++ b/eastwood.clj @@ -12,11 +12,3 @@ (disable-warning {:linter :deprecations :symbol-matches #{#"^public final void java\.lang\.Thread\.stop\(\)$"}}) - -(disable-warning - {:linter :deprecations - :symbol-matches #{#"^public long java\.lang\.Thread\.getId\(\)$"}}) - -(disable-warning - {:linter :deprecations - :symbol-matches #{#"^public int java\.util\.logging\.LogRecord\.getThreadID\(\)$"}}) From f7f92bb34da638299cbdc13ebca2217449843113 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Tue, 13 Jun 2023 13:30:28 +0200 Subject: [PATCH 13/20] Add namespace doc strings and metadata --- src/cider/log/appender.clj | 2 ++ src/cider/log/event.clj | 2 ++ src/cider/log/framework.clj | 7 ++++--- src/cider/log/framework/jul.clj | 2 ++ src/cider/log/framework/log4j2.clj | 2 ++ src/cider/log/framework/logback.clj | 2 ++ src/cider/nrepl/middleware/log.clj | 3 +++ 7 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/cider/log/appender.clj b/src/cider/log/appender.clj index f630b9f2a..17aea8618 100644 --- a/src/cider/log/appender.clj +++ b/src/cider/log/appender.clj @@ -1,4 +1,6 @@ (ns cider.log.appender + "A log appender that captures log events in memory." + {:author "r0man"} (:require [cider.log.event :as event])) (def ^:private default-size diff --git a/src/cider/log/event.clj b/src/cider/log/event.clj index c9f204af6..412121303 100644 --- a/src/cider/log/event.clj +++ b/src/cider/log/event.clj @@ -1,4 +1,6 @@ (ns cider.log.event + "Log event related utilities like searching and calculating frequencies." + {:author "r0man"} (:require [clojure.string :as str]) (:import [java.util.regex Pattern])) diff --git a/src/cider/log/framework.clj b/src/cider/log/framework.clj index b3b62bc3e..18e232ecd 100644 --- a/src/cider/log/framework.clj +++ b/src/cider/log/framework.clj @@ -1,12 +1,13 @@ (ns cider.log.framework + "A unified interface to capture and inspect log events of Java logging + frameworks." + {:author "r0man"} (:require [cider.log.appender :as appender] [cider.log.event :as event])) (def ^:dynamic *frameworks* ['cider.log.framework.logback/framework - 'cider.log.framework.jul/framework - ;; 'cider.log.framework.log4j2/framework - ]) + 'cider.log.framework.jul/framework]) (defn- ex-appender-not-found "Return the appender not found exception info." diff --git a/src/cider/log/framework/jul.clj b/src/cider/log/framework/jul.clj index 0652a3542..c042c6610 100644 --- a/src/cider/log/framework/jul.clj +++ b/src/cider/log/framework/jul.clj @@ -1,4 +1,6 @@ (ns cider.log.framework.jul + "Log event capturing implementation for Java Util Logging." + {:author "r0man"} (:require [cider.log.appender :as appender] [clojure.set :as set]) (:import (java.util.logging Level Logger LogRecord StreamHandler))) diff --git a/src/cider/log/framework/log4j2.clj b/src/cider/log/framework/log4j2.clj index 55711274b..79146a032 100644 --- a/src/cider/log/framework/log4j2.clj +++ b/src/cider/log/framework/log4j2.clj @@ -1,4 +1,6 @@ (ns cider.log.framework.log4j2 + "Log event capturing implementation for LOG4J2." + {:author "r0man"} (:require [cider.log.appender :as appender] [clojure.set :as set]) (:import (org.apache.logging.log4j Level MarkerManager ThreadContext) diff --git a/src/cider/log/framework/logback.clj b/src/cider/log/framework/logback.clj index dd23af092..f4e5e3a5f 100644 --- a/src/cider/log/framework/logback.clj +++ b/src/cider/log/framework/logback.clj @@ -1,4 +1,6 @@ (ns cider.log.framework.logback + "Log event capturing implementation for Logback." + {:author "r0man"} (:require [cider.log.appender :as appender] [clojure.set :as set]) (:import (ch.qos.logback.classic Level Logger LoggerContext) diff --git a/src/cider/nrepl/middleware/log.clj b/src/cider/nrepl/middleware/log.clj index 3e950a184..019c84847 100644 --- a/src/cider/nrepl/middleware/log.clj +++ b/src/cider/nrepl/middleware/log.clj @@ -1,4 +1,7 @@ (ns cider.nrepl.middleware.log + "Capture, debug, inspect and view log events emitted by Java logging frameworks." + {:author "r0man" + :added "1.8"} (:require [cider.log.event :as event] [cider.log.framework :as framework] [cider.nrepl.middleware.inspect :as middleware.inspect] From 874fc48af693ef0cf2be7a7076737879d83bf1c9 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Wed, 14 Jun 2023 11:04:31 +0200 Subject: [PATCH 14/20] Add changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2166853f5..1c8f08eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master (unreleased) +### New features + +* [#773](https://github.com/clojure-emacs/cider-nrepl/pull/773) Add middleware to capture, debug, inspect and view log events emitted by Java logging frameworks. + ### Changes * Bump `cljfmt` to 0.9.2. From ba09c4fd550a25b07413d52c30fe788a8efc4f5a Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Wed, 14 Jun 2023 11:05:03 +0200 Subject: [PATCH 15/20] Use the next cider-nrepl version in metadata --- src/cider/nrepl/middleware/log.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cider/nrepl/middleware/log.clj b/src/cider/nrepl/middleware/log.clj index 019c84847..9ed631a9f 100644 --- a/src/cider/nrepl/middleware/log.clj +++ b/src/cider/nrepl/middleware/log.clj @@ -1,7 +1,7 @@ (ns cider.nrepl.middleware.log "Capture, debug, inspect and view log events emitted by Java logging frameworks." {:author "r0man" - :added "1.8"} + :added "0.30.1"} (:require [cider.log.event :as event] [cider.log.framework :as framework] [cider.nrepl.middleware.inspect :as middleware.inspect] From 59f4535a163a1c15223769af60bc23b6da79a9d1 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Wed, 14 Jun 2023 11:07:55 +0200 Subject: [PATCH 16/20] Add cider.nrepl/wrap-log to usage doc --- doc/modules/ROOT/pages/usage.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/modules/ROOT/pages/usage.adoc b/doc/modules/ROOT/pages/usage.adoc index 599f75071..86e030e14 100644 --- a/doc/modules/ROOT/pages/usage.adoc +++ b/doc/modules/ROOT/pages/usage.adoc @@ -41,6 +41,7 @@ under `:repl-options`. 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-spec From d0bae21e7edd502f321d328070c7e6a112f44ecb Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Wed, 14 Jun 2023 11:08:37 +0200 Subject: [PATCH 17/20] Add cider.nrepl/wrap-log to cider-middleware --- doc/modules/ROOT/pages/usage.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/modules/ROOT/pages/usage.adoc b/doc/modules/ROOT/pages/usage.adoc index 86e030e14..27e7a7ef6 100644 --- a/doc/modules/ROOT/pages/usage.adoc +++ b/doc/modules/ROOT/pages/usage.adoc @@ -154,6 +154,7 @@ That's how CIDER's nREPL handler is created: 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 From f249be0a4506d06acb7aa373cf6ea35eba3c04ef Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Wed, 14 Jun 2023 11:11:49 +0200 Subject: [PATCH 18/20] Add wrap-log to supplied middlewares --- doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc b/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc index 5a3d9b296..f986f6d08 100644 --- a/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc +++ b/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc @@ -57,6 +57,12 @@ | `inspect-(start/refresh/pop/push/reset/get-path)` | Inspect a Clojure expression. +| `wrap-log` +| 0.30.1 +| No +| `cider-version` +| Capture, debug, inspect and view log events emitted by Java logging frameworks. + | `wrap-macroexpand` | - | Yes From a1541ef46b36278a35c1a7ea51c07a16c4a43568 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Wed, 14 Jun 2023 11:15:56 +0200 Subject: [PATCH 19/20] Add log ops to supplied middleware --- doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc b/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc index f986f6d08..f35212c0f 100644 --- a/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc +++ b/doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc @@ -60,7 +60,7 @@ | `wrap-log` | 0.30.1 | No -| `cider-version` +| `cider/log-add-appender`, `cider/log-add-consumer`, `cider/log-analyze-stacktrace`, `cider/log-clear-appender`, `cider/log-exceptions`, `cider/log-format-event`, `cider/log-frameworks`, `cider/log-inspect-event`, `cider/log-levels`, `cider/log-loggers`, `cider/log-remove-appender`, `cider/log-remove-consumer`, `cider/log-search`, `cider/log-update-appender`, `cider/log-update-consumer`, `cider/log-threads` | Capture, debug, inspect and view log events emitted by Java logging frameworks. | `wrap-macroexpand` From f6ef379bc6d522c11c5c2b676b131042c9ded3ec Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Mon, 19 Jun 2023 11:03:38 +0200 Subject: [PATCH 20/20] Accept keyword arguments in cider.log.repl This is a bit more user friendly when typing in commands in the REPL. In Clojure 1.11 both, keyword arguments and a single options map, are supported. --- src/cider/log/repl.clj | 40 +++++++-------- test/clj/cider/log/repl_test.clj | 42 ++++++++-------- test/clj/cider/log/specs.clj | 86 ++++++++++++++++---------------- 3 files changed, 84 insertions(+), 84 deletions(-) diff --git a/src/cider/log/repl.clj b/src/cider/log/repl.clj index 6bcec864f..e2b624372 100644 --- a/src/cider/log/repl.clj +++ b/src/cider/log/repl.clj @@ -87,7 +87,7 @@ (alter-var-root #'*settings* #(apply f % args)) *settings*) -;; Log Frameworks +;; Log Framework (defn- ensure-framework "Ensure that the :framework in `options` is valid. Returns the @@ -104,7 +104,7 @@ Options: :framework - The identifier of the framework." - [& [options]] + [& {:as options}] (let [{:keys [framework]} (merge-default options)] (get *frameworks* (some-> framework name)))) @@ -125,11 +125,11 @@ Options: :framework - The identifier of the framework." - [& [options]] + [& {:as options}] (let [options (merge-default options)] (swap-framework! options framework/shutdown))) -;; Log Appenders +;; Log Appender (defn- ensure-appender "Ensure that the :appender in `options` is registered. Returns the @@ -148,7 +148,7 @@ Options: :framework - The identifier of the framework." - [& [options]] + [& {:as options}] (framework/appenders (ensure-framework options))) (defn appender @@ -157,7 +157,7 @@ Options: :framework - The identifier of the framework. :appender - The identifier of the appender." - [& [options]] + [& {:as options}] (let [{:keys [appender]} (merge-default options)] (framework/appender (ensure-framework options) {:id appender}))) @@ -171,10 +171,10 @@ :logger - The name of the logger to which the append will be attached. :size - The number of events to capture in the appender. :threshold - A threshold in percentage used to garbage collect log events." - [& [options]] + [& {:as options}] (let [options (merge-default options)] - (swap-framework! options framework/add-appender (appender-options options)) - (appender options))) + (-> (swap-framework! options framework/add-appender (appender-options options)) + (framework/appender {:id (:appender options)})))) (defn clear-appender "Clear the events captured by and appender of a framework. @@ -182,7 +182,7 @@ Options: :framework - The identifier of the framework. :appender - The identifier of the appender." - [& [options]] + [& {:as options}] (swap-framework! options framework/clear-appender @(ensure-appender options))) (defn remove-appender @@ -191,7 +191,7 @@ Options: :framework - The identifier of the framework. :appender - The identifier of the appender." - [& [options]] + [& {:as options}] (swap-framework! options framework/remove-appender @(ensure-appender options))) (defn set-appender! @@ -209,11 +209,11 @@ :logger - The name of the logger to which the append will be attached. :size - The number of events to capture in the appender. :threshold - A threshold in percentage used to garbage collect log events." - [& [options]] + [& {:as options}] (ensure-appender options) (swap-framework! options framework/update-appender (appender-options options))) -;; Log Consumers +;; Log Consumer (defn add-consumer "Add a log consumer to a framework. @@ -224,7 +224,7 @@ :consumer - The identifier of the consumer. :callback - A function that will be called for each log event. :filters - A map of criteria used to filter log events." - [& [options]] + [& {:as options}] (swap-framework! options framework/add-consumer @(ensure-appender options) (consumer-options options))) @@ -236,7 +236,7 @@ :framework - The identifier of the framework. :appender - The identifier of the appender. :consumer - The identifier of the consumer." - [& [options]] + [& {:as options}] (let [{:keys [consumer]} (merge-default options)] (swap-framework! options framework/remove-consumer @(ensure-appender options) @@ -250,7 +250,7 @@ :appender - The identifier of the appender. :consumer - The identifier of the consumer. :filters - A map of criteria used to filter log events." - [& [options]] + [& {:as options}] (let [options (merge-default options)] (swap-framework! options framework/update-consumer @(ensure-appender options) @@ -261,7 +261,7 @@ [identifier] (update-settings! assoc :consumer (name identifier))) -;; Log Events +;; Log Event (defn event "Find a log event captured by the an appender of a framework. @@ -270,7 +270,7 @@ :framework - The identifier of the framework. :appender - The identifier of the appender. :event - The identifier of the event." - [& [options]] + [& {:as options}] (when-let [event-id (:event options)] (framework/event (ensure-framework options) @(ensure-appender options) @@ -289,7 +289,7 @@ :pattern - Only include events whose message matches the regex pattern. :start-time - Only include events after this timestamp. :threads - Only include events emitted by the given threads." - [& [options]] + [& {:as options}] (framework/search-events (ensure-framework options) @(ensure-appender options) (criteria options))) @@ -304,7 +304,7 @@ :arguments - The arguments of the log event. :level - The level of the log event. :logger - The logger used to emit the log event." - [options] + [& {:as options}] (let [options (merge-default options)] (framework/log (ensure-framework options) (let [{:keys [arguments level logger message mdc]} options] diff --git a/test/clj/cider/log/repl_test.clj b/test/clj/cider/log/repl_test.clj index 186ce37f7..5c275a2a4 100644 --- a/test/clj/cider/log/repl_test.clj +++ b/test/clj/cider/log/repl_test.clj @@ -17,7 +17,7 @@ (repl/set-framework! (:id framework#)) (try ~@body (finally - (repl/shutdown {:framework (:id framework#)}) + (repl/shutdown :framework (:id framework#)) (alter-var-root #'repl/*settings* (constantly settings#)))))))) (deftest test-appender @@ -52,8 +52,8 @@ (with-each-framework [framework (frameworks)] (let [level (-> framework :levels last :name)] (repl/add-appender) - (repl/log {:level level :message "1"}) - (repl/log {:level level :message "2"}) + (repl/log :level level :message "1") + (repl/log :level level :message "2") (is (= 2 (count (repl/events)))) (repl/clear-appender) (is (= 0 (count (repl/events))))))) @@ -63,24 +63,24 @@ (let [events (atom []) level (-> framework :levels last :name)] (repl/add-appender) - (is (nil? (repl/event {:event (UUID/randomUUID)}))) - (repl/add-consumer {:callback (fn [_ event] (swap! events conj event))}) - (repl/log {:arguments [1 2 3] - :mdc {"a" "1"} - :level level - :logger (:root-logger framework) - :message "Hello World"}) + (is (nil? (repl/event :event (UUID/randomUUID)))) + (repl/add-consumer :callback (fn [_ event] (swap! events conj event))) + (repl/log :arguments [1 2 3] + :mdc {"a" "1"} + :level level + :logger (:root-logger framework) + :message "Hello World") (is (= 1 (count @events))) (let [event (first @events)] - (is (= event (repl/event {:event (:id event)}))))))) + (is (= event (repl/event :event (:id event)))))))) (deftest test-events (with-each-framework [framework (frameworks)] (let [level (-> framework :levels last :name)] (repl/add-appender) - (repl/log {:level level :message "Hello World"}) - (repl/log {:level level :message "Hello Moon"}) - (let [events (repl/events {:pattern ".*World"})] + (repl/log :level level :message "Hello World") + (repl/log :level level :message "Hello Moon") + (let [events (repl/events :pattern ".*World")] (is (= 1 (count events))) (is (= "Hello World" (:message (first events)))))))) @@ -88,19 +88,19 @@ (testing "default framework" (is (map? (repl/framework)))) (testing "unsupported framework" - (is (nil? (repl/framework {:framework "unknown"}))))) + (is (nil? (repl/framework :framework "unknown"))))) (deftest test-log (with-each-framework [framework (frameworks)] (let [events (atom []) level (-> framework :levels last :name)] (repl/add-appender) - (repl/add-consumer {:callback (fn [_ event] (swap! events conj event))}) - (repl/log {:arguments [1 2 3] - :mdc {"a" "1"} - :level level - :logger (:root-logger framework) - :message "Hello World"}) + (repl/add-consumer :callback (fn [_ event] (swap! events conj event))) + (repl/log :arguments [1 2 3] + :mdc {"a" "1"} + :level level + :logger (:root-logger framework) + :message "Hello World") (is (= 1 (count @events))) (let [event (first @events)] (is (= [1 2 3] (:arguments event))) diff --git a/test/clj/cider/log/specs.clj b/test/clj/cider/log/specs.clj index bb449f8c5..c0b8a8669 100644 --- a/test/clj/cider/log/specs.clj +++ b/test/clj/cider/log/specs.clj @@ -288,64 +288,64 @@ (s/nilable (s/and nat-int? #(<= 0 % 100)))) (s/fdef repl/add-appender - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework - :cider.log.repl.option/appender - :cider.log.repl.option/filters - :cider.log.repl.option/logger - :cider.log.repl.option/size - :cider.log.repl.option/threshold])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender + :cider.log.repl.option/filters + :cider.log.repl.option/logger + :cider.log.repl.option/size + :cider.log.repl.option/threshold]) :ret :cider.log/appender) (s/fdef repl/add-consumer - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender - :cider.log.repl.option/callback - :cider.log.repl.option/consumer - :cider.log.repl.option/filters - :cider.log.repl.option/framework])))) + :args (s/keys* :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/callback + :cider.log.repl.option/consumer + :cider.log.repl.option/filters + :cider.log.repl.option/framework]) :ret :cider.log/framework) (s/fdef repl/appender - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework - :cider.log.repl.option/appender])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender]) :ret (s/nilable :cider.log/appender)) (s/fdef repl/appenders - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework]) :ret (s/coll-of :cider.log/appender)) (s/fdef repl/clear-appender - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework - :cider.log.repl.option/appender])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender]) :ret :cider.log/framework) (s/fdef repl/event - :args (s/cat :options (s/? (s/nilable (s/keys :req-un [:cider.log.repl.option/event] - :opt-un [:cider.log.repl.option/appender - :cider.log.repl.option/framework])))) + :args (s/keys* :req-un [:cider.log.repl.option/event] + :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/framework]) :ret :cider.log/framework) (s/fdef repl/events - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender - :cider.log.repl.option/exceptions - :cider.log.repl.option/framework - :cider.log.repl.option/loggers - :cider.log.repl.option/pattern - :cider.log.repl.option/threads])))) + :args (s/keys* :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/exceptions + :cider.log.repl.option/framework + :cider.log.repl.option/loggers + :cider.log.repl.option/pattern + :cider.log.repl.option/threads]) :ret :cider.log/framework) (s/fdef repl/framework - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework]) :ret :cider.log/framework) (s/fdef repl/remove-appender - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework - :cider.log.repl.option/appender])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender]) :ret :cider.log/framework) (s/fdef repl/remove-consumer - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender - :cider.log.repl.option/consumer - :cider.log.repl.option/framework])))) + :args (s/keys* :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/consumer + :cider.log.repl.option/framework]) :ret :cider.log/framework) (s/fdef repl/set-appender! @@ -357,22 +357,22 @@ :ret map?) (s/fdef repl/shutdown - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework]) :ret :cider.log/framework) (s/fdef repl/update-appender - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/framework - :cider.log.repl.option/appender - :cider.log.repl.option/filters - :cider.log.repl.option/logger - :cider.log.repl.option/size - :cider.log.repl.option/threshold])))) + :args (s/keys* :opt-un [:cider.log.repl.option/framework + :cider.log.repl.option/appender + :cider.log.repl.option/filters + :cider.log.repl.option/logger + :cider.log.repl.option/size + :cider.log.repl.option/threshold]) :ret :cider.log/framework) (s/fdef repl/update-consumer - :args (s/cat :options (s/? (s/nilable (s/keys :opt-un [:cider.log.repl.option/appender - :cider.log.repl.option/callback - :cider.log.repl.option/consumer - :cider.log.repl.option/filters - :cider.log.repl.option/framework])))) + :args (s/keys* :opt-un [:cider.log.repl.option/appender + :cider.log.repl.option/callback + :cider.log.repl.option/consumer + :cider.log.repl.option/filters + :cider.log.repl.option/framework]) :ret :cider.log/framework)