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}