Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CIDER log middleware #773

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions doc/modules/ROOT/pages/nrepl-api/supplied_middleware.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
| `inspect-(start/refresh/pop/push/reset/get-path)`
| Inspect a Clojure expression.

| `wrap-log`
| 0.30.1
| No
| `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`
| -
| Yes
Expand Down
2 changes: 2 additions & 0 deletions doc/modules/ROOT/pages/usage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -153,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
Expand Down
12 changes: 10 additions & 2 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,23 @@

:profiles {:provided {:dependencies [[org.clojure/clojure "1.10.3"]
[org.clojure/clojurescript "1.11.4" :scope "provided"]
;; 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"]]
Expand All @@ -102,9 +108,11 @@
: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"]
[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
Expand Down
139 changes: 139 additions & 0 deletions src/cider/log/appender.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
(ns cider.log.appender
"A log appender that captures log events in memory."
{:author "r0man"}
(:require [cider.log.event :as event]))
r0man marked this conversation as resolved.
Show resolved Hide resolved

(def ^:private default-size
"The default number of events captured by an appender."
100000)

(def ^:private default-threshold
"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?
"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))))))
73 changes: 73 additions & 0 deletions src/cider/log/event.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
(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]))

(defn- exception-name
"Return the `exception` class name."
[^Throwable exception]
(some-> exception .getClass .getName))

(defn exception-frequencies
"Return the exception name frequencies of `events`."
[events]
(frequencies (keep #(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)))
Loading