001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.ops; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.io.*; 023import java.lang.management.*; 024import java.util.*; 025import java.util.stream.*; 026 027import org.apache.juneau.commons.inject.*; 028import org.apache.juneau.http.response.*; 029import org.apache.juneau.marshall.json.*; 030import org.apache.juneau.rest.server.*; 031import org.apache.juneau.rest.server.guard.*; 032import org.apache.juneau.rest.server.guard.RateLimitGuard.*; 033 034/** 035 * Flavor-neutral operational-introspection worker bean shared by the admin {@code @Rest} flavors. 036 * 037 * <p> 038 * Holds the capability state (the name-keyed cache-flush hooks + the thread-name-prefix exclude list) 039 * and the serve logic for the four admin endpoints (thread dump, heap statistics, cache flush, and 040 * rate-limit-bucket inspection). The {@link AdminMixin} (mixin), {@link AdminResource} (child), and 041 * {@link AdminServlet} (servlet) flavors are independent {@code @Rest} classes that each hold an 042 * {@code AdminProvider} worker and delegate to it — so the three deployment forms cannot drift, and no 043 * flavor is another flavor's worker. 044 * 045 * <h5 class='section'>See Also:</h5><ul> 046 * <li class='jc'>{@link AdminMixin} 047 * <li class='jc'>{@link AdminResource} 048 * <li class='jc'>{@link AdminServlet} 049 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 050 * </ul> 051 * 052 * @since 10.0.0 053 */ 054// @formatter:off 055public class AdminProvider { 056 057 /** 058 * Default thread-name-prefix exclude list: filter out JVM internals, servlet container, and 059 * Spring Boot infrastructure threads. 060 */ 061 public static final List<String> DEFAULT_THREAD_NAME_PREFIX_EXCLUDE = List.of( 062 "Reference Handler", "Finalizer", "Signal Dispatcher", "Common-Cleaner", 063 "Notification Thread", "Attach Listener", 064 "jetty-", "qtp", 065 "spring-", 066 "GC ", "G1 "); 067 068 /** 069 * Creates a new builder. 070 * 071 * @return A new builder. 072 */ 073 public static Builder create() { 074 return new Builder(); 075 } 076 077 private final Map<String,Runnable> cacheFlushHooks; 078 private final List<String> threadNamePrefixExclude; 079 080 /** No-arg constructor — uses the default thread filter and no cache-flush hooks. */ 081 public AdminProvider() { 082 this(create()); 083 } 084 085 /** 086 * Builder constructor. 087 * 088 * @param builder The builder. Must not be {@code null}. 089 */ 090 protected AdminProvider(Builder builder) { 091 cacheFlushHooks = u(cp(builder.cacheFlushHooks)); 092 threadNamePrefixExclude = List.copyOf(builder.threadNamePrefixExclude); 093 } 094 095 /** 096 * Serves the thread dump as a JSON list of currently-live threads. 097 * 098 * @param res The current REST response. Must not be {@code null}. 099 * @throws IOException If an I/O error occurs while writing the response. 100 */ 101 @SuppressWarnings({ 102 "java:S1874" // Thread.getId() is not deprecated on the Java 17 baseline; its replacement Thread.threadId() was only added in Java 19, so getId() is the correct call here. 103 }) 104 public void serveThreads(RestResponse res) throws IOException { 105 var allTraces = Thread.getAllStackTraces(); 106 var out = new ArrayList<Map<String,Object>>(); 107 for (var e : allTraces.entrySet()) { 108 var t = e.getKey(); 109 if (isExcludedThread(t.getName())) 110 continue; 111 Map<String,Object> entry = m( 112 "name", t.getName(), 113 "id", t.getId(), 114 "state", t.getState().toString(), 115 "daemon", t.isDaemon(), 116 "priority", t.getPriority() 117 ); 118 var stack = Arrays.stream(e.getValue()).map(StackTraceElement::toString).collect(Collectors.toCollection(ArrayList::new)); 119 entry.put("stack", stack); 120 out.add(entry); 121 } 122 writeJson(res, out); 123 } 124 125 /** 126 * Serves JVM heap and non-heap memory statistics. 127 * 128 * @param res The current REST response. Must not be {@code null}. 129 * @throws IOException If an I/O error occurs while writing the response. 130 */ 131 public void serveHeap(RestResponse res) throws IOException { 132 var rt = Runtime.getRuntime(); 133 Map<String,Object> heap = m( 134 "total", rt.totalMemory(), 135 "free", rt.freeMemory(), 136 "max", rt.maxMemory(), 137 "used", rt.totalMemory() - rt.freeMemory() 138 ); 139 140 var mx = ManagementFactory.getMemoryMXBean(); 141 var nonHeapUsage = mx.getNonHeapMemoryUsage(); 142 Map<String,Object> nonHeap = m( 143 "init", nonHeapUsage.getInit(), 144 "used", nonHeapUsage.getUsed(), 145 "committed", nonHeapUsage.getCommitted(), 146 "max", nonHeapUsage.getMax() 147 ); 148 149 Map<String,Object> out = m("heap", heap, "nonHeap", nonHeap, "availableProcessors", rt.availableProcessors()); 150 writeJson(res, out); 151 } 152 153 /** 154 * Runs all registered cache-flush hooks (or a subset). 155 * 156 * <p> 157 * When {@code names} is supplied, only the named hooks are invoked. Unknown names are 158 * silently ignored (404-on-unknown would leak the registered hook set). Hook execution is 159 * synchronous; long-running hooks block the request thread. 160 * 161 * @param req The current REST request — {@code names} query parameter is read off it. Must not be {@code null}. 162 * @param res The current REST response. Must not be {@code null}. 163 * @throws IOException If an I/O error occurs while writing the response. 164 */ 165 public void serveCacheFlush(RestRequest req, RestResponse res) throws IOException { 166 var namesParam = req.getQueryParams().get("names").asString().orElse(null); 167 Set<String> selected = null; 168 if (inb(namesParam)) { 169 selected = new LinkedHashSet<>(); 170 for (var n : namesParam.split(",")) 171 if (! n.isBlank()) 172 selected.add(n.trim()); 173 } 174 var executed = new ArrayList<String>(); 175 for (var e : cacheFlushHooks.entrySet()) { 176 if (selected != null && ! selected.contains(e.getKey())) 177 continue; 178 e.getValue().run(); 179 executed.add(e.getKey()); 180 } 181 var out = new LinkedHashMap<String,Object>(); 182 out.put("registered", new ArrayList<>(cacheFlushHooks.keySet())); 183 out.put("executed", executed); 184 writeJson(res, out); 185 } 186 187 /** 188 * Serves the registered {@link RateLimitGuard} bean configuration. 189 * 190 * <p> 191 * Returns {@code 404 Not Found} when no {@code RateLimitGuard} bean is registered. 192 * 193 * @param req The current REST request — supplies the bean store for guard lookup. Must not be {@code null}. 194 * @param res The current REST response. Must not be {@code null}. 195 * @throws IOException If an I/O error occurs while writing the response. 196 * @throws NotFound If no {@link RateLimitGuard} bean is registered on the importer's bean store. 197 */ 198 @SuppressWarnings({ 199 "resource" // The context's WritableBeanStore is shared, long-lived infrastructure owned by RestContext; a per-request handler must never close it. 200 }) 201 public void serveRateLimit(RestRequest req, RestResponse res) throws IOException { 202 var bs = req.getContext().getBeanStore(); 203 var guards = collectRateLimitGuards(bs); 204 if (guards.isEmpty()) 205 throw new NotFound("No RateLimitGuard bean is registered."); 206 var entries = new LinkedHashMap<String,Object>(); 207 for (var e : guards.entrySet()) { 208 var g = e.getValue(); 209 var bucket = new LinkedHashMap<String,Object>(); 210 bucket.put("config", describeRateLimitGuard(g)); 211 bucket.put("snapshot", g.snapshot().values().stream() 212 .sorted(Comparator.comparing(BucketState::key)) 213 .toList()); 214 entries.put(e.getKey(), bucket); 215 } 216 var out = new LinkedHashMap<String,Object>(); 217 out.put("guards", entries); 218 writeJson(res, out); 219 } 220 221 /** 222 * Returns the registered cache-flush hooks (test/inspection helper). 223 * 224 * @return The hooks, keyed by registration name. Never {@code null}. 225 */ 226 public Map<String,Runnable> getCacheFlushHooks() { 227 return cacheFlushHooks; 228 } 229 230 /** 231 * Returns the configured thread-name-prefix exclude list (test/inspection helper). 232 * 233 * @return The exclude list. Never {@code null}. 234 */ 235 public List<String> getThreadNamePrefixExclude() { 236 return threadNamePrefixExclude; 237 } 238 239 private boolean isExcludedThread(String threadName) { 240 return threadName != null && threadNamePrefixExclude.stream().anyMatch(threadName::startsWith); 241 } 242 243 private static Map<String,RateLimitGuard> collectRateLimitGuards(BeanStore bs) { 244 // Multi-bean lookup so users running per-tier guards (free / paid / etc.) see every 245 // registered bean keyed by its bean name. Falls through to the single-bean path when no 246 // per-tier configuration is registered. 247 var byName = bs.getBeansOfType(RateLimitGuard.class); 248 if (byName != null && ! byName.isEmpty()) 249 return new LinkedHashMap<>(byName); 250 var out = new LinkedHashMap<String,RateLimitGuard>(); 251 bs.getBean(RateLimitGuard.class).ifPresent(g -> out.put("rateLimit", g)); 252 return out; 253 } 254 255 private static Map<String,Object> describeRateLimitGuard(RateLimitGuard g) { 256 return m( 257 "class", cn(g), 258 "limit", g.getCapacity(), 259 "permitsPerSecond", g.getPermitsPerSecond(), 260 "xForwardedForAware", g.isXForwardedForAware(), 261 "exemptPaths", g.getExemptPaths().stream().sorted().toList() 262 ); 263 } 264 265 private static void writeJson(RestResponse res, Object payload) throws IOException { 266 try (var w = res.getDirectWriter("application/json")) { 267 JsonSerializer.DEFAULT_READABLE.write(payload, w); 268 } 269 } 270 271 /** 272 * Builder for {@link AdminProvider} instances. 273 */ 274 public static class Builder { 275 276 private final Map<String,Runnable> cacheFlushHooks = m(); 277 private final List<String> threadNamePrefixExclude = new ArrayList<>(DEFAULT_THREAD_NAME_PREFIX_EXCLUDE); 278 279 /** Constructor — package access for {@link AdminProvider#create()}. */ 280 protected Builder() {} 281 282 /** 283 * Registers a cache-flush hook under a name. 284 * 285 * <p> 286 * The hook is invoked synchronously when {@code POST /admin/cache/flush} is called (with 287 * no {@code names} parameter, or with a {@code names} parameter that includes this name). 288 * 289 * @param name The hook name. Must not be {@code null} or blank. 290 * @param hook The hook. Must not be {@code null}. 291 * @return This object. 292 */ 293 public Builder cacheFlush(String name, Runnable hook) { 294 if (isBlank(name)) 295 throw new IllegalArgumentException("Argument 'name' must not be null or blank"); 296 if (hook == null) 297 throw new IllegalArgumentException("Argument 'hook' must not be null"); 298 cacheFlushHooks.put(name, hook); 299 return this; 300 } 301 302 /** 303 * Registers multiple cache-flush hooks at once. 304 * 305 * @param hooks The hooks, keyed by registration name. Can be {@code null} (no-op). 306 * @return This object. 307 */ 308 public Builder cacheFlushAll(Map<String,Runnable> hooks) { 309 if (hooks != null) 310 hooks.forEach(this::cacheFlush); 311 return this; 312 } 313 314 /** 315 * Replaces the thread-name-prefix exclude list. 316 * 317 * <p> 318 * Threads whose {@link Thread#getName() name} starts with any of the supplied prefixes are 319 * omitted from the {@code /admin/threads} output. Pass an empty array to disable 320 * filtering entirely. 321 * 322 * @param values The thread-name prefixes to exclude. Can be {@code null} (equivalent to an empty 323 * array — disables filtering); {@code null} or empty elements are skipped. 324 * @return This object. 325 */ 326 public Builder threadNamePrefixExclude(String...values) { 327 threadNamePrefixExclude.clear(); 328 if (values != null) 329 for (var v : values) 330 if (v != null && ! v.isEmpty()) 331 threadNamePrefixExclude.add(v); 332 return this; 333 } 334 335 /** 336 * Builds an {@link AdminProvider} instance. 337 * 338 * @return A configured instance. 339 */ 340 public AdminProvider build() { 341 return new AdminProvider(this); 342 } 343 } 344}