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.stats;
018
019import static java.util.Comparator.*;
020import static java.util.stream.Collectors.toSet;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.ObjectUtils.*;
023import static org.apache.juneau.commons.utils.Shorts.*;
024
025import java.time.*;
026import java.util.*;
027import java.util.concurrent.*;
028
029import org.apache.juneau.commons.inject.*;
030
031/**
032 * An in-memory cache of thrown exceptions.
033 *
034 * <p>
035 * Used for preventing duplication of stack traces in log files and replacing them with small hashes.
036 *
037 * <h5 class='section'>See Also:</h5><ul>
038 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ExecutionStatistics">REST method execution statistics</a>
039 * </ul>
040 */
041public class ThrownStore {
042
043   /**
044    * Builder class.
045    */
046   public static class Builder {
047
048      final BeanStore beanStore;
049      ThrownStore parent;
050      Class<? extends ThrownStats> statsImplClass;
051      Set<Class<?>> ignoreClasses;
052      Clock clock;
053
054      private ThrownStore impl;
055      private Class<? extends ThrownStore> type;
056
057      /**
058       * Constructor.
059       *
060       * @param beanStore The bean store to use for creating beans.
061       */
062      protected Builder(BeanStore beanStore) {
063         this.beanStore = beanStore;
064      }
065
066      /**
067       * Returns the bean store.
068       *
069       * @return The bean store.
070       */
071      public BeanStore beanStore() {
072         return beanStore;
073      }
074
075      /**
076       * Specifies the clock used to stamp the first and last occurrence times of recorded exceptions.
077       *
078       * <p>
079       * Defaults to the parent store's clock, or {@link Clock#systemUTC()} if there is no parent.  Tests can pass
080       * {@link Clock#fixed(Instant, ZoneId)} to make the occurrence times reported by {@link ThrownStats} deterministic.
081       *
082       * @param value The new value for this setting.  Can be <jk>null</jk> (inherits the default).
083       * @return This object.
084       */
085      public Builder clock(Clock value) {
086         clock = value;
087         return this;
088      }
089
090      /**
091       * Specifies the list of classes to ignore when calculating stack traces.
092       *
093       * <p>
094       * Stack trace elements that are the specified class will be ignored.
095       *
096       * @param value The list of classes to ignore.
097       * @return This object.
098       */
099      public Builder ignoreClasses(Class<?>...value) {
100         this.ignoreClasses = set(value);
101         return this;
102      }
103
104      /**
105       * Overrides the bean returned by the {@link #build()} method with a pre-instantiated instance.
106       *
107       * @param value The setting value. Can be <jk>null</jk> (the {@link #build()} method instantiates the {@link #type(Class) type} instead).
108       * @return This object.
109       */
110      public Builder impl(Object value) {
111         impl = (ThrownStore) value;
112         return this;
113      }
114
115      /**
116       * Specifies the parent store of this store.
117       *
118       * <p>
119       * Parent stores are used for aggregating statistics across multiple child stores.
120       * <br>The {@link ThrownStore#GLOBAL} store can be used for aggregating all thrown exceptions in a single JVM.
121       *
122       * @param value The parent store.  Can be <jk>null</jk> for a root store with no parent chain.
123       * @return This object.
124       */
125      public Builder parent(ThrownStore value) {
126         parent = value;
127         return this;
128      }
129
130      /**
131       * Specifies a subclass of {@link ThrownStats} to use for individual method statistics.
132       *
133       * @param value The new value for this setting.
134       * @return This object.
135       */
136      public Builder statsImplClass(Class<? extends ThrownStats> value) {
137         statsImplClass = value;
138         return this;
139      }
140
141      /**
142       * Overrides the bean type produced by the {@link #build()} method.
143       *
144       * @param value The setting value. Can be <jk>null</jk> (defaults to {@link ThrownStore}).
145       * @return This object.
146       */
147      @SuppressWarnings({
148         "unchecked" // Type erasure on reflective/generic cast; element type is verified at call site
149      })
150      public Builder type(Class<?> value) {
151         type = (Class<? extends ThrownStore>) value;
152         return this;
153      }
154
155      /**
156       * Creates the bean.
157       *
158       * @return A new bean.
159       */
160      public ThrownStore build() {
161         if (nn(impl))
162            return impl;
163         if (type == null || type == ThrownStore.class)
164            return new ThrownStore(this);
165         return BeanInstantiator.of(ThrownStore.class, beanStore)
166            .type(type)
167            .noBuilder()
168            .addBean(Builder.class, this)
169            .run();
170      }
171   }
172
173   /** Identifies a single global store for the entire JVM. */
174   public static final ThrownStore GLOBAL = new ThrownStore();
175
176   /**
177    * Static creator.
178    *
179    * @return A new builder for this object.
180    */
181   public static Builder create() {
182      return new Builder(BasicBeanStore.INSTANCE);
183   }
184
185   /**
186    * Static creator.
187    *
188    * @param beanStore The bean store to use for creating beans.
189    * @return A new builder for this object.
190    */
191   public static Builder create(BeanStore beanStore) {
192      return new Builder(beanStore);
193   }
194
195   private final ConcurrentHashMap<Long,ThrownStats> db = new ConcurrentHashMap<>();
196   private final Optional<ThrownStore> parent;
197   private final BeanStore beanStore;
198   private final Class<? extends ThrownStats> statsImplClass;
199   private final Set<String> ignoreClasses;
200   private final Clock clock;
201
202   /**
203    * Constructor.
204    */
205   public ThrownStore() {
206      this(create(BasicBeanStore.INSTANCE));
207   }
208
209   /**
210    * Constructor.
211    *
212    * @param builder The builder for this object. Must not be <jk>null</jk>.
213    */
214   public ThrownStore(Builder builder) {
215      this.parent = o(builder.parent);
216      this.beanStore = builder.beanStore();
217
218      this.statsImplClass = coalesce(builder.statsImplClass, parent.isPresent() ? parent.get().statsImplClass : null, null);
219      this.clock = coalesce(builder.clock, parent.isPresent() ? parent.get().clock : null, Clock.systemUTC());
220
221      Set<String> s = null;
222      if (nn(builder.ignoreClasses))
223         s = builder.ignoreClasses.stream().map(Class::getName).collect(toSet());
224      if (s == null && parent.isPresent())
225         s = parent.get().ignoreClasses;
226      if (s == null)
227         s = Collections.emptySet();
228      this.ignoreClasses = u(s);
229   }
230
231   /**
232    * Adds the specified thrown exception to this database.
233    *
234    * @param e The exception to add.
235    * @return This object.
236    */
237   @SuppressWarnings({
238      "javabugs:S2259" // The exception must not be null, per the Javadoc contract; find() returns null only for a null input.
239   })
240   public ThrownStats add(Throwable e) {
241      ThrownStats s = find(e);
242      s.increment();
243      parent.ifPresent(x -> x.add(e));
244      return s;
245   }
246
247   /**
248    * Returns the list of all stack traces in this database.
249    *
250    * @return The list of all stack traces in this database, cloned and sorted by count descending.
251    */
252   public List<ThrownStats> getStats() { return db.values().stream().map(ThrownStats::copyOrNull).sorted(comparingInt(ThrownStats::getCount).reversed()).toList(); }
253
254   /**
255    * Retrieves the stack trace information for the exception with the specified hash as calculated by {@link #hash(Throwable)}.
256    *
257    * @param hash The hash of the exception.
258    * @return A clone of the stack trace info, never <jk>null</jk>.
259    */
260   public Optional<ThrownStats> getStats(long hash) {
261      ThrownStats s = db.get(hash);
262      return o(ThrownStats.copyOrNull(s));
263   }
264
265   /**
266    * Retrieves the stats for the specified thrown exception.
267    *
268    * @param e The exception. Must not be <jk>null</jk>.
269    * @return A clone of the stats, never <jk>null</jk>.
270    */
271   public Optional<ThrownStats> getStats(Throwable e) {
272      return getStats(hash(e));
273   }
274
275   /**
276    * Clears out the stack trace cache.
277    */
278   public void reset() {
279      db.clear();
280   }
281
282   private ThrownStats find(Throwable t) {
283
284      if (t == null)
285         return null;
286
287      long hash = hash(t);
288
289      ThrownStats stc = db.get(hash);
290      if (stc == null) {
291         // @formatter:off
292         stc = ThrownStats
293            .create(beanStore)
294            .type(statsImplClass)
295            .clock(clock)
296            .throwable(t)
297            .hash(hash)
298            .stackTrace(createStackTrace(t))
299            .causedBy(find(t.getCause()))
300            .build();
301         // @formatter:on
302
303         db.putIfAbsent(hash, stc);
304         stc = db.get(hash);
305      }
306
307      return stc;
308   }
309
310   /**
311    * Converts the stack trace for the specified throwable into a simple list of strings.
312    *
313    * <p>
314    * The stack trace elements for the throwable are sent through {@link #normalize(StackTraceElement)} to convert
315    * them to simple strings.
316    *
317    *
318    * @param t The throwable to create the stack trace for. Must not be <jk>null</jk>.
319    * @return A modifiable list of strings.
320    */
321   protected List<String> createStackTrace(Throwable t) {
322      return l(t.getStackTrace()).stream().filter(this::include).map(this::normalize).toList();
323   }
324
325   /**
326    * Calculates a 32-bit hash for the specified throwable based on the stack trace generated by {@link #createStackTrace(Throwable)}.
327    *
328    * <p>
329    * Subclasses can override this method to provide their own implementation.
330    *
331    * @param t The throwable to calculate the stack trace on. Must not be <jk>null</jk>.
332    * @return A calculated hash.
333    */
334   protected long hash(Throwable t) {
335      long h = 1125899906842597L; // prime
336      for (var s : createStackTrace(t)) {
337         var len = s.length();
338         for (var i = 0; i < len; i++)
339            h = 31 * h + s.charAt(i);
340      }
341      return h;
342   }
343
344   /**
345    * Returns <jk>true</jk> if the specified stack trace element should be included in {@link #createStackTrace(Throwable)}.
346    *
347    * @param e The stack trace element.
348    * @return <jk>true</jk> if the specified stack trace element should be included in {@link #createStackTrace(Throwable)}.
349    */
350   protected boolean include(StackTraceElement e) {
351      return true;
352   }
353
354   /**
355    * Converts the specified stack trace element into a normalized string.
356    *
357    * <p>
358    * The default implementation simply replaces <js>"\\$.*"</js> with <js>"..."</js> which should take care of stuff like stack
359    * trace elements of lambda expressions.
360    *
361    * @param e The stack trace element to convert. Must not be <jk>null</jk>.
362    * @return The converted stack trace element.
363    */
364   protected String normalize(StackTraceElement e) {
365      if (ignoreClasses.contains(e.getClassName()))
366         return "<ignored>";
367      var s = e.toString();
368      var i = s.indexOf('$');
369      if (i == -1)
370         return s;
371      var j = s.indexOf('(', i);
372      if (j == -1)
373         return s;  // Probably can't happen.
374      var s2 = s.substring(0, i);
375      var s3 = s.substring(j);
376      if (ignoreClasses.contains(s2))
377         return "<ignored>";
378      return s2 + "..." + s3;
379   }
380}