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 &mdash; 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 &mdash; one factory method on the host
053 * &mdash; 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}} &mdash; 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/*}) &mdash; 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>, () -&gt; 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> &mdash; 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> &mdash; 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> &mdash; 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> &mdash; 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 &mdash; {@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 &mdash; 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] &mdash; 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] &mdash; 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] &mdash; run all registered cache-flush hooks (or a subset).
216    *
217    * @param req The current REST request &mdash; {@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] &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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}