This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-06-29
Channels
- # announcements (44)
- # architecture (12)
- # babashka (45)
- # beginners (56)
- # calva (16)
- # cider (34)
- # clj-kondo (6)
- # clojure (47)
- # clojure-austin (2)
- # clojure-brasil (3)
- # clojure-europe (39)
- # clojure-germany (2)
- # clojure-nl (1)
- # clojure-norway (39)
- # clojurescript (7)
- # cursive (1)
- # datahike (2)
- # datomic (28)
- # emacs (8)
- # gratitude (3)
- # humbleui (4)
- # hyperfiddle (45)
- # kaocha (1)
- # lsp (94)
- # nbb (2)
- # off-topic (29)
- # practicalli (8)
- # releases (2)
- # shadow-cljs (6)
- # squint (17)
- # tools-deps (12)
- # vim (11)
Does anyone have a nice way to handle reloading when using Pedestal+Reitit? Ideally I’d like to change a handler function, evaluate it (or even just re-eval the namespace), refresh the browser and see the changes. Currently I’m using Integrant and am just resetting it at the REPL which works fine but am hoping there’s a cleaner step.
I might be misunderstanding, but can't you just use this "trick": https://github.com/pedestal/pedestal/blob/a51d99ea698dab20d46aff5ec837c8076079fee9/samples/hello-world/src/hello_world/server.clj#L30-L32
I don’t think it works in the case where Reitit is used instead of Pedestal’s own routes. E.g. here’s a subset of my setup (using integrant):
(defmethod ig/prep-key :http/service
[_ {:keys [port allowed-origins environment router]}]
{::server/type :jetty
::server/port port
::server/join? false
::server/routes []
::server/allowed-origins allowed-origins
:router router})
(defmethod ig/init-key :http/service
[_ svc-map]
(-> svc-map
(server/default-interceptors)
(pedestal/replace-last-interceptor (:router svc-map))
(server/create-server)
(server/start)))
(defmethod ig/init-key :router/main
[_ services]
(pedestal/routing-interceptor
(http/router routes)
(ring/create-default-handler)
{:interceptors [(services-interceptor services)]}))
So here Pedestal’s ::server/routes
is empty and I use Reitit’s interceptors to do the routing as per the docs. (http/router routes)
is where the router is built and routes
is just a map.I seem to recall a Ring trick of specifying any handler function not by its function-symbol, but by a var-quoted symbol. I guess the program ran a tiny bit slower, but the places that held that symbol in closure didn't prevent reloaded-redefined code from taking effect.
Thanks for the link, I checked that one out earlier but it also doesn’t work in this context (it works with Ring however). WIth pedestal I have to call routing-interceptor
to make an interceptor out of the router, this requires a concrete Router
impl and won’t accept a function as its first argument.
(def routes
[["/" {:get {:handler get-home}}]
["/admin"
{:roles #{:admin}}
["" {:get {:handler get-admin}}]
["/login" {:get {:handler get-admin-login}
:post {:handler post-admin-login}}]]
["/api"
{:interceptors [(muuntaja-interceptors/format-interceptor)]}
["/csp-violation" {:post {:handler csp-violation}}]]
["/assets/*" (ring/create-resource-handler)]])
(let [dev? false
http-router (fn [routes] (println "Router") routes)
routes (if dev?
(fn [] (println "Reloading routes...") (#'routes))
(constantly (#'routes)))]
(http-router (routes))
(http-router (routes))
(http-router (routes)))
;; dev? false:
Router
Router
Router
[["/" :home]]
;; dev? true
Reloading routes...
Router
Reloading routes...
Router
Reloading routes...
Router
[["/" :home]]
You legend! That did it, thanks for your help!
You probably want to pass in dev?
as part of the integrant config or something. I'm not too familiar with integrant. It's also helpful for routes to be a function (in case you want to pass in args to modify or pass additional state while building the routes)
Yeah I already have a dev?
key in the config edn it reads in so am just using that
Hi. Sorry I think I’m being slow here but how did you get this working with reitit and pedestal? I’m using the same ‘replace last interceptor’ approach to use reitit with pedestal. Unlike pedestal, reitit appears to not handle a function as a parameter for building the routes table, so I get a Don't know how to create ISeq from: clojure.core$constantly$fn__5756
exception if I pass in a fn and not a routing table.
I too would love hot reload, but end up stopping and restarting my integrant system when I change routes and interceptors.
Not sure where the problem is, so let me break this down into small steps (for other readers benefit). The trick employed here is not specific to reitit or pedestal. (1) We have some function that is dependent on a sequence
(defn router [routes]
[:router routes])
(2) We can define a static list to pass in:
(def routes
[["/" :home]])
(router routes)
;; => [:router [["/" :home]]]
(3) But we can replace this with a function (and call that function directly). We control the calling code, so this makes no difference for the underlying router. (We'll also add some I/O to show when this gets called)
(defn routes []
(println "Loading newest routes")
[["/" :home]])
(router (routes))
;; Loading newest routes
;; => [:router [["/" :home]]]
(4) We can wrap this function in another function (that just calls the original routes fn).
(defn custom-routes []
;; Calls original routes fn
(routes))
(router (custom-routes))
;; Loading newest routes
;; => [:router [["/" :home]]]
(5) Why do we want this? Well, we can define multiple versions:
(defn dev-routes []
(routes))
(repeatedly 2 #(router (dev-routes)))
;; => Loading newest routes
;; ([:router [["/" :home]]]
;; Loading newest routes
;; [:router [["/" :home]]])
(6) Now, the trick related to var references:
(type routes)
;; => dev$routes
(type #'routes)
;; => clojure.lang.Var
Normally you would need to deref to get the original function:
(type (deref #'routes))
;; => dev$routes
But Clojure let's call the var directly (and hide the deref from the caller):
(routes)
;; => [["/" :home]]
(#'routes)
;; => [["/" :home]]
(7) So, now we can define a dev version:
(defn dev-routes []
(#'routes))
(repeatedly 2 #(router (dev-routes)))
;; => Loading newest routes
;; ([:router [["/" :home]]]
;; Loading newest routes
;; [:router [["/" :home]]])
(8) And the second trick is related to constantly
which let's us return a function that will cache the inner body:
(def prod-routes
(constantly (#'routes)))
;; Loading newest routes
(prod-routes)
;; => [["/" :home]]
(repeatedly 2 #(router (prod-routes)))
;; => ([:router [["/" :home]]]
;; [:router [["/" :home]]])
Note the use of def
instead of defn
and that the I/O happens when we create prod-routes
instead of when we call (prod-routes)
(9) Putting it all together, we get something like:
(let [dev? false
routes (if dev? dev-routes prod-routes)]
(repeatedly 2 #(router (routes))))
;; => ([:router [["/" :home]]]
;; [:router [["/" :home]]])
(let [dev? true
routes (if dev? dev-routes prod-routes)]
(repeatedly 2 #(router (routes))))
;; => Loading newest routes
;; ([:router [["/" :home]]]
;; Loading newest routes
;; [:router [["/" :home]]])
Does that help @U013CFKNP2R?
Dear @U05476190 - thank you very much for your explanation - that is very kind indeed. I think my problem is that the reitit http router is created in my integrant from the routes. The routes are reloaded, because there is a top-level def
, but of course, this doesn't affect the routing interceptor already set-up in integrant. The OP was asking about this with integrant - and it sounds as if they solved this. But it just isn't updating for me, because the reload isn't affecting the already built integrant system. I can see that I can fix this however, by setting something at the top-level of the namespace to stop and restart my integrant system on reload... but it sounded as if this might be possible without this step... but I can't see how that could be. Many thanks again for your thoughtful and comprehensive reply!
For the benefit of anyone reading this in the future, my mistake was of course to not also be using integrant.repl for namespace/system reloading. That, in combination with the above routes in development, and a custom command in Cursive, means that a double tap saves all files, and then resets the integrant system. See https://github.com/weavejester/integrant-repl and https://cognitect.com/blog/2013/06/04/clojure-workflow-reloaded and https://lambdaisland.com/blog/2018-02-09-reloading-woes
hey @U013CFKNP2R sorry to be bringing up an old thread, but were you able to solve this without reloading integrant?
Apologies I switched back to using pedestal for routing as I switched to using a different approach that didn't need lots of routes - now I have only one route /api and don't need to change it during a REPL session.
hi guys, i am trying to configure clojure/tools.logging messages i get in a CLI interface that i am working on for this project https://github.com/yogthos/migratus i get these kind of logs:
Jun 29, 2023 1:48:36 PM clojure.tools.logging$eval1053$fn__1056 invoke
INFO: Running up for [20230619094618]
Jun 29, 2023 1:48:36 PM clojure.tools.logging$eval1053$fn__1056 invoke
INFO: Up 20230619094618-create-user
first of all, i want to delete :
Jun 29, 2023 1:48:36 PM clojure.tools.logging$eval1053$fn__1056 invoke
Jun 29, 2023 1:48:36 PM clojure.tools.logging$eval1053$fn__1056 invoke
and then add more useful information.
Does anyone know how this can be done? Thankstools.logging
is just a common interface for logging. What you get in your logs depends on the actual implementation.
The log lines that you get seem to come from JUL with its default configuration. You can configure JUL explicitly to use the desired format (I would then also explicitly set JUL as the logging implementation in tools.logging
, to avoid any issues if the classpath changes) or you can change the logging implementation altogether.
for example using the Logback logging implentation, add these dependencies:
[ch.qos.logback/logback-classic "1.2.3" :exclusions [org.slf4j/slf4j-api]]
[org.slf4j/slf4j-api "1.7.36"]
[org.slf4j/jul-to-slf4j "1.7.36"]
[org.slf4j/jcl-over-slf4j "1.7.36"]
[org.slf4j/log4j-over-slf4j "1.7.36"]
to route all various Java types of logging over Logback. Then add resources/logback.xml, and for example using STDOUT appender:
<configuration scan="true" scanPeriod="10 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
now you can change the logging pattern in the encoder section of the config to change how the logs to console are formattedhi, thanks for the information. we are working to provide the CLI for migratus (Andrei is doing most of the work).
in our case, for the cli, adding a logging implementation is a no go so we should go for configuring JUL
do you know if tools.logging
allows for changing the implemntation at runtime ?
(I should probably check the source code 😄 )
For configuration JUL you can specify a logging.properties
file with formatting. It seems you can change logging factory at runtime by changing the dynamic var *`logger-factory*` (https://github.com/clojure/tools.logging/blob/master/src/main/clojure/clojure/tools/logging.clj#L326)?
for the formatter I think we will go the route or programatic configuration and implement a handler as a fn. Seems to be doable https://www.logicbig.com/tutorials/core-java-tutorial/logging/customizing-default-format.html
thank you guys for your time and solutions :)
Hello again guys.
i have successfully configured (programatic way) JUL to display the log messages the way i wanted.
Now i want to make use of clojure.tools.logging/`debug` and trace
,
but nothing is displayed, after setting the level to ALL for the handler (.setLevel java.util.logging.Level/ALL)
As i understand so far, clojure.tools.logging is just an interface and does not have a logging engine.
The logging engine that i am using now is java.util.logging, which has other levels of log ( https://docs.oracle.com/en/java/javase/13/docs/api/java.logging/java/util/logging/Level.html):
* ALL, CONFIG, FINE, FINER, FINEST, INFO, OFF, SEVERE, WARNING
I want to implement a --verbose
switch in the CLI app that i am working on, to set the log level.
Thanks.Just configuring the JUL itself might not be enough - you have to tell c.t.l that you want to use it. At least, you have to tell if there are other logging frameworks on the classpath since JUL is checked the last one: https://github.com/clojure/tools.logging/blob/master/src/main/clojure/clojure/tools/logging/impl.clj#L249-L253
Here is the code i am using:
(ns migratus-cli.tools-cli
(:require
[clojure.tools.logging :as log]
[clojure.core :as core])
(:import [java.time ZoneOffset]
[java.util.logging ConsoleHandler Logger LogRecord Level
Formatter SimpleFormatter]))
(defn simple-formatter
" Clojure bridge for java.util.logging.SimpleFormatter.
Can register a clojure fn as a logger formatter.
* format-fn - clojure fn that receives the record to send to logging. "
(^SimpleFormatter [format-fn]
(proxy [SimpleFormatter] []
(format [record]
(format-fn record)))))
(defn format-log-record
"Format jul logger record."
(^String [^LogRecord record]
(let [fmt "%5$s"
instant (.getInstant record)
date (-> instant (.atZone ZoneOffset/UTC))
level (.getLevel record)
src (.getSourceClassName record)
msg (.getMessage record)
thr (.getThrown record)
logger (.getLoggerName record)]
(core/format fmt date src logger level msg thr))))
(defn set-logger-format
"Configure JUL logger to use a custom log formatter.
* formatter - instance of java.util.logging.Formatter"
([]
(set-logger-format (simple-formatter format-log-record)))
([^Formatter formatter]
(let [main-logger (doto (Logger/getLogger "")
(.setUseParentHandlers false))
handler (doto (ConsoleHandler.)
(.setFormatter formatter)
(.setLevel java.util.logging.Level/ALL))
handlers (.getHandlers main-logger)]
(doseq [h handlers]
(.removeHandler main-logger h))
(.addHandler main-logger handler))))
(defn -main [& args]
(set-logger-format)
(log/trace "This is a TRACE msg \n")
(log/debug "This is DEBUG msg \n")
(log/info "This is INFO msg \n")
(log/warn "This is a WARN msg \n")
(log/error "This is a ERROR MSG \n")
(log/fatal "This is a FATAL MSG"))
this will just print:
^[This is INFO msg
This is a WARN msg
This is a ERROR MSG
This is a FATAL MSG
So, have you tried setting the logging factory for c.t.l explicitly? I don't see it in the above code, but it's also possible to do that outside of code.
i am not sure how to do that. i am reading about loading custom logging.properties file to set up java.util.logging config programatically. Or i can do this simpler with c.t.l. api ?
Haven't tested but should work:
(binding [clojure.tools.logging/*logger-factory* (clojure.tools.logging.impl/jul-factory)]
(log/info "info"))
Of course, outside of just testing things out you want to set *logger-factory*
only once.
Also, no need to create a properties file if you provide the property as a Java argument.logging does work with JUL - since the configuration kicks in. However logging some specific levels does not seem to work from what I can figure out
thanks for the help, i have set the log level not just for the handler, but also for the main-logger:
(defn set-logger-format
"Configure JUL logger to use a custom log formatter.
* formatter - instance of java.util.logging.Formatter"
([]
(set-logger-format (simple-formatter format-log-record)))
([^Formatter formatter]
(let [main-logger (doto (Logger/getLogger "")
(.setUseParentHandlers false)
(.setLevel java.util.logging.Level/ALL))
handler (doto (ConsoleHandler.)
(.setFormatter formatter)
(.setLevel java.util.logging.Level/ALL))
handlers (.getHandlers main-logger)]
(doseq [h handlers]
(.removeHandler main-logger h))
(.addHandler main-logger handler))))
now it prints the logs on all levels:face_palm::skin-tone-2: