Filters
Filters are only applied to the Orchestration Cluster REST API of the gateway, and do not affect any gRPC calls.
For gRPC-related middleware, review the gateway interceptors documentation.
All communication from a client to a broker must first pass through a gateway. There, it can be filtered before being dispatched.
Zeebe provides a way to load arbitrary REST API filters into the gateway. Some typical examples of what you can accomplish with this include:
- Enforcing custom authorization rules on incoming calls
- Monitoring and logging incoming calls
Implementing a filter
To communicate between client and gateway, Zeebe uses REST.
A filter is then implemented as a Jakarta servlet filter.
An implementation must adhere to the following requirements:
- It implements a filter
- It has public visibility
- It has a public default constructor (for example, no-arg constructor)
Consider a filter logging incoming calls as an example:
package io.camunda.zeebe.example;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A simple filter that logs each incoming call. The class must be public
* since we will load it via JAR into the gateway.
*/
public final class LoggingFilter implements Filter {
private static final Logger LOGGER =
LoggerFactory.getLogger("LoggingFilter");
@Override
public void doFilter(
final ServletRequest servletRequest,
final ServletResponse servletResponse,
final FilterChain filterChain)
throws IOException, ServletException {
LOGGER.trace("filtered a call");
filterChain.doFilter(servletRequest, servletResponse);
}
}
This example filter will log filtered a call at TRACE level for
each incoming call it filters. This specific filter always dispatches
all incoming calls to the target broker, but it is possible to stop
the message from being filtered by other filters, and even block it from
dispatching to the broker.
Compiling your filter
Our source code for the filter class can now be compiled. There are many
ways to do this, but for simplicity we'll use javac directly.
When compiling your class, ensure all compile-time dependencies
are provided. In the example above, you'll need the jakarta.servlet-api and
slf4j-api libraries available when compiling.
Since the filter will be running inside the Zeebe Gateway, the language
level of the compiled code must be the same as Zeebe's (i.e. currently JDK 21) or lower. This example thus assumes you're using version 21 of javac.
# to compile LoggingFilter.java, we'll need to provide the api libraries
# the -d option places the class file in a directory that matches its package
javac -d . -classpath .:lib/jakarta.servlet-api.jar:lib/slf4j-api.jar ./LoggingFilter.java
Packaging a filter
Next, package the filter class into a fat JAR. Such a JAR must contain all classes (for example, all classes your own classes depend upon at runtime).
Like compiling, there are many ways to do this, but we'll use
jar directly. This means we must define a Java manifest file by hand to place the libraries' classes on the classpath.
Similar to your filter class, any libraries you package must be compiled for the same language level as Zeebe's (currently JDK 21) or lower.
The file path for jar should match the package name. For example, if your package name is com.example, you should package jar as jar cvfm LoggingFilter.jar ./MANIFEST.MF ./com/example/*.class ./lib.
# both runtime libraries and the manifest must be packaged together with the compiled classes
jar cvfm LoggingFilter.jar ./MANIFEST.MF ./io/camunda/zeebe/example/*.class ./lib
# let's verify the contents of the JAR
jar tf ./LoggingFilter.jar
# META-INF/
# META-INF/MANIFEST.MF
# io/camunda/zeebe/example/LoggingFilter.class
# lib/
# lib/jakarta.servlet-api.jar
# lib/slf4j-api.jar
# lib/slf4j.jar
Loading a filter into a gateway
A filter can be loaded into your gateway as a fat JAR. For each filter, provide the gateway with:
- A filter order index
- An identifier to identify this specific filter
- Where to find the JAR with the filter class
- The fully qualified name of the filter class. For example,
com.acme.ExampleFilter.
Continuing with the LoggingFilter example, provide these
configurations
using a gateway config file, environment variables, or a mix of both. We'll be
using a config file in this example.
The following gateway config file configures our LoggingFilter so it can be
loaded into the gateway at start-up:
zeebe:
gateway:
...
# allows specifying multiple filters
filters:
- # identifier, can be used for debugging
id: logging-filter
# name of our Filter implementation
# this must be the fully qualified name of the class
className: io.camunda.zeebe.example.LoggingFilter
# path to the fat JAR, can be absolute or relative
jarPath: /tmp/LoggingFilter.jar
# you can add additional filters by listing them
- id: ...
className: ...
jarPath: ...
Multiple filters can be configured (for example,
zeebe.gateway.filters expects a list of filter configurations). The
listing order determines the order in which a call is filtered by the
different filters. The first filter in the list wraps the second, etc.
The first filter is thus the outermost filter. In other words, calls
are filtered first by the first listed filter, followed by the second
listed filter, etc.
This configuration can also be provided using environment variables. Provide an index for the filter in the variable name to distinguish the
ordering of the different filters. For example, to configure the
className of the first filter use zeebe_gateway_filters_0_className. Likewise, a second filter's
jarPath can be configured using zeebe_gateway_filters_1_jarPath.
Class loading
Previously, we stated that you need to package the filter class into a fat JAR. Although good general advice, this is not entirely true. To understand why, let's discuss how the class loading of your filter works.
When your JAR is loaded into the gateway, Zeebe provides a special class loader for it. This class loader isolates your filter from the rest of Zeebe, but it also exposes our own code to your filter.
When loading classes for your filter, it will always first look in this special class loader. If it is not available, it will look in Zeebe's main class loader. Therefore, you can access any classes from Zeebe's main class loader when they are not provided by your JAR. For internal class loading, Zeebe will still only look in its main class loader.
You can reduce your JAR size by leaving out libraries already provided by Zeebe's class loader. Additionally, if your filter depends on a different version of a class than the one provided by Zeebe, you can provide your own version without having to worry about breaking Zeebe.
Troubleshooting
The gateway won't start with a misconfigured filter. The following errors identify what went wrong and how to fix it.
Environment variables can overwrite your gateway configuration file. The gateway logs the configuration it uses during startup, use this to verify your configuration.
java.lang.ClassNotFoundException
Observed behavior: The gateway fails to start and logs java.lang.ClassNotFoundException for your filter class.
Why this happens: Your Filter implementation couldn't be found on the classpath.
How to fix: Confirm the className is configured correctly in the gateway configuration, and that your JAR contains your class.
io.camunda.zeebe.gateway.rest.impl.filters.FilterLoadException
Observed behavior: The gateway fails to start and logs FilterLoadException.
Why this happens: Something went wrong loading your filter. The exception message describes the specific cause, but the two common ones are:
- Unable to instantiate your class: your class doesn't meet the filter requirements.
- The JAR could not be loaded: the filter isn't configured correctly in the gateway configuration, or the JAR isn't packaged correctly (for example, missing runtime dependencies in the manifest's classpath).
How to fix: Match the specific cause in the exception message to the list above, then apply the corresponding fix.
io.camunda.zeebe.util.jar.ExternalJarLoadException
Observed behavior: The gateway fails to start and logs ExternalJarLoadException.
Why this happens: The JAR itself couldn't be loaded.
How to fix: Confirm your filter is configured correctly in the gateway configuration.
java.lang.UnsupportedClassVersionError
Observed behavior: The gateway fails to start and logs UnsupportedClassVersionError.
Why this happens: Your filter was compiled with a more recent Java Runtime version than the gateway supports.
How to fix: Recompile your class with JDK 21.