clojure -Ttools install-latest :lib io.github.seancorfield/deps-new :as new
Pedestal includes a template that can be used with deps-new, a tool used to generate new projects from a template.
deps-new
works with the clojure
(or clj
) tool, and generates a deps.edn
-based project.
If you are a Leiningen user, it is relative straight forward to create an equivalent project.clj
from the
generated deps.edn
.
The embedded part indicates that the template is configured to work using reference:jetty.adoc, and it starts Jetty from within a running Clojure application (an alternate, and less used approach is to bundle a Pedestal application into a WAR and deploy into Jetty, or another servlet container).
deps-new
operates as a Clojure tool, and can be added using the following command:
clojure -Ttools install-latest :lib io.github.seancorfield/deps-new :as new
You will need the very latest version of this, version 0.7.0; you should re-execute the above command to ensure you have the latest.
You will also need some scaffolding in your ~/.clojure/deps.edn
; add the following
to the :aliases map:
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0-alpha5"}}}
This step is only necessary if Clojure 1.12 is not your default; at the time of writing, 1.12 was still in alpha. |
Before you begin, you should decide on a group name and project name for your new Pedestal application. These are combined with a slash to form the full project name.
For example, you might choose com.blueant
as your group name, and peripheral
as you project name (we’ll use
this example below), in which case, your full project name is com.blueant/peripheral
.
deps-new
will create a new project in a subdirectory matching your project name: peripheral
.
The command for this is somewhat arcane:
clojure -A:1.12 -Tnew create :template io.github.pedestal/pedestal%embedded%io.pedestal/embedded :name com.blueant/peripheral
The -A:1.12 option references the global alias you set up earlier and, again,
is only needed if Clojure 1.12 is not your default.
|
Example:
> clojure -A:1.12 -Tnew create :template io.github.pedestal/pedestal%embedded%io.pedestal/embedded :name com.blueant/peripheral
Resolving io.github.pedestal/pedestal as a git dependency
Creating project from io.pedestal/embedded in peripheral
> tree peripheral
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.clj
├── deps.edn
├── dev
│ ├── com
│ │ └── blueant
│ │ └── peripheral
│ │ └── telemetry_test_init.clj
│ ├── dev.clj
│ └── user.clj
├── doc
│ └── intro.md
├── resources
│ ├── pedestal-config.edn
│ └── public
│ └── index.html
├── src
│ └── com
│ └── blueant
│ └── peripheral
│ ├── routes.clj
│ ├── service.clj
│ └── telemetry_init.clj
├── start-jaeger.sh
├── test
│ └── com
│ └── blueant
│ └── peripheral
│ └── service_test.clj
└── test-resources
├── logback-test.xml
└── pedestal-test-config.edn
17 directories, 18 files
>
The exact set of files created may change over time, as the embedded template evolves. |
From the new directory (peripheral
) you can run tests:
> clj -T:build test
Running tests in #{"test"}
Testing com.blueant.peripheral.service-test
Ran 2 tests containing 2 assertions.
0 failures, 0 errors.
>
You can also fire up a REPL and start the service:
> clj -A:test
Clojure 1.11.1
user=> (use 'dev)
nil
user=> (go)
Routing table:
┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Method ┃ Path ┃ Name ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ :get ┃ /hello ┃ :com.blueant.peripheral.routes/hello ┃
┗━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
#:io.pedestal.http{:port 8080, :service-fn #object[io.pedestal.http.impl.servlet_interceptor$interceptor_service_fn$fn__17265 0x6853bae "io.pedestal.http.impl.servlet_interceptor$interceptor_service_fn$fn__17265@6853bae"], :host "localhost", :type :jetty, :start-fn #object[io.pedestal.http.jetty$server$fn__17934 0x26714a4a "io.pedestal.http.jetty$server$fn__17934@26714a4a"], :resource-path "public", :interceptors [#Interceptor{:name :io.pedestal.http.impl.servlet-interceptor/exception-debug} #Interceptor{:name :io.pedestal.http.cors/dev-allow-origin} #Interceptor{:name :io.pedestal.http/log-request} #Interceptor{:name :io.pedestal.http/not-found} #Interceptor{:name :io.pedestal.http.ring-middlewares/content-type-interceptor} #Interceptor{:name :io.pedestal.http.route/query-params} #Interceptor{:name :io.pedestal.http.route/method-param} #Interceptor{:name :io.pedestal.http.secure-headers/secure-headers} #Interceptor{:name :io.pedestal.http.ring-middlewares/resource} #Interceptor{:name :io.pedestal.http.route/router} #Interceptor{:name :io.pedestal.http.route/path-params-decoder}], :routes #object[com.blueant.peripheral.service$service_map$fn__17845 0x7589371f "com.blueant.peripheral.service$service_map$fn__17845@7589371f"], :servlet #object[io.pedestal.http.servlet.FnServlet 0x46a4eecd "io.pedestal.http.servlet.FnServlet@46a4eecd"], :server #object[org.eclipse.jetty.server.Server 0x1cc1ddad "Server@1cc1ddad{STARTED}[11.0.18,sto=0]"], :join? false, :stop-fn #object[io.pedestal.http.jetty$server$fn__17936 0x6953f5fc "io.pedestal.http.jetty$server$fn__17936@6953f5fc"]}
user=>
From another window, you can open http://localhost:8080/index.html, to see a brief welcoming page.
The dev
namespace provides the functions go
, start
, and stop
.
The :test alias sets up the classpath so that the dev
namespace is
available, and enables
REPL oriented development mode, including
the output of the routing table as the service started.
Because the application is running in debug mode, Pedestal has enabled extra logging output about the execution of each interceptor, and how the interceptor changed the context map.
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.cors/dev-allow-origin, :stage :enter, :execution-id 1, :context-changes {:added {[:request :headers "origin"] ""}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.tracing/tracing, :stage :enter, :execution-id 1, :context-changes {:added {[:bindings] ..., [:io.pedestal.http.tracing/otel-context-cleanup] ..., [:io.pedestal.http.tracing/prior-otel-context] ..., [:io.pedestal.http.tracing/otel-context] ..., [:io.pedestal.http.tracing/span] ...}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http/log-request, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/query-params, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/method-param, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.ring-middlewares/resource, :stage :enter, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/router, :stage :enter, :execution-id 1, :context-changes {:added {[:request :url-for] ..., [:request :path-params] [], [:route] ..., [:url-for] ...}, :changed {[:bindings] ..., [:io.pedestal.interceptor.chain/queue] ...}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.route/path-params-decoder, :stage :enter, :execution-id 1, :context-changes {:changed {[:request :path-params] {:from [], :to {}}}, :added {[:io.pedestal.http.route/path-params-decoded?] true}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor "#Interceptor{}", :stage :enter, :execution-id 1, :context-changes {:added {[:response] {:status 200, :body ...}}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.secure-headers/secure-headers, :stage :leave, :execution-id 1, :context-changes {:added {[:response :headers] {"Strict-Transport-Security" "max-age=31536000; includeSubdomains", "X-Frame-Options" "DENY", "X-Content-Type-Options" "nosniff", "X-XSS-Protection" "1; mode=block", "X-Download-Options" "noopen", "X-Permitted-Cross-Domain-Policies" "none", "Content-Security-Policy" "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;"}}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.ring-middlewares/content-type-interceptor, :stage :leave, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http/not-found, :stage :leave, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.tracing/tracing, :stage :leave, :execution-id 1, :context-changes {:changed {[:bindings] ...}, :removed {[:io.pedestal.http.tracing/otel-context-cleanup] ..., [:io.pedestal.http.tracing/prior-otel-context] ..., [:io.pedestal.http.tracing/otel-context] ..., [:io.pedestal.http.tracing/span] ...}}, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.impl.servlet-interceptor/ring-response, :stage :leave, :execution-id 1, :context-changes nil, :line 128}
DEBUG io.pedestal.interceptor.chain.debug - {:interceptor :io.pedestal.http.impl.servlet-interceptor/stylobate, :stage :leave, :execution-id 1, :context-changes nil, :line 128}
The template includes very basic support for gathering and reporting telementry using {otel}. For local work, this is best accomplished by running a Docker container with the Jaeger server running; the container will collect telemetry from the running application, and also provides a user interface to examine the traces produced by the application.
The template includes a script, start-jaeger.sh
that downloads the necessary files and starts
the container, and opens your web browser to the Jaeger UI:
> ./start-jaeger.sh
Downloading Open Telemetry Java Agent to target directory ...
f7296a450ab2bfad684451ed7e0ed22125c0743f79e9675c4e15f593570986de
Jaeger is running, execute `docker stop jaeger` to stop it.
>
Stop your old REPL session, if necessary, and start a new one:
> clj -A:test:otel-agent
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
[otel.javaagent 2024-03-01 16:32:45:144 -0800] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 2.1.0
Clojure 1.11.1
user=> (use 'dev)
nil
user=> (go)
Routing table:
┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Method ┃ Path ┃ Name ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ :get ┃ /hello ┃ :com.blueant.peripheral.routes/hello ┃
┗━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
#:io.pedestal.http{:port 8080, :service-fn #object[io.pedestal.http.impl ...
>
The :otel-agent alias enables the Open Telementry Java Agent; a Java Agent is a special library that "hooks into" the Java Virtual Machine, and can instrument classes as they are loaded from disk, or from JAR files. In this case, the agent will add code that initializes open telemetry in our application, and instrument the Jetty classes to capture the real times when requests arrive and responses are sent.
In a separate window, you can open http://localhost:8080/hello or http://localhost:8080/index.html. Your application will handle the requests while gathering and sending tracing data to the Jaeger server running inside the Docker container.
After that, go back to the Jaeger UI, and select com.blueant/peripheral
in the Service drop-down list [1], then click "Find Traces".
You can then select a specific trace to get more details about it:
You’ll notice that the single request has two overlapping traces; the outer trace was started and ended by the Open Telemetry java agent; the inner trace is just the part that Pedestal (not Jetty) is responsible for.
You don’t need to run your application with the Java agent in order to gather and send traces; however, the alternative involves quite a bit more setup, and many additional dependencies for all the necessary Open Telemetry libraries. Even then, without the Java agent, you will not get the most accurate measurements as you’ll only get the inner measurement covering the span of time Pedestal was handling the request. |
The lint
command uses clj-kondo to identify problems in your source code:
> clj -T:build lint
WARNING: update-vals already refers to: #'clojure.core/update-vals in namespace: clj-kondo.impl.analysis.java, being replaced by: #'clj-kondo.impl.utils/update-vals
linting took 137ms, errors: 0, warnings: 0
clj-kondo approves ☺️
>
The lint
command will exit with a -1 status code if there are linter errors; this aligns well with
using it inside a CI/CD pipeline.
The jar
command builds a Maven POM file, and a JAR for the project:
> clj -T:build jar
Writing pom.xml...
Copying source...
Building JAR target/com.blueant/peripheral-0.1.0-SNAPSHOT.jar ...
>
There’s also an install
command to install the JAR to your local Maven repository, and a deploy
command, to deploy the JAR to Clojars.
The template provides a tiny amount of structure and examples; it’s a seed from which you can grow a full project, but small as it is, it’s worth exploring in more detail.
com.blueant/peripheral
isn’t present, you will need to refresh the browser so that it can populate the list of services.
Can you improve this documentation? These fine people already did:
Howard M. Lewis Ship & Howard Lewis ShipEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close