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 &mdash; 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 &mdash; 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 &mdash; {@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 &mdash; 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 &mdash; 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 &mdash; 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}