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.stats;
018
019import static java.util.Comparator.*;
020import static java.util.stream.Collectors.toList;
021import static java.util.stream.Collectors.toSet;
022import static org.apache.juneau.commons.utils.CollectionUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
024
025import java.util.*;
026import java.util.concurrent.*;
027
028import org.apache.juneau.*;
029import org.apache.juneau.cp.*;
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    * Builder class.
044    */
045   public static class Builder extends BeanBuilder<ThrownStore> {
046
047      ThrownStore parent;
048      Class<? extends ThrownStats> statsImplClass;
049      Set<Class<?>> ignoreClasses;
050
051      /**
052       * Constructor.
053       *
054       * @param beanStore The bean store to use for creating beans.
055       */
056      protected Builder(BeanStore beanStore) {
057         super(ThrownStore.class, beanStore);
058      }
059
060      /**
061       * Specifies the list of classes to ignore when calculating stack traces.
062       *
063       * <p>
064       * Stack trace elements that are the specified class will be ignored.
065       *
066       * @param value The list of classes to ignore.
067       * @return This object.
068       */
069      public Builder ignoreClasses(Class<?>...value) {
070         this.ignoreClasses = set(value);
071         return this;
072      }
073
074      @Override /* Overridden from BeanBuilder */
075      public Builder impl(Object value) {
076         super.impl(value);
077         return this;
078      }
079
080      /**
081       * Specifies the parent store of this store.
082       *
083       * <p>
084       * Parent stores are used for aggregating statistics across multiple child stores.
085       * <br>The {@link ThrownStore#GLOBAL} store can be used for aggregating all thrown exceptions in a single JVM.
086       *
087       * @param value The parent store.  Can be <jk>null</jk>.
088       * @return This object.
089       */
090      public Builder parent(ThrownStore value) {
091         parent = value;
092         return this;
093      }
094
095      /**
096       * Specifies a subclass of {@link ThrownStats} to use for individual method statistics.
097       *
098       * @param value The new value for this setting.
099       * @return This object.
100       */
101      public Builder statsImplClass(Class<? extends ThrownStats> value) {
102         statsImplClass = value;
103         return this;
104      }
105
106      @Override /* Overridden from BeanBuilder */
107      public Builder type(Class<?> value) {
108         super.type(value);
109         return this;
110      }
111
112      @Override /* Overridden from BeanBuilder */
113      protected ThrownStore buildDefault() {
114         return new ThrownStore(this);
115      }
116   }
117
118   /** Identifies a single global store for the entire JVM. */
119   public static final ThrownStore GLOBAL = new ThrownStore();
120
121   /**
122    * Static creator.
123    *
124    * @return A new builder for this object.
125    */
126   public static Builder create() {
127      return new Builder(BeanStore.INSTANCE);
128   }
129
130   /**
131    * Static creator.
132    *
133    * @param beanStore The bean store to use for creating beans.
134    * @return A new builder for this object.
135    */
136   public static Builder create(BeanStore beanStore) {
137      return new Builder(beanStore);
138   }
139
140   private final ConcurrentHashMap<Long,ThrownStats> db = new ConcurrentHashMap<>();
141   private final Optional<ThrownStore> parent;
142   private final BeanStore beanStore;
143   private final Class<? extends ThrownStats> statsImplClass;
144   private final Set<String> ignoreClasses;
145
146   /**
147    * Constructor.
148    */
149   public ThrownStore() {
150      this(create(BeanStore.INSTANCE));
151   }
152
153   /**
154    * Constructor.
155    *
156    * @param builder The builder for this object.
157    */
158   public ThrownStore(Builder builder) {
159      this.parent = opt(builder.parent);
160      this.beanStore = builder.beanStore();
161
162      this.statsImplClass = firstNonNull(builder.statsImplClass, parent.isPresent() ? parent.get().statsImplClass : null, null);
163
164      var s = (Set<String>)null;
165      if (nn(builder.ignoreClasses))
166         s = builder.ignoreClasses.stream().map(Class::getName).collect(toSet());
167      if (s == null && parent.isPresent())
168         s = parent.get().ignoreClasses;
169      if (s == null)
170         s = Collections.emptySet();
171      this.ignoreClasses = u(s);
172   }
173
174   /**
175    * Adds the specified thrown exception to this database.
176    *
177    * @param e The exception to add.
178    * @return This object.
179    */
180   public ThrownStats add(Throwable e) {
181      ThrownStats s = find(e);
182      s.increment();
183      parent.ifPresent(x -> x.add(e));
184      return s;
185   }
186
187   /**
188    * Returns the list of all stack traces in this database.
189    *
190    * @return The list of all stack traces in this database, cloned and sorted by count descending.
191    */
192   public List<ThrownStats> getStats() { return db.values().stream().map(ThrownStats::clone).sorted(comparingInt(ThrownStats::getCount).reversed()).collect(toList()); }
193
194   /**
195    * Retrieves the stack trace information for the exception with the specified hash as calculated by {@link #hash(Throwable)}.
196    *
197    * @param hash The hash of the exception.
198    * @return A clone of the stack trace info, never <jk>null</jk>.
199    */
200   public Optional<ThrownStats> getStats(long hash) {
201      ThrownStats s = db.get(hash);
202      return opt(s == null ? null : s.clone());
203   }
204
205   /**
206    * Retrieves the stats for the specified thrown exception.
207    *
208    * @param e The exception.
209    * @return A clone of the stats, never <jk>null</jk>.
210    */
211   public Optional<ThrownStats> getStats(Throwable e) {
212      return getStats(hash(e));
213   }
214
215   /**
216    * Clears out the stack trace cache.
217    */
218   public void reset() {
219      db.clear();
220   }
221
222   private ThrownStats find(Throwable t) {
223
224      if (t == null)
225         return null;
226
227      long hash = hash(t);
228
229      ThrownStats stc = db.get(hash);
230      if (stc == null) {
231         // @formatter:off
232         stc = ThrownStats
233            .create(beanStore)
234            .type(statsImplClass)
235            .throwable(t)
236            .hash(hash)
237            .stackTrace(createStackTrace(t))
238            .causedBy(find(t.getCause()))
239            .build();
240         // @formatter:on
241
242         db.putIfAbsent(hash, stc);
243         stc = db.get(hash);
244      }
245
246      return stc;
247   }
248
249   /**
250    * Converts the stack trace for the specified throwable into a simple list of strings.
251    *
252    * <p>
253    * The stack trace elements for the throwable are sent through {@link #normalize(StackTraceElement)} to convert
254    * them to simple strings.
255    *
256    *
257    * @param t The throwable to create the stack trace for.
258    * @return A modifiable list of strings.
259    */
260   protected List<String> createStackTrace(Throwable t) {
261      return l(t.getStackTrace()).stream().filter(this::include).map(this::normalize).collect(toList());
262   }
263
264   /**
265    * Calculates a 32-bit hash for the specified throwable based on the stack trace generated by {@link #createStackTrace(Throwable)}.
266    *
267    * <p>
268    * Subclasses can override this method to provide their own implementation.
269    *
270    * @param t The throwable to calculate the stack trace on.
271    * @return A calculated hash.
272    */
273   protected long hash(Throwable t) {
274      long h = 1125899906842597L; // prime
275      for (var s : createStackTrace(t)) {
276         var len = s.length();
277         for (var i = 0; i < len; i++)
278            h = 31 * h + s.charAt(i);
279      }
280      return h;
281   }
282
283   /**
284    * Returns <jk>true</jk> if the specified stack trace element should be included in {@link #createStackTrace(Throwable)}.
285    *
286    * @param e The stack trace element.
287    * @return <jk>true</jk> if the specified stack trace element should be included in {@link #createStackTrace(Throwable)}.
288    */
289   protected boolean include(StackTraceElement e) {
290      return true;
291   }
292
293   /**
294    * Converts the specified stack trace element into a normalized string.
295    *
296    * <p>
297    * The default implementation simply replaces <js>"\\$.*"</js> with <js>"..."</js> which should take care of stuff like stack
298    * trace elements of lambda expressions.
299    *
300    * @param e The stack trace element to convert.
301    * @return The converted stack trace element.
302    */
303   protected String normalize(StackTraceElement e) {
304      if (ignoreClasses.contains(e.getClassName()))
305         return "<ignored>";
306      var s = e.toString();
307      var i = s.indexOf('$');
308      if (i == -1)
309         return s;
310      var j = s.indexOf('(', i);
311      if (j == -1)
312         return s;  // Probably can't happen.
313      var s2 = s.substring(0, i);
314      var s3 = s.substring(j);
315      if (ignoreClasses.contains(s2))
316         return "<ignored>";
317      return s2 + "..." + s3;
318   }
319}