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