Designing a Tomcat Valve for Request Logging: Architecture Note
Architecture note for a Tomcat Valve: requirements, minimal design, trust boundaries, operational checks, failure modes, and triggers for redesign.
30 Jul 2026, 07:29 UTC

Requirements
When adding cross‑cutting behavior to a Tomcat‑hosted web application without touching the application code, the typical goals are:
- Intercept every request that reaches a specific container (Engine, Host, or Context).
- Perform lightweight work such as logging, header manipulation, or basic authentication checks.
- Leave the request‑processing pipeline intact so downstream components (filters, servlets) receive the original request and response objects.
- Allow the component to be updated or reloaded without a full server restart when using a reloadable context.
Smallest Suitable Design
The minimal implementation that satisfies the above is a single Valve class chained into the container’s pipeline. The Valve must:
- Implement
org.apache.catalina.Valve(commonly by extendingorg.apache.catalina.valves.ValveBase). - Override
invoke(Request request, Response response)to perform the desired work and then callgetNext().invoke(request, response)to pass control onward. - Be thread‑safe because a single instance is shared by all request‑processing threads.
- Declare any required initialization (e.g., opening a log file) in the constructor or
start()method and clean up instop().
No additional components (filters, listeners, or custom connectors) are needed for the basic logging use‑case.
Trust and Data Boundaries
A Valve runs with the same privileges as the Tomcat process. Therefore:
- It must not expose internal state (e.g., file descriptors, in‑memory caches) to untrusted clients via the request or response objects.
- If the Valve inspects request parameters, headers, or cookies for security decisions, it must validate or sanitize that data before using it downstream.
- Storing request‑specific data in instance fields is unsafe; use request attributes or thread‑local storage only if the Valve guarantees isolation per request.
Operational Checks
To ensure the Valve behaves correctly in production, verify the following:
- Execution time: Measure the latency added by the Valve (e.g., with a simple
System.nanoTime()around theinvokebody) and confirm it stays within an acceptable budget (typically < 1 ms for logging). - Blocking I/O: Avoid synchronous file writes or network calls inside
invoke. If I/O is necessary, delegate to an asynchronous logger or a bounded queue. - Exception handling: The Valve must not swallow
Throwableinstances that would break the pipeline. Either let them propagate (Tomcat will log and return 500) or catch, log, and then re‑throw. - Reloadability: When the Valve class is packaged in a web‑application’s
WEB-INF/classesorWEB-INF/libdirectory, set the Host or Context attributereloadable="true". After updating the class file, touch the context’sWEB-INF/web.xmlor issue a manager reload command to trigger a reload.
Failure Modes
Common ways a Valve can disrupt service:
- Request hang: If
invokenever callsgetNext().invoke(e.g., due to an infinite loop), the request thread remains occupied, exhausting the connector’s thread pool. - Silent drop: Swallowing an exception and returning without invoking the next Valve causes Tomcat to treat the request as completed, leading to a 200 response with no content.
- Data leakage: Storing per‑request values in instance fields can cause one thread to see another thread’s data, potentially exposing credentials or session IDs.
- Pipeline order issues: Placing a Valve after the Authenticator valve may allow unauthenticated requests to reach downstream logic if the Valve modifies security‑relevant headers.
Conditions That Would Change the Design
Re‑evaluate the Valve‑based approach when any of the following arise:
- High‑volume, low‑latency requirements where even microsecond overhead matters – consider a custom Connector or Netty‑based handler instead.
- Need for per‑application isolation (different logging formats per webapp) – move the logic into a Servlet Filter scoped to each application.
- Requirement to modify the request before authentication (e.g., to inject a trusted header) – the Valve must be placed before the Authenticator valve in the pipeline.
- Desire to avoid sharing a single Valve instance across all hosts – configure separate Valve instances per Host or Context, which increases memory usage but provides stronger isolation.
Concrete Example: Request‑URI Logging Valve
The following Java snippet shows a minimal, thread‑safe Valve that logs each request’s URI and timestamp to a file using java.util.logging. The example assumes the Valve is bundled in a reloadable web application.
import org.apache.catalina.valves.ValveBase;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
public class RequestUriLoggerValve extends ValveBase {
private static final Logger LOG = Logger.getLogger(RequestUriLoggerValve.class.getName());
public RequestUriLoggerValve() {
try {
FileHandler fh = new FileHandler("request-uri.log", true);
fh.setFormatter(new SimpleFormatter());
LOG.addHandler(fh);
} catch (IOException e) {
throw new IllegalStateException("Cannot open log file", e);
}
}
@Override
public void invoke(Request request, Response response) throws IOException {
String uri = request.getRequestURI();
long start = System.nanoTime();
try {
getNext().invoke(request, response);
} finally {
long latencyMs = (System.nanoTime() - start) / 1_000_000;
LOG.info(String.format("%s %dms", uri, latencyMs));
}
}
}
Configuration
Place the compiled class in WEB-INF/classes of the web application and declare the Valve in the application’s context.xml:
<Context>
<Valve className="com.example.RequestUriLoggerValve" />
</Context>
Ensure the Host or Context has reloadable="true" so that updating the Valve class only requires a context reload.
Deployment and Verification Commands
Run the following steps on a Unix‑like system where Tomcat is installed at $CATALINA_HOME. The user must have read/write access to the Tomcat webapps directory and permission to execute the manager script.
- Build the Valve:
javac -cp $CATALINA_HOME/lib/tomcat-catalina.jar:src src/com/example/RequestUriLoggerValve.java -d target/classes jar -cf request-uri-logger-valve.jar -C target/classes . - Copy the JAR to the webapp:
cp request-uri-logger-valve.jar $CATALINA_HOME/webapps/myapp/WEB-INF/lib/ - Add or update context.xml:
echo '<Context><Valve className="com.example.RequestUriLoggerValve" /></Context>' > $CATALINA_HOME/webapps/myapp/META-INF/context.xml - Start (or restart) Tomcat:
$CATALINA_HOME/bin/startup.sh - Trigger a reload (if already running):
$CATALINA_HOME/bin/manager.sh reload /myapp - Send a test request:
curl -i http://localhost:8080/myapp/test - Check the log:
tail -f $CATALINA_HOME/logs/request-uri.log
Expected checks: Each request should produce a line in request-uri.log containing the requested URI and a latency value in milliseconds. If the Valve throws an exception, Tomcat’s console will show a stack trace and the client receives a 500 response.
Risks: Mis‑typing the class name prevents the Valve from loading, causing Tomcat to log a "Valve class not found" warning but otherwise continue processing. Ensure the JAR is readable by the Tomcat process user.
Limitations and Practical Verification
The Valve approach works well for low‑overhead, globally scoped concerns. It is not suitable for:
- Modifying the request body before it reaches a servlet (the Valve sees the raw
Requestbut the body may already be parsed). - Scoping logic to a subset of URLs within a single webapp without duplicating the Valve configuration (use a Filter instead).
- Situations where the Valve must maintain mutable state that cannot be made thread‑safe (consider a per‑request attribute or external service).
To verify that the Valve is indeed on the request path after a code change, add a temporary System.out.println("Valve invoked") statement, reload the context, and confirm the message appears in Tomcat’s console for each request. Remove the statement before production deployment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.