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 java.io.*; 020import java.lang.management.*; 021import java.util.*; 022 023import org.apache.juneau.rest.server.*; 024import org.apache.juneau.rest.server.guard.*; 025import org.apache.juneau.rest.server.guard.RateLimitGuard.*; 026 027/** 028 * Mixin that serves operational-introspection endpoints under {@code /admin/*}: thread dump, 029 * heap statistics, configurable cache-flush hooks, and rate-limit-bucket inspection. 030 * 031 * <p> 032 * Sibling of {@link EchoMixin} ({@code /echo/*}) and 033 * {@link RouteIndexMixin} ({@code /options}). All three classes live in 034 * the {@code org.apache.juneau.rest.server.ops} ops/introspection mixin pack. 035 * 036 * <h5 class='section'>Default-deny security posture:</h5> 037 * 038 * <p> 039 * The mixin is annotated with {@link Rest#guards() @Rest(guards=DenyAllGuard.class)}, so every 040 * admin path returns {@code 403 Forbidden} until the importer registers a 041 * {@link org.apache.juneau.commons.inject.Bean @Bean} {@link RestGuardList} factory on the host. 042 * The framework's bean-store override seam <b>replaces</b> the entire annotation-derived guard 043 * list (including this deny-all) with the user-supplied chain — pair the mixin with whatever 044 * authentication / authorization story your service uses (bearer-token guard, API-key guard, 045 * Spring Security adapter, etc.). 046 * 047 * <p> 048 * <b>Why deny-all rather than a non-existent role-name placeholder?</b> The role-guard approach 049 * (declaring a role that no real principal has) has clean semantics but harder ergonomics: the 050 * importer overrides by setting the principal's roles to include the placeholder, which couples 051 * the host's auth strategy to a framework-internal role name. Replacing a {@link RestGuardList} 052 * via {@code @Bean} keeps the override surface narrow — one factory method on the host 053 * — and matches the way every other Juneau op-context bean is wired. 054 * 055 * <h5 class='section'>Configurable mount prefix:</h5> 056 * 057 * <p> 058 * The default mount prefix {@code /admin} can be overridden via the SVL variable 059 * {@code ${juneau.admin.path:admin}} — a single override relocates all four endpoints 060 * ({@code /threads}, {@code /heap}, {@code /cache/flush}, {@code /ratelimit}) under the new 061 * prefix. Set via system property ({@code -Djuneau.admin.path=ops}), environment variable 062 * ({@code JUNEAU_ADMIN_PATH=ops}), or {@code Config} key 063 * ({@code juneau.admin.path = ops}) to change the runtime mount without subclassing. 064 * Resolution happens once at {@link RestContext} construction time (SVL resolution in {@code @RestOp(path)}). 065 * 066 * <p> 067 * Override accepts bare token ({@code admin}), leading slash ({@code /admin}), trailing slash 068 * ({@code admin/}), or wildcard suffix ({@code /admin/*}) — all resolve to the same mount. 069 * 070 * <h5 class='section'>Mixin-only deployment:</h5> 071 * 072 * <p> 073 * This resource is designed for composition via {@code @Rest(mixins=...)}. The four mount paths 074 * ({@code /admin/threads}, {@code /admin/heap}, {@code /admin/cache/flush}, 075 * {@code /admin/ratelimit}) are pinned at the op level by {@code @RestGet/@RestPost(path=...)} 076 * on the handler methods; a class-level {@code @Rest(paths=...)} declaration would be silently 077 * ignored under the mixin pattern (see {@link Rest#paths() @Rest(paths)} Javadoc). 078 * 079 * <h5 class='figure'>Composition example:</h5> 080 * 081 * <p class='bjava'> 082 * <ja>@Rest</ja>(path=<js>"/api"</js>, mixins=AdminMixin.<jk>class</jk>) 083 * <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet { 084 * 085 * <jc>// Required: register an auth guard chain to unlock the admin paths.</jc> 086 * <ja>@Bean</ja>(name=<js>"guards"</js>) 087 * <jk>public</jk> RestGuardList guards(BeanStore <jv>bs</jv>) { 088 * <jk>return</jk> RestGuardList.<jsm>create</jsm>(<jv>bs</jv>) 089 * .append(<jk>new</jk> MyBearerTokenGuard()) 090 * .build(); 091 * } 092 * 093 * <ja>@Bean</ja> AdminMixin admin() { 094 * <jk>return</jk> AdminMixin.<jsm>create</jsm>() 095 * .cacheFlush(<js>"primary"</js>, () -> primaryCache.invalidateAll()) 096 * .build(); 097 * } 098 * } 099 * </p> 100 * 101 * <h5 class='section'>Endpoints:</h5> 102 * 103 * <ul class='spaced-list'> 104 * <li><b>{@code GET /admin/threads}</b> — emits a JSON list of currently-live threads. The 105 * default thread filter excludes framework noise (JVM internals, servlet container, Spring 106 * Boot infrastructure); override via {@link Builder#threadNamePrefixExclude(String...)}. 107 * <li><b>{@code GET /admin/heap}</b> — emits a JSON map with {@code Runtime} heap stats 108 * ({@code total}, {@code free}, {@code max}, {@code used}) plus 109 * {@link MemoryMXBean#getNonHeapMemoryUsage() non-heap} memory usage. No heap-dump file 110 * generation in v1 (security risk). 111 * <li><b>{@code POST /admin/cache/flush}</b> — runs all registered cache-flush hooks, or 112 * just a comma-separated {@code names} subset when supplied as a query parameter. Returns a 113 * JSON map of the hooks that were invoked. Hooks are registered name-keyed via 114 * {@link Builder#cacheFlush(String,Runnable)}; users that want async semantics own the 115 * threading model. 116 * <li><b>{@code GET /admin/ratelimit}</b> — emits a JSON map keyed by bean name listing the 117 * registered {@link RateLimitGuard} configuration and live per-bucket state. Each entry has 118 * two sub-fields: {@code config} (the guard's static configuration — {@code class}, 119 * {@code limit}, {@code permitsPerSecond}, {@code xForwardedForAware}, {@code exemptPaths}) 120 * and {@code snapshot} (a sorted array of {@link BucketState} entries describing every 121 * per-key bucket the storage backend currently tracks). Returns {@code 404 Not Found} when 122 * no {@code RateLimitGuard} bean is registered on the importer's bean store. Storage 123 * backends that don't override {@link org.apache.juneau.rest.server.guard.RateLimitGuard.Storage#snapshot() RateLimitGuard.Storage#snapshot()} (e.g. Redis-backed 124 * impls that can't cheaply enumerate buckets) emit an empty {@code snapshot} array. 125 * </ul> 126 * 127 * <p> 128 * All four endpoints carry {@link OpSwagger#ignore() @OpSwagger(ignore=true)} so the admin surface 129 * stays out of any generated Swagger / OpenAPI spec. 130 * 131 * <h5 class='section'>See Also:</h5><ul> 132 * <li class='jc'>{@link EchoMixin} 133 * <li class='jc'>{@link RouteIndexMixin} 134 * <li class='jc'>{@link DenyAllGuard} 135 * <li class='jc'>{@link RestGuardList} 136 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerComposition">REST Server — Mixins and Multi-Mount Paths</a> 137 * </ul> 138 * 139 * @since 10.0.0 140 */ 141// @formatter:off 142@Rest( 143 guards=DenyAllGuard.class 144) 145public class AdminMixin { 146 147 /** 148 * Default thread-name-prefix exclude list: filter out JVM internals, servlet container, and 149 * Spring Boot infrastructure threads. 150 */ 151 public static final List<String> DEFAULT_THREAD_NAME_PREFIX_EXCLUDE = List.copyOf(AdminProvider.DEFAULT_THREAD_NAME_PREFIX_EXCLUDE); 152 153 /** 154 * Creates a new builder. 155 * 156 * @return A new builder. 157 */ 158 public static Builder create() { 159 return new Builder(); 160 } 161 162 private final AdminProvider worker; 163 164 /** 165 * No-arg constructor — delegates to a default {@link AdminProvider} worker (default thread filter 166 * and no cache-flush hooks). 167 */ 168 public AdminMixin() { 169 this(new AdminProvider()); 170 } 171 172 /** 173 * Worker constructor. 174 * 175 * @param worker The shared {@link AdminProvider} worker this flavor delegates to. Must not be 176 * <jk>null</jk>. 177 */ 178 protected AdminMixin(AdminProvider worker) { 179 this.worker = worker; 180 } 181 182 /** 183 * [GET /admin/threads] — emit a JSON list of currently-live threads. 184 * 185 * @param res The current REST response. 186 * @throws IOException If an I/O error occurs while writing the response. 187 */ 188 @RestGet( 189 path="/#{pathToken(${juneau.admin.path:admin})}/threads", 190 summary="Thread dump", 191 description="JSON list of currently-live threads (filtered to exclude framework noise by default).", 192 swagger=@OpSwagger(ignore=true) 193 ) 194 public void getThreads(RestResponse res) throws IOException { 195 worker.serveThreads(res); 196 } 197 198 /** 199 * [GET /admin/heap] — emit JVM heap and non-heap memory statistics. 200 * 201 * @param res The current REST response. 202 * @throws IOException If an I/O error occurs while writing the response. 203 */ 204 @RestGet( 205 path="/#{pathToken(${juneau.admin.path:admin})}/heap", 206 summary="Heap statistics", 207 description="JVM heap + non-heap memory statistics (Runtime + MemoryMXBean).", 208 swagger=@OpSwagger(ignore=true) 209 ) 210 public void getHeap(RestResponse res) throws IOException { 211 worker.serveHeap(res); 212 } 213 214 /** 215 * [POST /admin/cache/flush] — run all registered cache-flush hooks (or a subset). 216 * 217 * @param req The current REST request — {@code names} query parameter is read off it. 218 * @param res The current REST response. 219 * @throws IOException If an I/O error occurs while writing the response. 220 */ 221 @RestPost( 222 path="/#{pathToken(${juneau.admin.path:admin})}/cache/flush", 223 summary="Cache flush", 224 description="Runs the registered cache-flush hooks (all by default; ?names=foo,bar for a subset).", 225 swagger=@OpSwagger(ignore=true) 226 ) 227 public void postCacheFlush(RestRequest req, RestResponse res) throws IOException { 228 worker.serveCacheFlush(req, res); 229 } 230 231 /** 232 * [GET /admin/ratelimit] — emit registered {@link RateLimitGuard} bean configuration. 233 * 234 * <p> 235 * Returns {@code 404 Not Found} when no {@code RateLimitGuard} bean is registered. 236 * 237 * @param req The current REST request — supplies the bean store for guard lookup. 238 * @param res The current REST response. 239 * @throws IOException If an I/O error occurs while writing the response. 240 */ 241 @RestGet( 242 path="/#{pathToken(${juneau.admin.path:admin})}/ratelimit", 243 summary="Rate-limit inspection", 244 description="JSON map of registered RateLimitGuard beans keyed by bean name.", 245 swagger=@OpSwagger(ignore=true) 246 ) 247 public void getRateLimit(RestRequest req, RestResponse res) throws IOException { 248 worker.serveRateLimit(req, res); 249 } 250 251 /** 252 * Returns the registered cache-flush hooks (test/inspection helper). 253 * 254 * @return The hooks, keyed by registration name. Never {@code null}. 255 */ 256 public Map<String,Runnable> getCacheFlushHooks() { 257 return worker.getCacheFlushHooks(); 258 } 259 260 /** 261 * Returns the configured thread-name-prefix exclude list (test/inspection helper). 262 * 263 * @return The exclude list. Never {@code null}. 264 */ 265 public List<String> getThreadNamePrefixExclude() { 266 return worker.getThreadNamePrefixExclude(); 267 } 268 269 /** 270 * Builder for {@link AdminMixin} instances. 271 * 272 * <p> 273 * Mirrors {@link AdminProvider.Builder}'s configuration methods on the mixin's own surface and forwards 274 * each call to an underlying {@link AdminProvider.Builder}, which builds the shared worker the mixin 275 * delegates to. 276 */ 277 public static class Builder { 278 279 private final AdminProvider.Builder worker = AdminProvider.create(); 280 281 /** Constructor — package access for {@link AdminMixin#create()}. */ 282 protected Builder() {} 283 284 /** 285 * Registers a cache-flush hook under a name. 286 * 287 * <p> 288 * The hook is invoked synchronously when {@code POST /admin/cache/flush} is called (with 289 * no {@code names} parameter, or with a {@code names} parameter that includes this name). 290 * 291 * @param name The hook name. Must not be {@code null} or blank. 292 * @param hook The hook. Must not be {@code null}. 293 * @return This object. 294 */ 295 public Builder cacheFlush(String name, Runnable hook) { 296 worker.cacheFlush(name, hook); 297 return this; 298 } 299 300 /** 301 * Registers multiple cache-flush hooks at once. 302 * 303 * @param hooks The hooks, keyed by registration name. Can be {@code null} (no-op). 304 * @return This object. 305 */ 306 public Builder cacheFlushAll(Map<String,Runnable> hooks) { 307 worker.cacheFlushAll(hooks); 308 return this; 309 } 310 311 /** 312 * Replaces the thread-name-prefix exclude list. 313 * 314 * <p> 315 * Threads whose {@link Thread#getName() name} starts with any of the supplied prefixes are 316 * omitted from the {@code /admin/threads} output. Pass an empty array to disable 317 * filtering entirely. 318 * 319 * @param values The thread-name prefixes to exclude. Can be {@code null} (equivalent to an empty 320 * array — disables filtering); {@code null} or empty elements are skipped. 321 * @return This object. 322 */ 323 public Builder threadNamePrefixExclude(String...values) { 324 worker.threadNamePrefixExclude(values); 325 return this; 326 } 327 328 /** 329 * Builds an {@link AdminMixin} instance. 330 * 331 * @return A configured instance. 332 */ 333 public AdminMixin build() { 334 return new AdminMixin(worker.build()); 335 } 336 } 337}