Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
GlowrootJettyHandler.java
Go to the documentation of this file.
1package dev.rafex.ether.glowroot.jetty12;
2
3import java.util.Collections;
4import java.util.HashMap;
5import java.util.HashSet;
6import java.util.Map;
7import java.util.Set;
8import java.util.concurrent.TimeUnit;
9import java.util.function.Function;
10
11import org.eclipse.jetty.server.Handler;
12import org.eclipse.jetty.server.Request;
13import org.eclipse.jetty.server.Response;
14import org.eclipse.jetty.util.Callback;
15import org.glowroot.agent.api.Glowroot;
16
17/*-
18 * #%L
19 * ether-glowroot-jetty12
20 * %%
21 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
22 * %%
23 * Permission is hereby granted, free of charge, to any person obtaining a copy
24 * of this software and associated documentation files (the "Software"), to deal
25 * in the Software without restriction, including without limitation the rights
26 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27 * copies of the Software, and to permit persons to whom the Software is
28 * furnished to do so, subject to the following conditions:
29 *
30 * The above copyright notice and this permission notice shall be included in
31 * all copies or substantial portions of the Software.
32 *
33 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
39 * THE SOFTWARE.
40 * #L%
41 */
42
43import dev.rafex.ether.http.jetty12.JettyAuthHandler;
44import dev.rafex.ether.observability.core.request.RequestIdGenerator;
45
46/**
47 * Jetty-level {@link Handler.Wrapper} that provides comprehensive Glowroot APM
48 * instrumentation in a single handler, designed for use with
49 * {@link dev.rafex.ether.http.jetty12.JettyMiddleware}-based architectures.
50 *
51 * <p>
52 * Combines all the capabilities of the ether-level middleware suite into one
53 * Jetty handler, making it compatible with projects that use Jetty's
54 * {@code Handler.Wrapper} chain rather than the ether {@code HttpExchange}
55 * middleware model.
56 * </p>
57 *
58 * <h2>What it instruments</h2>
59 * <ul>
60 * <li><b>Transaction type/name</b> — {@code "Web"} +
61 * {@code "METHOD /normalized/path"}</li>
62 * <li><b>Response status</b> — {@code http.status} and
63 * {@code http.status_class}</li>
64 * <li><b>Authenticated user</b> — {@code Glowroot.setTransactionUser()} via
65 * configurable extractor</li>
66 * <li><b>Request ID</b> — from a configurable header (e.g.
67 * {@code X-Request-Id}), with optional UUID generation</li>
68 * <li><b>Health check suppression</b> — raises slow-threshold to max for probe
69 * paths</li>
70 * <li><b>Per-route slow thresholds</b> — different thresholds per normalized
71 * path</li>
72 * <li><b>Error attributes</b> — {@code error} and {@code error.message} on
73 * uncaught exceptions</li>
74 * </ul>
75 *
76 * <h2>Usage (Jetty middleware / Kiwi-style registration)</h2>
77 *
78 * <pre>{@code
79 * final var glowroot = GlowrootJettyHandler.builder().healthPath("/health").requestIdHeader("X-Request-Id")
80 * .defaultSlowThreshold(2_000).userExtractor(ctx -> ctx instanceof MyAuthContext a ? a.subject() : null)
81 * .build();
82 *
83 * middlewareRegistry.add(glowroot::wrap); // Kiwi
84 * etherMiddlewares.add(glowroot::wrap); // ether JettyMiddleware
85 * }</pre>
86 */
87public final class GlowrootJettyHandler extends Handler.Wrapper {
88
89 private final Set<String> healthPaths;
90 private final Map<String, Long> thresholdByNormalizedPath;
91 private final long defaultThresholdMs;
92 private final String requestIdHeader;
93 private final RequestIdGenerator requestIdGenerator;
94 private final Function<Object, String> userExtractor;
95
96 private GlowrootJettyHandler(final Handler next, final Builder builder) {
97 super(next);
98 healthPaths = Set.copyOf(builder.healthPaths);
99 thresholdByNormalizedPath = Map.copyOf(builder.thresholds);
100 defaultThresholdMs = builder.defaultThresholdMs;
101 requestIdHeader = builder.requestIdHeader;
102 requestIdGenerator = builder.requestIdGenerator;
103 userExtractor = builder.userExtractor;
104 }
105
106 /** Returns a new {@link Builder}. */
107 public static Builder builder() {
108 return new Builder();
109 }
110
111 @Override
112 public boolean handle(final Request request, final Response response, final Callback callback) throws Exception {
113 final var method = request.getMethod();
114 final var path = request.getHttpURI() != null ? request.getHttpURI().getPath() : null;
115 final var normalized = PathNormalizer.normalize(path);
116
117 setTransactionIdentity(method, path, normalized);
118 applySlowThreshold(path, normalized);
119 captureRequestId(request);
120
121 try {
122 final var result = super.handle(request, response, callback);
123 captureResponseStatus(response);
124 captureAuthenticatedUser(request);
125 return result;
126 } catch (final Throwable t) {
127 captureError(t);
128 throw t;
129 }
130 }
131
132 /** Step 1 — sets the Glowroot transaction type, name and HTTP attributes. */
133 private void setTransactionIdentity(final String method, final String path, final String normalized) {
134 try {
135 Glowroot.setTransactionType("Web");
136 Glowroot.setTransactionName(method + " " + normalized);
137 Glowroot.addTransactionAttribute("http.method", method);
138 Glowroot.addTransactionAttribute("http.path", path == null ? "unknown" : path);
139 Glowroot.addTransactionAttribute("http.normalized_path", normalized);
140 } catch (final Throwable ignore) {
141 }
142 }
143
144 /** Step 2 — raises the slow-threshold for health probes, or applies the per-route/default one. */
145 private void applySlowThreshold(final String path, final String normalized) {
146 if (path != null && healthPaths.contains(path)) {
147 try {
148 Glowroot.setTransactionSlowThreshold(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
149 } catch (final Throwable ignore) {
150 }
151 return;
152 }
153 final var threshold = thresholdByNormalizedPath.getOrDefault(normalized, defaultThresholdMs);
154 if (threshold > 0) {
155 try {
156 Glowroot.setTransactionSlowThreshold(threshold, TimeUnit.MILLISECONDS);
157 } catch (final Throwable ignore) {
158 }
159 }
160 }
161
162 /** Step 3 — records the request ID from the configured header, generating one if absent. */
163 private void captureRequestId(final Request request) {
164 if (requestIdHeader == null) {
165 return;
166 }
167 try {
168 var reqId = request.getHeaders().get(requestIdHeader);
169 if ((reqId == null || reqId.isBlank()) && requestIdGenerator != null) {
170 reqId = requestIdGenerator.nextId();
171 }
172 if (reqId != null && !reqId.isBlank()) {
173 Glowroot.addTransactionAttribute("request.id", reqId);
174 }
175 } catch (final Throwable ignore) {
176 }
177 }
178
179 /** Step 5 — records the response status code and status class (after synchronous handling). */
180 private void captureResponseStatus(final Response response) {
181 try {
182 final var status = response.getStatus();
183 if (status > 0) {
184 Glowroot.addTransactionAttribute("http.status", String.valueOf(status));
185 Glowroot.addTransactionAttribute("http.status_class", status / 100 + "xx");
186 }
187 } catch (final Throwable ignore) {
188 }
189 }
190
191 /** Step 6 — records the authenticated user set by {@link JettyAuthHandler} inside the chain. */
192 private void captureAuthenticatedUser(final Request request) {
193 if (userExtractor == null) {
194 return;
195 }
196 try {
197 final var ctx = request.getAttribute(JettyAuthHandler.REQ_ATTR_AUTH);
198 if (ctx != null) {
199 final var user = userExtractor.apply(ctx);
200 if (user != null && !user.isBlank()) {
201 Glowroot.setTransactionUser(user);
202 Glowroot.addTransactionAttribute("auth.user", user);
203 }
204 }
205 } catch (final Throwable ignore) {
206 }
207 }
208
209 /** Step 7 — records error attributes for an uncaught exception. */
210 private void captureError(final Throwable t) {
211 try {
212 Glowroot.addTransactionAttribute("error", t.getClass().getName());
213 Glowroot.addTransactionAttribute("error.message", t.getMessage() == null ? "" : t.getMessage());
214 } catch (final Throwable ignore) {
215 }
216 }
217
218 /* ── Builder ─────────────────────────────────────────────────────────── */
219
220 public static final class Builder {
221
222 private final Set<String> healthPaths = new HashSet<>();
223 private final Map<String, Long> thresholds = new HashMap<>();
224 private long defaultThresholdMs = 2_000L;
225 private String requestIdHeader = null;
226 private RequestIdGenerator requestIdGenerator = null;
227 private Function<Object, String> userExtractor = null;
228
229 private Builder() {
230 }
231
232 /**
233 * Adds a path that should never appear in Glowroot's slow-transaction list
234 * (e.g. Kubernetes liveness/readiness probes).
235 */
236 public Builder healthPath(final String path) {
237 healthPaths.add(path);
238 return this;
239 }
240
241 /** Adds multiple health-check paths at once. */
242 public Builder healthPaths(final String... paths) {
243 Collections.addAll(healthPaths, paths);
244 return this;
245 }
246
247 /**
248 * Registers a custom slow-threshold (in ms) for a specific normalized path. The
249 * path should use placeholders: {@code "/api/export/:id"}.
250 */
251 public Builder slowThreshold(final String normalizedPath, final long thresholdMs) {
252 thresholds.put(normalizedPath, thresholdMs);
253 return this;
254 }
255
256 /**
257 * Sets the default slow-threshold (ms) used for paths with no specific entry.
258 * Defaults to {@code 2 000} ms.
259 */
260 public Builder defaultSlowThreshold(final long thresholdMs) {
261 defaultThresholdMs = thresholdMs;
262 return this;
263 }
264
265 /**
266 * Enables request-ID capture from the given header name.
267 *
268 * @param header header to read (e.g. {@code "X-Request-Id"})
269 */
270 public Builder requestIdHeader(final String header) {
271 requestIdHeader = header;
272 requestIdGenerator = null;
273 return this;
274 }
275
276 /**
277 * Enables request-ID capture from the given header name, with automatic UUID
278 * generation when the header is absent.
279 */
280 public Builder requestIdHeader(final String header, final boolean generateIfAbsent) {
281 requestIdHeader = header;
282 requestIdGenerator = generateIfAbsent ? new GlowrootRequestIdGenerator() : null;
283 return this;
284 }
285
286 /**
287 * Enables request-ID capture from the given header and delegates generation to
288 * a {@link RequestIdGenerator} when the header is absent.
289 */
290 public Builder requestIdHeader(final String header, final RequestIdGenerator requestIdGenerator) {
291 requestIdHeader = header;
292 this.requestIdGenerator = requestIdGenerator;
293 return this;
294 }
295
296 /**
297 * Sets the function used to extract the transaction user from the auth-context
298 * object stored in {@link JettyAuthHandler#REQ_ATTR_AUTH}.
299 *
300 * <p>
301 * The function receives the raw {@code Object} that was passed to
302 * {@link dev.rafex.ether.http.jetty12.TokenVerificationResult#ok(Object)} and
303 * should return the user identifier string, or {@code null} to skip user
304 * recording.
305 * </p>
306 *
307 * <p>
308 * Example for a custom {@code AuthContext} record:
309 * </p>
310 *
311 * <pre>{@code
312 * .userExtractor(ctx -> ctx instanceof MyAuthContext a ? a.subject() : null)
313 * }</pre>
314 */
315 public Builder userExtractor(final Function<Object, String> extractor) {
316 userExtractor = extractor;
317 return this;
318 }
319
320 /**
321 * Creates a {@link GlowrootJettyHandler} wrapping {@code next}.
322 *
323 * <p>
324 * This method is designed to be used as a method reference, matching both the
325 * {@link dev.rafex.ether.http.jetty12.JettyMiddleware} and any Kiwi-style
326 * {@code Middleware} functional interfaces:
327 * </p>
328 *
329 * <pre>{@code
330 * middlewareRegistry.add(glowrootBuilder::wrap);
331 * }</pre>
332 */
333 public GlowrootJettyHandler wrap(final Handler next) {
334 return new GlowrootJettyHandler(next, this);
335 }
336 }
337}
boolean handle(final Request request, final Response response, final Callback callback)