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