Embedded Template

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 Jetty 11, 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).

Setting up deps-new

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-beta1"}}}
This step is only necessary if Clojure 1.12 is not your default; at the time of writing, 1.12 was still in beta.

Creating a project

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. Clojure 1.12 is only needed to initially construct your project, to build or run your project, Clojure 1.10 or above is all that’s needed.

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
│   ├── logback.xml
│   ├── pedestal-config.edn
│   └── public
│       └── index.html
├── src
│   └── com
│       └── blueant
│           └── peripheral
│               ├── main.clj
│               ├── 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, 20 files
>
The exact set of files created may change over time, as the embedded template evolves.

Exploring the new project

From the new directory (peripheral) you can run tests:

> clj -T:build test

Running tests in #{"test"}

Testing com.blueant.peripheral.service-test

Ran 3 tests containing 3 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                 ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  :post ┃ /hello ┃ :com.blueant.peripheral.routes/greet ┃
┃   :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}

You can also use curl or http to make a request:

> http --json post :8080/hello name="Pedestal User"
HTTP/1.1 200 OK
Content-Security-Policy: object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;
Content-Type: text/plain
Date: Fri, 12 Jul 2024 18:52:06 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: DENY
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 1; mode=block

Hello, Pedestal User.


>

Starting the service

Alternately, you can start the service directly without starting a REPL:

> clj -X:run
INFO  com.blueant.peripheral.main - {:msg "Service com.blueant/peripheral startup", :port 8080, :line 9}

At this point, the service is running; you can use another window to execute HTTP requests. If you open a browser window to http://localhost:8080/index.html, you’ll see the following logged to the service’s console:

INFO  io.pedestal.http - {:msg "GET /index.html", :line 83}
INFO  io.pedestal.http - {:msg "GET /favicon.ico", :line 83}

Gathering Telemetry

The template includes very basic support for gathering and reporting telementry using Open Telemetry. 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                 ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  :post ┃ /hello ┃ :com.blueant.peripheral.routes/greet ┃
┃   :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".

jaeger ui search

You can then select a specific trace to get more details about it:

jaeger ui trace

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.

Other build commands

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.

Conclusion

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.


1. If com.blueant/peripheral isn’t present, you will need to refresh the browser so that it can populate the list of services.