Skip to content
Snippets Groups Projects
Unverified Commit 5d106f30 authored by Tim Macdonald's avatar Tim Macdonald Committed by GitHub
Browse files

Log a warning when extraneous parameters are passed into a request (#28032)


* Log a warning when extraneous parameters are passed into a request

* Massive Metabase logging performance improvement: use configured log levels in logs API endpoint

---------

Co-authored-by: default avatarCam Saul <github@camsaul.com>
parent 78ce7993
No related branches found
No related tags found
No related merge requests found
...@@ -87,6 +87,7 @@ ...@@ -87,6 +87,7 @@
`(require [(symbol (str "metabase.models." (quote ~model-sym))) :as (quote ~model-sym)])) `(require [(symbol (str "metabase.models." (quote ~model-sym))) :as (quote ~model-sym)]))
(defmacro with-permissions (defmacro with-permissions
"Execute the body with the given permissions."
[permissions & body] [permissions & body]
`(binding [api/*current-user-permissions-set* (delay ~permissions)] `(binding [api/*current-user-permissions-set* (delay ~permissions)]
~@body)) ~@body))
......
(ns user (ns user
(:require (:require
[hawk.assert-exprs] [hawk.assert-exprs]
[metabase.bootstrap]
[metabase.test-runner.assert-exprs])) [metabase.test-runner.assert-exprs]))
;; make sure stuff like `schema=` and what not are loaded (comment metabase.bootstrap/keep-me
(comment hawk.assert-exprs/keep-me ;; make sure stuff like `schema=` and what not are loaded
hawk.assert-exprs/keep-me
metabase.test-runner.assert-exprs/keep-me) metabase.test-runner.assert-exprs/keep-me)
(defn dev (defn dev
"Load and switch to the 'dev' namespace." "Load and switch to the 'dev' namespace."
[] []
(require 'metabase.bootstrap)
(require 'dev) (require 'dev)
(in-ns 'dev) (in-ns 'dev)
:loaded) :loaded)
(ns metabase.api.common (ns metabase.api.common
"Dynamic variables and utility functions/macros for writing API functions." "Dynamic variables and utility functions/macros for writing API functions."
(:require (:require
[clojure.set :as set]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.string :as str] [clojure.string :as str]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
...@@ -16,6 +17,7 @@ ...@@ -16,6 +17,7 @@
route-fn-name route-fn-name
validate-params validate-params
wrap-response-if-needed]] wrap-response-if-needed]]
[metabase.config :as config]
[metabase.models.interface :as mi] [metabase.models.interface :as mi]
[metabase.util :as u] [metabase.util :as u]
[metabase.util.i18n :as i18n :refer [deferred-tru tru]] [metabase.util.i18n :as i18n :refer [deferred-tru tru]]
...@@ -261,16 +263,50 @@ ...@@ -261,16 +263,50 @@
(ns-name *ns*) fn-name))) (ns-name *ns*) fn-name)))
(assoc parsed :fn-name fn-name, :route route, :docstr docstr)))) (assoc parsed :fn-name fn-name, :route route, :docstr docstr))))
(defn validate-param-values
"Log a warning if the request body contains any parameters not included in `expected-params` (which is presumably
populated by the defendpoint schema)"
[{route :compojure/route body :body} expected-params]
(when (and (not config/is-prod?)
(map? body))
(let [extraneous-params (set/difference (set (keys body))
(set expected-params))]
(when (seq extraneous-params)
(log/warnf "Unexpected parameters at %s: %s\nPlease add them to the schema or remove them from the API client"
route (vec extraneous-params))))))
(defn method-symbol->keyword
"Convert Compojure-style HTTP method symbols (PUT, POST, etc.) to the keywords used internally by
Compojure (:put, :post, ...)"
[method-symbol]
(-> method-symbol
name
u/lower-case-en
keyword))
(defmacro defendpoint* (defmacro defendpoint*
"Impl macro for [[defendpoint]]; don't use this directly." "Impl macro for [[defendpoint]]; don't use this directly."
[{:keys [method route fn-name docstr args body]}] [{:keys [method route fn-name docstr args body arg->schema]}]
{:pre [(or (string? route) (vector? route))]} {:pre [(or (string? route) (vector? route))]}
`(def ~(vary-meta fn-name (let [method-kw (method-symbol->keyword method)
assoc allowed-params (keys arg->schema)
:doc docstr prep-route #'compojure/prepare-route]
:is-endpoint? true) `(def ~(vary-meta fn-name
(~(symbol "compojure.core" (name method)) ~route ~args assoc
~@body))) :doc docstr
:is-endpoint? true)
;; The next form is a copy of `compojure/compile-route`, with the sole addition of the call to
;; `validate-param-values`. This is because to validate the request body we need to intercept the request
;; before the destructuring takes place. I.e., we need to validate the value of `(:body request#)`, and that's
;; not available if we called `compile-route` ourselves.
(compojure/make-route
~method-kw
~(prep-route route)
(fn [request#]
(validate-param-values request# (quote ~allowed-params))
(compojure/let-request [~args request#]
~@body))))))
;; TODO - several of the things `defendpoint` does could and should just be done by custom Ring middleware instead ;; TODO - several of the things `defendpoint` does could and should just be done by custom Ring middleware instead
;; e.g. `auto-parse` ;; e.g. `auto-parse`
......
...@@ -6,13 +6,16 @@ ...@@ -6,13 +6,16 @@
[amalloy.ring-buffer :refer [ring-buffer]] [amalloy.ring-buffer :refer [ring-buffer]]
[clj-time.coerce :as time.coerce] [clj-time.coerce :as time.coerce]
[clj-time.format :as time.format] [clj-time.format :as time.format]
[metabase.config :as config]) [metabase.config :as config]
[metabase.plugins.classloader :as classloader])
(:import (:import
(org.apache.commons.lang3.exception ExceptionUtils) (org.apache.commons.lang3.exception ExceptionUtils)
(org.apache.logging.log4j LogManager) (org.apache.logging.log4j LogManager)
(org.apache.logging.log4j.core Appender LogEvent LoggerContext) (org.apache.logging.log4j.core Appender LogEvent LoggerContext)
(org.apache.logging.log4j.core.config LoggerConfig))) (org.apache.logging.log4j.core.config LoggerConfig)))
(set! *warn-on-reflection* true)
(def ^:private ^:const max-log-entries 2500) (def ^:private ^:const max-log-entries 2500)
(defonce ^:private messages* (atom (ring-buffer max-log-entries))) (defonce ^:private messages* (atom (ring-buffer max-log-entries)))
...@@ -47,13 +50,11 @@ ...@@ -47,13 +50,11 @@
(when-not *compile-files* (when-not *compile-files*
(when-not @has-added-appender? (when-not @has-added-appender?
(reset! has-added-appender? true) (reset! has-added-appender? true)
(let [^LoggerContext ctx (LogManager/getContext false) (let [^LoggerContext ctx (LogManager/getContext (classloader/the-classloader) false)
config (.getConfiguration ctx) config (.getConfiguration ctx)
appender (metabase-appender) appender (metabase-appender)]
^org.apache.logging.log4j.Level level nil
^org.apache.logging.log4j.core.Filter filter nil]
(.start appender) (.start appender)
(.addAppender config appender) (.addAppender config appender)
(doseq [[_ ^LoggerConfig logger-config] (.getLoggers config)] (doseq [[_ ^LoggerConfig logger-config] (.getLoggers config)]
(.addAppender logger-config appender level filter)) (.addAppender logger-config appender (.getLevel logger-config) (.getFilter logger-config))
(.updateLoggers ctx)))) (.updateLoggers ctx)))))
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
(:require (:require
[cheshire.core :as json] [cheshire.core :as json]
[clj-http.client :as http] [clj-http.client :as http]
[clojure.string :as str]
[clojure.test :refer :all] [clojure.test :refer :all]
[compojure.core :refer [POST]] [compojure.core :refer [POST]]
[malli.util :as mut] [malli.util :as mut]
...@@ -9,7 +10,9 @@ ...@@ -9,7 +10,9 @@
[metabase.api.common :as api] [metabase.api.common :as api]
[metabase.api.common.internal :as internal] [metabase.api.common.internal :as internal]
[metabase.config :as config] [metabase.config :as config]
[metabase.logger :as mb.logger]
[metabase.server.middleware.exceptions :as mw.exceptions] [metabase.server.middleware.exceptions :as mw.exceptions]
[metabase.test :as mt]
[metabase.util :as u] [metabase.util :as u]
[ring.adapter.jetty :as jetty])) [ring.adapter.jetty :as jetty]))
...@@ -68,59 +71,64 @@ ...@@ -68,59 +71,64 @@
(deftest defendpoint-test (deftest defendpoint-test
(let [server (jetty/run-jetty (json-mw (exception-mw #'routes)) {:port 0 :join? false}) (let [server (jetty/run-jetty (json-mw (exception-mw #'routes)) {:port 0 :join? false})
port (.. server getURI getPort) port (.. server getURI getPort)
post! (fn [route body] post! (fn [route body]
(http/post (str "http://localhost:" port route) (http/post (str "http://localhost:" port route)
{:throw-exceptions false {:throw-exceptions false
:accept :json :accept :json
:as :json :as :json
:coerce :always :coerce :always
:body (json/generate-string body)}))] :body (json/generate-string body)}))]
(is (= {:a 1 :b 2} (:body (post! "/post/any" {:a 1 :b 2})))) (is (= {:a 1 :b 2} (:body (post! "/post/any" {:a 1 :b 2}))))
(is (= {:id 1} (:body (post! "/post/id-int" {:id 1})))) (is (= {:id 1} (:body (post! "/post/id-int" {:id 1}))))
(is (= {:errors {:id "integer"}, (is (= {:errors {:id "integer"},
:specific-errors {:id ["should be an int"]}} :specific-errors {:id ["should be an int"]}}
(:body (post! "/post/id-int" {:id "1"})))) (:body (post! "/post/id-int" {:id "1"}))))
(is (= {:id "myid" (mt/with-log-level [metabase.api.common :warn]
:tags ["abc"] (is (= {:id "myid"
:address {:street "abc" :city "sdasd" :zip 2999 :lonlat [0.0 0.0]}} :tags ["abc"]
(:body (post! "/post/test-address" :address {:street "abc" :city "sdasd" :zip 2999 :lonlat [0.0 0.0]}}
{:id "myid" (:body (post! "/post/test-address"
:tags ["abc"] {:id "myid"
:address {:street "abc" :tags ["abc"]
:city "sdasd" :address {:street "abc"
:zip 2999 :city "sdasd"
:lonlat [0.0 0.0]}})))) :zip 2999
:lonlat [0.0 0.0]}}))))
(is (some (fn [{message :msg, :as entry}]
(when (str/includes? (str message) "Unexpected parameters")
entry))
(mb.logger/messages))))
(is (= {:errors (is (= {:errors
{:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"}, {:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"},
:specific-errors {:address {:id ["missing required key"], :specific-errors {:address {:id ["missing required key"],
:tags ["missing required key"], :tags ["missing required key"],
:address ["missing required key"]}}} :address ["missing required key"]}}}
(:body (post! "/post/test-address" {:x "1"})))) (:body (post! "/post/test-address" {:x "1"}))))
(is (= {:errors (is (= {:errors
{:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"}, {:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>}>}"},
:specific-errors {:address :specific-errors {:address
{:id ["should be a string"] {:id ["should be a string"]
:tags ["invalid type"] :tags ["invalid type"]
:address {:street ["missing required key"] :address {:street ["missing required key"]
:zip ["should be an int"]}}}} :zip ["should be an int"]}}}}
(:body (post! "/post/test-address" {:id 1288 (:body (post! "/post/test-address" {:id 1288
:tags "a,b,c" :tags "a,b,c"
:address {:streeqt "abc" :address {:streeqt "abc"
:city "sdasd" :city "sdasd"
:zip "12342" :zip "12342"
:lonlat [0.0 0.0]}})))) :lonlat [0.0 0.0]}}))))
(is (= {:errors (is (= {:errors
{:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>} with no other keys>} with no other keys"}, {:address "map (titled: ‘Address’) where {:id -> <string>, :tags -> <vector of string>, :address -> <map where {:street -> <string>, :city -> <string>, :zip -> <integer>, :lonlat -> <vector with exactly 2 items of type: double, double>} with no other keys>} with no other keys"},
:specific-errors {:address :specific-errors {:address
{:address ["missing required key"], {:address ["missing required key"],
:a ["disallowed key"], :a ["disallowed key"],
:b ["disallowed key"]}}} :b ["disallowed key"]}}}
(:body (post! "/post/closed-test-address" {:id "1" :tags [] :a 1 :b 2})))))) (:body (post! "/post/closed-test-address" {:id "1" :tags [] :a 1 :b 2}))))))
(deftest route-fn-name-test (deftest route-fn-name-test
......
(ns metabase.api.common-test (ns metabase.api.common-test
(:require (:require
[clojure.test :refer :all] [clojure.test :refer :all]
[hawk.assert-exprs.approximately-equal :as hawk.approx]
[metabase.api.common :as api] [metabase.api.common :as api]
[metabase.api.common.internal :as api.internal] [metabase.api.common.internal :as api.internal]
[metabase.server.middleware.exceptions :as mw.exceptions] [metabase.server.middleware.exceptions :as mw.exceptions]
[metabase.server.middleware.misc :as mw.misc] [metabase.server.middleware.misc :as mw.misc]
[metabase.server.middleware.security :as mw.security])) [metabase.server.middleware.security :as mw.security]
[methodical.core :as methodical]))
;;; TESTS FOR CHECK (ETC) ;;; TESTS FOR CHECK (ETC)
...@@ -92,18 +94,27 @@ ...@@ -92,18 +94,27 @@
;; compare easily. ;; compare easily.
(update-in [:route 2] str))))) (update-in [:route 2] str)))))
(methodical/defmethod hawk.approx/=?-diff [java.util.regex.Pattern clojure.lang.Symbol]
[expected-re sym]
(hawk.approx/=?-diff expected-re (name sym)))
(deftest ^:parallel defendpoint-test (deftest ^:parallel defendpoint-test
;; replace regex `#"[0-9]+"` with str `"#[0-9]+" so expectations doesn't barf ;; replace regex `#"[0-9]+"` with str `"#[0-9]+" so expectations doesn't barf
(binding [api.internal/*auto-parse-types* (update-in api.internal/*auto-parse-types* [:int :route-param-regex] (partial str "#"))] (binding [api.internal/*auto-parse-types* (update-in api.internal/*auto-parse-types* [:int :route-param-regex] (partial str "#"))]
(is (= '(def GET_:id (is (=? '(def
(compojure.core/GET GET_:id
["/:id" :id "#[0-9]+"] (compojure.core/make-route
[id] :get
(metabase.api.common.internal/auto-parse [id] {:source "/:id", :re #"/(#[0-9]+)", :keys [:id], :absolute? false}
(metabase.api.common.internal/validate-param 'id id metabase.util.schema/IntGreaterThanZero) (clojure.core/fn
(metabase.api.common.internal/wrap-response-if-needed [#"request__\d+__auto__"]
(do (metabase.api.common/validate-param-values #"request__\d+__auto__" '(id))
(select-one Card :id id)))))) (compojure.core/let-request
(macroexpand '(metabase.api.common/defendpoint-schema compojure.core/GET "/:id" [id] [[id] #"request__\d+__auto__"]
{id metabase.util.schema/IntGreaterThanZero} (metabase.api.common.internal/auto-parse
(select-one Card :id id))))))) [id]
(metabase.api.common.internal/validate-param 'id id metabase.util.schema/IntGreaterThanZero)
(metabase.api.common.internal/wrap-response-if-needed (do (select-one Card :id id))))))))
(macroexpand '(metabase.api.common/defendpoint-schema compojure.core/GET "/:id" [id]
{id metabase.util.schema/IntGreaterThanZero}
(select-one Card :id id)))))))
...@@ -829,7 +829,7 @@ ...@@ -829,7 +829,7 @@
{:field_order :database}) {:field_order :database})
:fields :fields
(map :name))))) (map :name)))))
(testing "Cane we set custom field ordering?" (testing "Can we set custom field ordering?"
(let [custom-field-order [(mt/id :venues :price) (mt/id :venues :longitude) (mt/id :venues :id) (let [custom-field-order [(mt/id :venues :price) (mt/id :venues :longitude) (mt/id :venues :id)
(mt/id :venues :category_id) (mt/id :venues :name) (mt/id :venues :latitude)]] (mt/id :venues :category_id) (mt/id :venues :name) (mt/id :venues :latitude)]]
(mt/user-http-request :crowberto :put 200 (format "table/%s/fields/order" (mt/id :venues)) (mt/user-http-request :crowberto :put 200 (format "table/%s/fields/order" (mt/id :venues))
......
(ns metabase.logger-test (ns metabase.logger-test
(:require (:require
[clojure.string :as str]
[clojure.test :refer :all] [clojure.test :refer :all]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[clojure.tools.logging.impl :as log.impl] [clojure.tools.logging.impl :as log.impl]
[metabase.logger :as mb.logger] [metabase.logger :as mb.logger]
[metabase.test :as mt])) [metabase.test :as mt])
(:import
(org.apache.logging.log4j.core Logger)))
(set! *warn-on-reflection* true)
(defn logger
(^Logger []
(logger 'metabase.logger-test))
(^Logger [ns-symb]
(log.impl/get-logger log/*logger-factory* ns-symb)))
(deftest added-appender-tests (deftest added-appender-tests
(testing "appender is added to the logger" (testing "appender is added to the logger"
(let [logger (log.impl/get-logger log/*logger-factory* *ns*)] (is (contains? (.getAppenders (logger)) "metabase-appender")
(is (contains? (.getAppenders logger) "metabase-appender") "Logger does not contain `metabase-appender` logger"))
"Logger does not contain `metabase-appender` logger")))
(testing "logging adds to in-memory ringbuffer" (testing "logging adds to in-memory ringbuffer"
(mt/with-log-level :warn (mt/with-log-level :debug
(let [before (count (mb.logger/messages))] (log/debug "testing in-memory logger")
(log/warn "testing in-memory logger") (is (some (fn [{message :msg, :as entry}]
(let [after (count (mb.logger/messages))] (when (str/includes? (str message) "testing in-memory logger")
;; it either increases (could have many logs from other tests) or it is the max capacity of the ring buffer entry))
(is (or (> after before) (mb.logger/messages))
(= before (var-get #'mb.logger/max-log-entries))) "In memory ring buffer did not receive log message")))
"In memory ring buffer did not receive log message")))))
(testing "set isAdditive = false if parent logger is root to prevent logging to console (#26468)" (testing "set isAdditive = false if parent logger is root to prevent logging to console (#26468)"
(testing "make sure it's true to starts with" (testing "make sure it's true to starts with"
(is (true? (.isAdditive (log.impl/get-logger log/*logger-factory* 'metabase))))) (is (.isAdditive (logger 'metabase))))
(testing "set to false if parent logger is root" (testing "set to false if parent logger is root"
(mt/with-log-level :warn (mt/with-log-level :warn
(is (false? (.isAdditive (log.impl/get-logger log/*logger-factory* 'metabase)))))) (is (not (.isAdditive (logger 'metabase))))))
(testing "still true if the parent logger is not root" (testing "still true if the parent logger is not root"
(mt/with-log-level [metabase.logger :warn] (mt/with-log-level [metabase.logger :warn]
(is (true? (.isAdditive (log.impl/get-logger log/*logger-factory* 'metabase.logger)))))))) (is (.isAdditive (logger 'metabase.logger)))))))
(deftest logger-test (deftest ^:parallel logger-test
(testing "Using log4j2 logger" (testing "Using log4j2 logger"
(is (= (log.impl/name log/*logger-factory*) (is (= "org.apache.logging.log4j"
"org.apache.logging.log4j") (log.impl/name log/*logger-factory*))
"Not using log4j2 logger factory. This could add two orders of magnitude of time to logging calls"))) "Not using log4j2 logger factory. This could add two orders of magnitude of time to logging calls")))
(deftest logger-respect-configured-log-level-test
(testing "The appender that we programmatically added should respect the log levels in the config file"
;; whether we're in the REPL or in test mode this should not show up
(log/debug "THIS SHOULD NOT SHOW UP")
(is (not (some (fn [{message :msg, :as entry}]
(when (str/includes? (str message) "THIS SHOULD NOT SHOW UP")
entry))
(mb.logger/messages))))))
...@@ -5,11 +5,15 @@ ...@@ -5,11 +5,15 @@
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[clojure.tools.logging.impl :as log.impl] [clojure.tools.logging.impl :as log.impl]
[hawk.parallel] [hawk.parallel]
[metabase.plugins.classloader :as classloader]
[potemkin :as p] [potemkin :as p]
[schema.core :as s]) [schema.core :as s])
(:import [org.apache.logging.log4j Level LogManager] (:import
[org.apache.logging.log4j.core Appender LifeCycle LogEvent Logger LoggerContext] (org.apache.logging.log4j Level LogManager)
[org.apache.logging.log4j.core.config Configuration LoggerConfig])) (org.apache.logging.log4j.core Appender LifeCycle LogEvent Logger LoggerContext)
(org.apache.logging.log4j.core.config Configuration LoggerConfig)))
(set! *warn-on-reflection* true)
(def ^:private keyword->Level (def ^:private keyword->Level
{:off Level/OFF {:off Level/OFF
...@@ -45,7 +49,7 @@ ...@@ -45,7 +49,7 @@
(name a-namespace))) (name a-namespace)))
(defn- logger-context ^LoggerContext [] (defn- logger-context ^LoggerContext []
(LogManager/getContext false)) (LogManager/getContext (classloader/the-classloader) false))
(defn- configuration ^Configuration [] (defn- configuration ^Configuration []
(.getConfiguration (logger-context))) (.getConfiguration (logger-context)))
...@@ -90,6 +94,9 @@ ...@@ -90,6 +94,9 @@
(into-array org.apache.logging.log4j.core.config.Property (.getPropertyList parent-logger)) (into-array org.apache.logging.log4j.core.config.Property (.getPropertyList parent-logger))
(configuration) (configuration)
(.getFilter parent-logger))] (.getFilter parent-logger))]
;; copy the appenders from the parent logger, e.g. the [[metabase.logger/metabase-appender]]
(doseq [[_name ^Appender appender] (.getAppenders parent-logger)]
(.addAppender new-logger appender (.getLevel new-logger) (.getFilter new-logger)))
(.addLogger (configuration) (logger-name a-namespace) new-logger) (.addLogger (configuration) (logger-name a-namespace) new-logger)
(.updateLoggers (logger-context)) (.updateLoggers (logger-context))
#_{:clj-kondo/ignore [:discouraged-var]} #_{:clj-kondo/ignore [:discouraged-var]}
...@@ -110,6 +117,14 @@ ...@@ -110,6 +117,14 @@
(let [logger (exact-ns-logger a-namespace) (let [logger (exact-ns-logger a-namespace)
new-level (->Level new-level)] new-level (->Level new-level)]
(.setLevel logger new-level) (.setLevel logger new-level)
;; it seems like changing the level doesn't update the level for the appenders
;; e.g. [[metabase.logger/metabase-appender]], so if we want the new level to be reflected there the only way I can
;; figure out to make it work is to remove the appender and then add it back with the updated level. See JavaDoc
;; https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/config/LoggerConfig.html
;; for more info. There's probably a better way to do this, but I don't know what it is. -- Cam
(doseq [[^String appender-name ^Appender appender] (.getAppenders logger)]
(.removeAppender logger appender-name)
(.addAppender logger appender new-level (.getFilter logger)))
(.updateLoggers (logger-context))))) (.updateLoggers (logger-context)))))
(defn do-with-log-level [a-namespace level thunk] (defn do-with-log-level [a-namespace level thunk]
......
(ns metabase.test-runner.assert-exprs (ns metabase.test-runner.assert-exprs
"Custom implementations of [[clojure.test/is]] expressions (i.e., implementations of [[clojure.test/assert-expr]]). "Custom implementations of a few [[clojure.test/is]] expressions (i.e., implementations of [[clojure.test/assert-expr]]):
`re=`, `schema=`, `query=`, `sql=`, `=?`, and more." `query=` and `sql=`.
Other expressions (`re=`, `schema=`, `=?`, and so forth) are implemented with the Hawk test-runner."
(:require (:require
[clojure.data :as data] [clojure.data :as data]
[clojure.test :as t] [clojure.test :as t]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment