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}